create-interview-cockpit 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,13 @@ import { parseInfraLabWorkspace } from "../infraLab";
4
4
  import {
5
5
  parseFrontendLabWorkspace,
6
6
  ISOLATED_MODULE_FEDERATION_LAB,
7
+ NEXTJS_MF_PLUGIN_LAB,
8
+ NEXTJS_MF_RUNTIME_LAB,
9
+ NEXTJS_MULTI_ZONES_LAB,
10
+ NEXTJS_MF_RUNTIME_API_LAB,
11
+ RSPACK_SHELL_LAB,
7
12
  } from "../reactLab";
13
+ import { BROWSER_SECURITY_TEMPLATES } from "../browserSecurityTemplates";
8
14
  import type { ContextFile } from "../types";
9
15
  import {
10
16
  Plus,
@@ -21,16 +27,20 @@ import {
21
27
  LinkIcon,
22
28
  Link2Off,
23
29
  Network,
30
+ Shield,
31
+ PenLine,
24
32
  } from "lucide-react";
25
33
 
26
34
  // ─── Helpers ─────────────────────────────────────────────
27
35
 
28
36
  const LAB_ORIGINS = new Set([
29
37
  "sandbox",
38
+ "browser-security",
30
39
  "infra",
31
40
  "react",
32
41
  "nextjs",
33
42
  "module-federation",
43
+ "canvas",
34
44
  ]);
35
45
 
36
46
  function isLabFile(cf: ContextFile) {
@@ -190,6 +200,7 @@ export default function LabsPanel() {
190
200
  openNextLab,
191
201
  openModuleFederationLab,
192
202
  openDeploymentLab,
203
+ openCanvasLab,
193
204
  removeQuestionFile,
194
205
  detachLabFile,
195
206
  attachLabFile,
@@ -239,19 +250,50 @@ export default function LabsPanel() {
239
250
  parsed.clientCode,
240
251
  parsed.clientLang,
241
252
  cf.id,
242
- parsed.clientType
243
- ? {
244
- clientType: parsed.clientType,
245
- reactFiles: parsed.reactFiles,
246
- reactActiveFile: parsed.reactActiveFile,
247
- }
248
- : undefined,
253
+ {
254
+ label: cf.label || cf.originalName,
255
+ origin:
256
+ cf.origin === "browser-security" ? "browser-security" : "sandbox",
257
+ ...(parsed.clientType
258
+ ? {
259
+ clientType: parsed.clientType,
260
+ reactFiles: parsed.reactFiles,
261
+ reactActiveFile: parsed.reactActiveFile,
262
+ }
263
+ : {}),
264
+ },
249
265
  );
250
266
  } catch {
251
267
  /* ignore */
252
268
  }
253
269
  };
254
270
 
271
+ const openBrowserSecurityTemplate = (templateId: string) => {
272
+ const template = BROWSER_SECURITY_TEMPLATES.find(
273
+ (item) => item.id === templateId,
274
+ );
275
+ if (!template) return;
276
+
277
+ openSandbox(
278
+ template.serverCode,
279
+ "javascript",
280
+ template.clientCode,
281
+ "javascript",
282
+ undefined,
283
+ {
284
+ label: template.label,
285
+ origin: "browser-security",
286
+ ...(template.clientType
287
+ ? {
288
+ clientType: template.clientType,
289
+ reactFiles: template.reactFiles,
290
+ reactActiveFile: template.reactActiveFile,
291
+ }
292
+ : {}),
293
+ },
294
+ );
295
+ };
296
+
255
297
  const openInfraFile = async (cf: ContextFile) => {
256
298
  const raw = await fetch(`/api/context-files/${cf.id}/content`)
257
299
  .then((r) => r.json())
@@ -372,6 +414,17 @@ export default function LabsPanel() {
372
414
  }
373
415
  };
374
416
 
417
+ const openCanvasFile = async (cf: ContextFile) => {
418
+ try {
419
+ const raw = await fetch(`/api/context-files/${cf.id}/content`)
420
+ .then((r) => r.json())
421
+ .then((d) => d.content as string);
422
+ openCanvasLab(raw, cf.id);
423
+ } catch {
424
+ /* ignore */
425
+ }
426
+ };
427
+
375
428
  // ── Section renderer ─────────────────────────────────────
376
429
 
377
430
  function Section({
@@ -531,6 +584,22 @@ export default function LabsPanel() {
531
584
  accentClass="text-slate-300"
532
585
  bgClass="bg-slate-500/10 border border-slate-500/20"
533
586
  />
587
+ <Section
588
+ title="Browser Security"
589
+ icon={Shield}
590
+ iconColor="text-cyan-400/70"
591
+ origin="browser-security"
592
+ emptyText="Save a browser security lab to reopen it here"
593
+ newLabMenu={BROWSER_SECURITY_TEMPLATES.map((template) => ({
594
+ label: template.label,
595
+ description: template.description,
596
+ onClick: () => openBrowserSecurityTemplate(template.id),
597
+ }))}
598
+ onOpen={openSandboxFile}
599
+ openTitle="Open in Browser Security Lab"
600
+ accentClass="text-cyan-200"
601
+ bgClass="bg-cyan-500/10 border border-cyan-500/20"
602
+ />
534
603
  <Section
535
604
  title="Infra Labs"
536
605
  icon={Globe}
@@ -590,12 +659,56 @@ export default function LabsPanel() {
590
659
  onClick: () =>
591
660
  openModuleFederationLab(ISOLATED_MODULE_FEDERATION_LAB),
592
661
  },
662
+ {
663
+ label: "Next.js MF — Plugin (Option A)",
664
+ description:
665
+ "Next.js shell + remote via built-in webpack.container.ModuleFederationPlugin",
666
+ onClick: () => openModuleFederationLab(NEXTJS_MF_PLUGIN_LAB),
667
+ },
668
+ {
669
+ label: "Next.js MF — Runtime Loader (Option B)",
670
+ description:
671
+ "Next.js shell with no plugin — loads remote via plain script injection",
672
+ onClick: () => openModuleFederationLab(NEXTJS_MF_RUNTIME_LAB),
673
+ },
674
+ {
675
+ label: "Next.js — Multi-Zones",
676
+ description:
677
+ "Two independent Next.js apps split by URL path via rewrites — Vercel recommended",
678
+ onClick: () => openModuleFederationLab(NEXTJS_MULTI_ZONES_LAB),
679
+ },
680
+ {
681
+ label: "Next.js — MF Runtime API",
682
+ description:
683
+ "@module-federation/enhanced/runtime inside a 'use client' component — no webpack config",
684
+ onClick: () =>
685
+ openModuleFederationLab(NEXTJS_MF_RUNTIME_API_LAB),
686
+ },
687
+ {
688
+ label: "Rspack Shell — Native MF 2.0",
689
+ description:
690
+ "Rspack as the host with built-in MF support; webpack app as the remote",
691
+ onClick: () => openModuleFederationLab(RSPACK_SHELL_LAB),
692
+ },
593
693
  ]}
594
694
  onOpen={openMFFile}
595
695
  openTitle="Open in Webpack Module Federation Lab"
596
696
  accentClass="text-emerald-200"
597
697
  bgClass="bg-emerald-500/10 border border-emerald-500/20"
598
698
  />
699
+ <Section
700
+ title="Canvas Labs"
701
+ icon={PenLine}
702
+ iconColor="text-orange-400/70"
703
+ origin="canvas"
704
+ emptyText="Save a canvas lab to reopen it here"
705
+ onNewLab={() => openCanvasLab()}
706
+ newLabTitle="Open Canvas Lab"
707
+ onOpen={openCanvasFile}
708
+ openTitle="Open in Canvas Lab"
709
+ accentClass="text-orange-200"
710
+ bgClass="bg-orange-500/10 border border-orange-500/20"
711
+ />
599
712
  </div>
600
713
  ) : (
601
714
  <div className="flex-1 flex items-center justify-center">
@@ -1,5 +1,5 @@
1
- import { useEffect, useState } from "react";
2
- import { X, MessageSquare, Check, Loader2 } from "lucide-react";
1
+ import { useMemo, useState } from "react";
2
+ import { X, MessageSquare, Check, Loader2, Search } from "lucide-react";
3
3
  import type { Question } from "../types";
4
4
  import { useStore } from "../store";
5
5
 
@@ -14,20 +14,10 @@ export default function LinkedConvosPicker({
14
14
  topicId,
15
15
  onClose,
16
16
  }: Props) {
17
- const { linkConversation, unlinkConversation } = useStore();
18
- const [siblings, setSiblings] = useState<Question[]>([]);
19
- const [loading, setLoading] = useState(true);
17
+ const { linkConversation, unlinkConversation, topics, questionsByTopic } =
18
+ useStore();
20
19
  const [pending, setPending] = useState<string | null>(null);
21
-
22
- useEffect(() => {
23
- fetch(`/api/topics/${topicId}/questions`)
24
- .then((r) => r.json())
25
- .then((qs: Question[]) => {
26
- // Exclude the current question itself
27
- setSiblings(qs.filter((q2) => q2.id !== question.id));
28
- })
29
- .finally(() => setLoading(false));
30
- }, [topicId, question.id]);
20
+ const [search, setSearch] = useState("");
31
21
 
32
22
  const linked = new Set(question.linkedConversationIds ?? []);
33
23
 
@@ -44,9 +34,35 @@ export default function LinkedConvosPicker({
44
34
  }
45
35
  };
46
36
 
37
+ // Current topic first, then the rest alphabetically
38
+ const grouped = useMemo(() => {
39
+ const q = search.toLowerCase().trim();
40
+ const sortedTopics = [
41
+ topics.find((t) => t.id === topicId),
42
+ ...topics.filter((t) => t.id !== topicId),
43
+ ].filter(Boolean);
44
+
45
+ return sortedTopics
46
+ .map((topic) => ({
47
+ topic: topic!,
48
+ questions: (questionsByTopic[topic!.id] ?? []).filter(
49
+ (q2) =>
50
+ q2.id !== question.id &&
51
+ (q ? q2.title.toLowerCase().includes(q) : true),
52
+ ),
53
+ }))
54
+ .filter((g) => g.questions.length > 0);
55
+ }, [topics, questionsByTopic, topicId, question.id, search]);
56
+
47
57
  return (
48
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
49
- <div className="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-full max-w-md mx-4 flex flex-col max-h-[70vh]">
58
+ <div
59
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
60
+ onClick={onClose}
61
+ >
62
+ <div
63
+ className="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-full max-w-md mx-4 flex flex-col max-h-[75vh]"
64
+ onClick={(e) => e.stopPropagation()}
65
+ >
50
66
  {/* Header */}
51
67
  <div className="flex items-center gap-2 px-4 py-3 border-b border-slate-700 shrink-0">
52
68
  <MessageSquare className="w-4 h-4 text-violet-400 shrink-0" />
@@ -61,58 +77,100 @@ export default function LinkedConvosPicker({
61
77
  </button>
62
78
  </div>
63
79
 
80
+ {/* Search */}
81
+ <div className="px-3 py-2 border-b border-slate-800 shrink-0">
82
+ <div className="flex items-center gap-2 bg-slate-800 rounded-lg px-2.5 py-1.5">
83
+ <Search className="w-3 h-3 text-slate-500 shrink-0" />
84
+ <input
85
+ autoFocus
86
+ value={search}
87
+ onChange={(e) => setSearch(e.target.value)}
88
+ placeholder="Search conversations…"
89
+ className="flex-1 bg-transparent text-xs text-slate-300 placeholder-slate-600 focus:outline-none"
90
+ />
91
+ {search && (
92
+ <button
93
+ onClick={() => setSearch("")}
94
+ className="text-slate-600 hover:text-slate-400 transition-colors"
95
+ >
96
+ <X className="w-3 h-3" />
97
+ </button>
98
+ )}
99
+ </div>
100
+ </div>
101
+
64
102
  {/* Body */}
65
- <div className="flex-1 min-h-0 overflow-y-auto p-3 space-y-1">
66
- {loading && (
67
- <div className="flex items-center justify-center py-8">
68
- <Loader2 className="w-5 h-5 text-violet-400 animate-spin" />
69
- </div>
70
- )}
71
- {!loading && siblings.length === 0 && (
103
+ <div className="flex-1 min-h-0 overflow-y-auto p-3 space-y-4">
104
+ {grouped.length === 0 && (
72
105
  <p className="text-xs text-slate-500 text-center py-8">
73
- No other conversations in this topic yet.
106
+ {search
107
+ ? "No conversations match your search."
108
+ : "No other conversations yet."}
74
109
  </p>
75
110
  )}
76
- {!loading &&
77
- siblings.map((sibling) => {
78
- const isLinked = linked.has(sibling.id);
79
- const isPending = pending === sibling.id;
80
- const msgCount = sibling.messages?.length ?? 0;
81
- return (
82
- <button
83
- key={sibling.id}
84
- onClick={() => toggle(sibling.id)}
85
- disabled={isPending}
86
- className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors ${
87
- isLinked
88
- ? "bg-violet-500/15 border border-violet-500/30 hover:bg-violet-500/20"
89
- : "bg-slate-800/50 border border-transparent hover:bg-slate-800"
111
+
112
+ {grouped.map(({ topic, questions }) => (
113
+ <div key={topic.id}>
114
+ {/* Topic group header */}
115
+ <div className="flex items-center gap-2 mb-1.5">
116
+ <span
117
+ className={`text-[10px] font-semibold uppercase tracking-wide shrink-0 ${
118
+ topic.id === topicId ? "text-violet-400" : "text-slate-500"
90
119
  }`}
91
120
  >
92
- <div
93
- className={`w-4 h-4 rounded border shrink-0 flex items-center justify-center transition-colors ${
94
- isLinked
95
- ? "bg-violet-500 border-violet-500"
96
- : "border-slate-600"
97
- }`}
98
- >
99
- {isPending ? (
100
- <Loader2 className="w-2.5 h-2.5 animate-spin text-white" />
101
- ) : isLinked ? (
102
- <Check className="w-2.5 h-2.5 text-white" />
103
- ) : null}
104
- </div>
105
- <div className="flex-1 min-w-0">
106
- <p className="text-xs font-medium text-slate-200 truncate">
107
- {sibling.title}
108
- </p>
109
- <p className="text-[10px] text-slate-500 mt-0.5">
110
- {msgCount} message{msgCount !== 1 ? "s" : ""}
111
- </p>
112
- </div>
113
- </button>
114
- );
115
- })}
121
+ {topic.name}
122
+ </span>
123
+ {topic.id === topicId && (
124
+ <span className="text-[9px] bg-violet-500/20 text-violet-400 px-1.5 py-0.5 rounded shrink-0">
125
+ current
126
+ </span>
127
+ )}
128
+ <div className="flex-1 h-px bg-slate-800" />
129
+ </div>
130
+
131
+ <div className="space-y-1">
132
+ {questions.map((sibling) => {
133
+ const isLinked = linked.has(sibling.id);
134
+ const isPending = pending === sibling.id;
135
+ const msgCount = sibling.messages?.length ?? 0;
136
+ return (
137
+ <button
138
+ key={sibling.id}
139
+ onClick={() => toggle(sibling.id)}
140
+ disabled={isPending}
141
+ className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors ${
142
+ isLinked
143
+ ? "bg-violet-500/15 border border-violet-500/30 hover:bg-violet-500/20"
144
+ : "bg-slate-800/50 border border-transparent hover:bg-slate-800"
145
+ }`}
146
+ >
147
+ <div
148
+ className={`w-4 h-4 rounded border shrink-0 flex items-center justify-center transition-colors ${
149
+ isLinked
150
+ ? "bg-violet-500 border-violet-500"
151
+ : "border-slate-600"
152
+ }`}
153
+ >
154
+ {isPending ? (
155
+ <Loader2 className="w-2.5 h-2.5 animate-spin text-white" />
156
+ ) : isLinked ? (
157
+ <Check className="w-2.5 h-2.5 text-white" />
158
+ ) : null}
159
+ </div>
160
+ <div className="flex-1 min-w-0">
161
+ <p className="text-xs font-medium text-slate-200 truncate">
162
+ {sibling.title}
163
+ </p>
164
+ <p className="text-[10px] text-slate-500 mt-0.5">
165
+ {msgCount} message{msgCount !== 1 ? "s" : ""}
166
+ </p>
167
+ </div>
168
+ </button>
169
+ );
170
+ })}
171
+ </div>
172
+ </div>
173
+ ))}
116
174
  </div>
117
175
 
118
176
  {/* Footer hint */}
@@ -3,6 +3,7 @@ import { useStore } from "../store";
3
3
  import type { Question } from "../types";
4
4
  import FileAttachments from "./FileAttachments";
5
5
  import WorkspaceSwitcher from "./WorkspaceSwitcher";
6
+
6
7
  import {
7
8
  ChevronRight,
8
9
  ChevronDown,
@@ -20,10 +21,46 @@ import {
20
21
  SlidersHorizontal,
21
22
  ArrowRightLeft,
22
23
  MoreHorizontal,
24
+ Download,
25
+ Link,
23
26
  } from "lucide-react";
24
27
 
25
28
  const ROOT_PARENT_VALUE = "__root__";
26
29
 
30
+ // ── Download helpers ─────────────────────────────────────────────────────────
31
+
32
+ function downloadJson(data: unknown, filename: string) {
33
+ const blob = new Blob([JSON.stringify(data, null, 2)], {
34
+ type: "application/json",
35
+ });
36
+ const url = URL.createObjectURL(blob);
37
+ const a = document.createElement("a");
38
+ a.href = url;
39
+ a.download = filename;
40
+ a.click();
41
+ URL.revokeObjectURL(url);
42
+ }
43
+
44
+ function buildQuestionExport(q: Question, allQuestions: Question[]): object {
45
+ const children = allQuestions
46
+ .filter((c) => c.parentQuestionId === q.id)
47
+ .map((c) => buildQuestionExport(c, allQuestions));
48
+ return {
49
+ id: q.id,
50
+ title: q.title,
51
+ topicId: q.topicId,
52
+ parentQuestionId: q.parentQuestionId ?? null,
53
+ systemContext: q.systemContext,
54
+ contextFiles: q.contextFiles,
55
+ codeContextFiles: q.codeContextFiles,
56
+ messages: q.messages,
57
+ annotations: q.annotations ?? [],
58
+ linkedConversationIds: q.linkedConversationIds ?? [],
59
+ createdAt: q.createdAt,
60
+ ...(children.length > 0 ? { children } : {}),
61
+ };
62
+ }
63
+
27
64
  export default function Sidebar() {
28
65
  const {
29
66
  topics,
@@ -56,6 +93,9 @@ export default function Sidebar() {
56
93
  uploadWorkspaceFiles,
57
94
  removeWorkspaceFile,
58
95
  updateTopicSystemContext,
96
+ currentQuestion,
97
+ linkConversation,
98
+ unlinkConversation,
59
99
  } = useStore();
60
100
 
61
101
  const [newTopicName, setNewTopicName] = useState("");
@@ -114,6 +154,9 @@ export default function Sidebar() {
114
154
 
115
155
  // Drive subfolder navigator
116
156
  const activeWs = workspaces.find((w) => w.id === activeWorkspaceId);
157
+ const activeWsSortOrder = (activeWs?.questionSortOrder ?? "name") as
158
+ | "name"
159
+ | "createdAt";
117
160
  const isDriveWs =
118
161
  activeWs?.type === "google_drive" && !!activeWs.driveConfig?.folderId;
119
162
  const currentSubFolder = activeWs?.driveConfig?.subFolderId
@@ -460,6 +503,53 @@ export default function Sidebar() {
460
503
  <ArrowRightLeft className="w-3 h-3" /> Move
461
504
  </button>
462
505
  <div className="border-t border-slate-700 my-0.5" />
506
+ <button
507
+ onClick={() => {
508
+ setOpenMenuQuestionId(null);
509
+ const allQ = questionsByTopic[topicId] ?? [];
510
+ downloadJson(
511
+ buildQuestionExport(q, allQ),
512
+ `${q.title.replace(/[^a-z0-9]+/gi, "-")}.json`,
513
+ );
514
+ }}
515
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-white transition-colors"
516
+ >
517
+ <Download className="w-3 h-3" /> Download
518
+ </button>
519
+ {/* Link to current — only shown when a different question is open */}
520
+ {currentQuestion &&
521
+ currentQuestion.id !== q.id &&
522
+ (() => {
523
+ const alreadyLinked = (
524
+ currentQuestion.linkedConversationIds ?? []
525
+ ).includes(q.id);
526
+ return (
527
+ <>
528
+ <div className="border-t border-slate-700 my-0.5" />
529
+ <button
530
+ onClick={() => {
531
+ setOpenMenuQuestionId(null);
532
+ if (alreadyLinked) {
533
+ unlinkConversation(currentQuestion.id, q.id);
534
+ } else {
535
+ linkConversation(currentQuestion.id, q.id);
536
+ }
537
+ }}
538
+ className={`w-full flex items-center gap-2 px-3 py-1.5 text-xs hover:bg-slate-700 transition-colors ${
539
+ alreadyLinked
540
+ ? "text-violet-400 hover:text-violet-300"
541
+ : "text-slate-300 hover:text-white"
542
+ }`}
543
+ >
544
+ <Link className="w-3 h-3" />
545
+ {alreadyLinked
546
+ ? "Unlink from current"
547
+ : "Link to current"}
548
+ </button>
549
+ </>
550
+ );
551
+ })()}
552
+ <div className="border-t border-slate-700 my-0.5" />
463
553
  <button
464
554
  onClick={() => {
465
555
  setOpenMenuQuestionId(null);
@@ -717,7 +807,15 @@ export default function Sidebar() {
717
807
  )
718
808
  .map((topic) => {
719
809
  const isExpanded = expandedTopics.includes(topic.id);
720
- const questions = questionsByTopic[topic.id] || [];
810
+ const questions = [...(questionsByTopic[topic.id] || [])].sort(
811
+ (a, b) =>
812
+ activeWsSortOrder === "createdAt"
813
+ ? a.createdAt.localeCompare(b.createdAt)
814
+ : a.title.localeCompare(b.title, undefined, {
815
+ numeric: true,
816
+ sensitivity: "base",
817
+ }),
818
+ );
721
819
 
722
820
  return (
723
821
  <div key={topic.id}>
@@ -789,6 +887,32 @@ export default function Sidebar() {
789
887
  >
790
888
  <Trash2 className="w-3 h-3" />
791
889
  </button>
890
+ <button
891
+ onClick={(e) => {
892
+ e.stopPropagation();
893
+ const topicQuestions = questionsByTopic[topic.id] ?? [];
894
+ const rootQuestions = topicQuestions.filter(
895
+ (q) => !q.parentQuestionId,
896
+ );
897
+ downloadJson(
898
+ {
899
+ id: topic.id,
900
+ name: topic.name,
901
+ systemContext: topic.systemContext ?? "",
902
+ contextFiles: topic.contextFiles,
903
+ createdAt: topic.createdAt,
904
+ questions: rootQuestions.map((q) =>
905
+ buildQuestionExport(q, topicQuestions),
906
+ ),
907
+ },
908
+ `${topic.name.replace(/[^a-z0-9]+/gi, "-")}.json`,
909
+ );
910
+ }}
911
+ className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-cyan-400 transition-all"
912
+ title="Download topic as JSON"
913
+ >
914
+ <Download className="w-3 h-3" />
915
+ </button>
792
916
  </div>
793
917
 
794
918
  {/* Questions list */}
@@ -27,6 +27,7 @@ export default function WorkspaceSwitcher() {
27
27
  createWorkspace,
28
28
  deleteWorkspace,
29
29
  renameWorkspace,
30
+ patchWorkspace,
30
31
  syncWorkspace,
31
32
  linkDriveFolder,
32
33
  attachDriveFolder,
@@ -814,6 +815,41 @@ export default function WorkspaceSwitcher() {
814
815
  )}
815
816
  </div>
816
817
  )}
818
+
819
+ {/* Question sort order */}
820
+ <div
821
+ className="mt-1 ml-5 flex items-center gap-1"
822
+ onClick={(e) => e.stopPropagation()}
823
+ >
824
+ <span className="text-[10px] text-slate-600">Order:</span>
825
+ <button
826
+ onClick={() =>
827
+ patchWorkspace(ws.id, { questionSortOrder: "name" })
828
+ }
829
+ className={`text-[10px] px-1 rounded transition-colors ${
830
+ (ws.questionSortOrder ?? "name") === "name"
831
+ ? "text-cyan-400"
832
+ : "text-slate-600 hover:text-slate-400"
833
+ }`}
834
+ >
835
+ Name
836
+ </button>
837
+ <span className="text-[10px] text-slate-700">·</span>
838
+ <button
839
+ onClick={() =>
840
+ patchWorkspace(ws.id, {
841
+ questionSortOrder: "createdAt",
842
+ })
843
+ }
844
+ className={`text-[10px] px-1 rounded transition-colors ${
845
+ ws.questionSortOrder === "createdAt"
846
+ ? "text-cyan-400"
847
+ : "text-slate-600 hover:text-slate-400"
848
+ }`}
849
+ >
850
+ Date created
851
+ </button>
852
+ </div>
817
853
  </div>
818
854
  ))}
819
855
  </div>