create-interview-cockpit 0.4.0 → 0.5.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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/template/client/package-lock.json +19 -0
  3. package/template/client/package.json +3 -0
  4. package/template/client/src/App.tsx +17 -0
  5. package/template/client/src/api.ts +135 -0
  6. package/template/client/src/components/AiSettingsModal.tsx +218 -4
  7. package/template/client/src/components/AnnotationDialog.tsx +3 -9
  8. package/template/client/src/components/ChatMessage.tsx +110 -27
  9. package/template/client/src/components/ChatView.tsx +69 -4
  10. package/template/client/src/components/CodeContextPanel.tsx +297 -0
  11. package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
  12. package/template/client/src/components/CodeRunnerModal.tsx +1549 -0
  13. package/template/client/src/components/DocRefModal.tsx +502 -0
  14. package/template/client/src/components/FileAttachments.tsx +109 -9
  15. package/template/client/src/components/FilePickerModal.tsx +181 -0
  16. package/template/client/src/components/FileViewerModal.tsx +406 -28
  17. package/template/client/src/components/MarkdownRenderer.tsx +205 -2
  18. package/template/client/src/components/Sidebar.tsx +213 -127
  19. package/template/client/src/components/TextAnnotator.tsx +8 -15
  20. package/template/client/src/components/VizCraftEmbed.tsx +162 -19
  21. package/template/client/src/store.ts +201 -0
  22. package/template/client/src/types.ts +8 -0
  23. package/template/cockpit.json +1 -1
  24. package/template/package.json +1 -1
  25. package/template/server/src/google-drive.ts +109 -1
  26. package/template/server/src/index.ts +1107 -46
  27. package/template/server/src/storage.ts +263 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-interview-cockpit",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Scaffold a personal AI-powered interview prep cockpit",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,12 +7,15 @@
7
7
  "name": "interview-cockpit-client",
8
8
  "dependencies": {
9
9
  "@ai-sdk/react": "^3.0.170",
10
+ "@types/prismjs": "^1.26.6",
10
11
  "ai": "^6.0.168",
11
12
  "lucide-react": "^0.460.0",
12
13
  "mermaid": "^11.4.0",
14
+ "prismjs": "^1.30.0",
13
15
  "react": "^19.0.0",
14
16
  "react-dom": "^19.0.0",
15
17
  "react-markdown": "^9.0.0",
18
+ "react-simple-code-editor": "^0.14.1",
16
19
  "react-syntax-highlighter": "^15.6.1",
17
20
  "remark-gfm": "^4.0.0",
18
21
  "vizcraft": "^1.17.0",
@@ -1749,6 +1752,12 @@
1749
1752
  "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
1750
1753
  "license": "MIT"
1751
1754
  },
1755
+ "node_modules/@types/prismjs": {
1756
+ "version": "1.26.6",
1757
+ "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz",
1758
+ "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==",
1759
+ "license": "MIT"
1760
+ },
1752
1761
  "node_modules/@types/react": {
1753
1762
  "version": "19.2.14",
1754
1763
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
@@ -4915,6 +4924,16 @@
4915
4924
  "node": ">=0.10.0"
4916
4925
  }
4917
4926
  },
4927
+ "node_modules/react-simple-code-editor": {
4928
+ "version": "0.14.1",
4929
+ "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.14.1.tgz",
4930
+ "integrity": "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==",
4931
+ "license": "MIT",
4932
+ "peerDependencies": {
4933
+ "react": ">=16.8.0",
4934
+ "react-dom": ">=16.8.0"
4935
+ }
4936
+ },
4918
4937
  "node_modules/react-syntax-highlighter": {
4919
4938
  "version": "15.6.6",
4920
4939
  "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz",
@@ -9,12 +9,15 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "@ai-sdk/react": "^3.0.170",
12
+ "@types/prismjs": "^1.26.6",
12
13
  "ai": "^6.0.168",
13
14
  "lucide-react": "^0.460.0",
14
15
  "mermaid": "^11.4.0",
16
+ "prismjs": "^1.30.0",
15
17
  "react": "^19.0.0",
16
18
  "react-dom": "^19.0.0",
17
19
  "react-markdown": "^9.0.0",
20
+ "react-simple-code-editor": "^0.14.1",
18
21
  "react-syntax-highlighter": "^15.6.1",
19
22
  "remark-gfm": "^4.0.0",
20
23
  "vizcraft": "^1.17.0",
@@ -4,7 +4,9 @@ import Sidebar from "./components/Sidebar";
4
4
  import ChatView from "./components/ChatView";
5
5
  import CodeContextPanel from "./components/CodeContextPanel";
6
6
  import FileViewerModal from "./components/FileViewerModal";
7
+ import DocRefModal from "./components/DocRefModal";
7
8
  import AiSettingsModal from "./components/AiSettingsModal";
9
+ import CodeRunnerModal from "./components/CodeRunnerModal";
8
10
  import { Code, Plane, PanelLeftClose, PanelLeft, Settings } from "lucide-react";
9
11
 
10
12
  export default function App() {
@@ -12,6 +14,7 @@ export default function App() {
12
14
  fetchTopics,
13
15
  fetchWorkspaces,
14
16
  fetchAiSettings,
17
+ fetchWorkspaceFiles,
15
18
  fetchQuestions,
16
19
  selectQuestion,
17
20
  currentQuestion,
@@ -21,9 +24,13 @@ export default function App() {
21
24
  toggleSidebar,
22
25
  viewingFile,
23
26
  closeFileViewer,
27
+ viewingDoc,
28
+ closeDocViewer,
24
29
  showSettings,
25
30
  openSettings,
26
31
  closeSettings,
32
+ showCodeRunner,
33
+ closeCodeRunner,
27
34
  } = useStore();
28
35
 
29
36
  useEffect(() => {
@@ -31,6 +38,7 @@ export default function App() {
31
38
  await fetchWorkspaces();
32
39
  await fetchTopics();
33
40
  fetchAiSettings();
41
+ fetchWorkspaceFiles();
34
42
  // Restore last-viewed question after page refresh
35
43
  const topicId = sessionStorage.getItem("lastTopicId");
36
44
  const questionId = sessionStorage.getItem("lastQuestionId");
@@ -139,7 +147,16 @@ export default function App() {
139
147
  {viewingFile && (
140
148
  <FileViewerModal filePath={viewingFile} onClose={closeFileViewer} />
141
149
  )}
150
+ {viewingDoc && (
151
+ <DocRefModal
152
+ fileId={viewingDoc.fileId}
153
+ quote={viewingDoc.quote}
154
+ fileName={viewingDoc.fileName}
155
+ onClose={closeDocViewer}
156
+ />
157
+ )}
142
158
  {showSettings && <AiSettingsModal />}
159
+ {showCodeRunner && <CodeRunnerModal />}
143
160
  </div>
144
161
  );
145
162
  }
@@ -22,6 +22,14 @@ export interface AiSettings {
22
22
  vizGuide: string;
23
23
  /** All user-selectable prompt groups. Add new entries here to extend the UI. */
24
24
  promptGroups: Record<string, PromptGroup>;
25
+ /** When true, preference prompt texts are appended to every message (not just on change). */
26
+ alwaysSendPrefsDefault?: boolean;
27
+ /** Gemini thinking budget in tokens. 0 = disabled. Only applies when provider is google/gemini. */
28
+ thinkingBudget?: number;
29
+ /** Read-only: current AI provider from .env (openai | google | anthropic). */
30
+ provider?: string;
31
+ /** Read-only: current model name from .env. */
32
+ model?: string;
25
33
  }
26
34
 
27
35
  export async function fetchAiSettings(): Promise<AiSettings> {
@@ -82,6 +90,10 @@ export async function uploadTopicFiles(
82
90
  method: "POST",
83
91
  body: form,
84
92
  });
93
+ if (!res.ok) {
94
+ const err = await res.json().catch(() => ({ error: "Upload failed" }));
95
+ throw new Error(err.error ?? "Upload failed");
96
+ }
85
97
  return res.json();
86
98
  }
87
99
 
@@ -94,6 +106,35 @@ export async function deleteTopicFile(
94
106
  });
95
107
  }
96
108
 
109
+ // --- Workspace Context Files ---
110
+
111
+ export async function fetchWorkspaceFiles(): Promise<ContextFile[]> {
112
+ const res = await fetch(`${BASE}/workspace/context-files`);
113
+ return res.json();
114
+ }
115
+
116
+ export async function uploadWorkspaceFiles(
117
+ files: FileList | File[],
118
+ ): Promise<ContextFile[]> {
119
+ const form = new FormData();
120
+ for (const file of files) form.append("files", file);
121
+ const res = await fetch(`${BASE}/workspace/context-files`, {
122
+ method: "POST",
123
+ body: form,
124
+ });
125
+ if (!res.ok) {
126
+ const err = await res.json().catch(() => ({ error: "Upload failed" }));
127
+ throw new Error(err.error ?? "Upload failed");
128
+ }
129
+ return res.json();
130
+ }
131
+
132
+ export async function deleteWorkspaceFile(fileId: string): Promise<void> {
133
+ await fetch(`${BASE}/workspace/context-files/${fileId}`, {
134
+ method: "DELETE",
135
+ });
136
+ }
137
+
97
138
  // --- Questions ---
98
139
 
99
140
  export async function fetchQuestions(topicId: string): Promise<Question[]> {
@@ -151,6 +192,10 @@ export async function uploadQuestionFiles(
151
192
  method: "POST",
152
193
  body: form,
153
194
  });
195
+ if (!res.ok) {
196
+ const err = await res.json().catch(() => ({ error: "Upload failed" }));
197
+ throw new Error(err.error ?? "Upload failed");
198
+ }
154
199
  return res.json();
155
200
  }
156
201
 
@@ -163,6 +208,96 @@ export async function deleteQuestionFile(
163
208
  });
164
209
  }
165
210
 
211
+ export interface PickableFile {
212
+ fileId: string;
213
+ originalName: string;
214
+ source: "workspace" | "topic" | "question";
215
+ sourceName: string;
216
+ }
217
+
218
+ export async function fetchAllContextFiles(): Promise<PickableFile[]> {
219
+ const res = await fetch(`${BASE}/context-files/all`);
220
+ return res.json();
221
+ }
222
+
223
+ export async function linkFileToTopic(
224
+ topicId: string,
225
+ fileId: string,
226
+ originalName: string,
227
+ ): Promise<ContextFile> {
228
+ const res = await fetch(`${BASE}/topics/${topicId}/context-files/link`, {
229
+ method: "POST",
230
+ headers: { "Content-Type": "application/json" },
231
+ body: JSON.stringify({ fileId, originalName }),
232
+ });
233
+ return res.json();
234
+ }
235
+
236
+ export async function linkFileToQuestion(
237
+ questionId: string,
238
+ fileId: string,
239
+ originalName: string,
240
+ ): Promise<ContextFile> {
241
+ const res = await fetch(
242
+ `${BASE}/questions/${questionId}/context-files/link`,
243
+ {
244
+ method: "POST",
245
+ headers: { "Content-Type": "application/json" },
246
+ body: JSON.stringify({ fileId, originalName }),
247
+ },
248
+ );
249
+ return res.json();
250
+ }
251
+
252
+ export async function saveCodeSnippet(
253
+ questionId: string,
254
+ code: string,
255
+ language: string,
256
+ label: string,
257
+ origin: "user" | "ai" | "sandbox",
258
+ ): Promise<ContextFile> {
259
+ const res = await fetch(`${BASE}/questions/${questionId}/save-code-snippet`, {
260
+ method: "POST",
261
+ headers: { "Content-Type": "application/json" },
262
+ body: JSON.stringify({ code, language, label, origin }),
263
+ });
264
+ if (!res.ok) throw new Error(await res.text());
265
+ return res.json();
266
+ }
267
+
268
+ export async function overwriteContextFileContent(
269
+ questionId: string,
270
+ fileId: string,
271
+ content: string,
272
+ ): Promise<void> {
273
+ const res = await fetch(
274
+ `${BASE}/questions/${questionId}/context-files/${fileId}/content`,
275
+ {
276
+ method: "PUT",
277
+ headers: { "Content-Type": "application/json" },
278
+ body: JSON.stringify({ code: content }),
279
+ },
280
+ );
281
+ if (!res.ok) throw new Error(await res.text());
282
+ }
283
+
284
+ export async function renameContextFile(
285
+ questionId: string,
286
+ fileId: string,
287
+ label: string,
288
+ ): Promise<ContextFile> {
289
+ const res = await fetch(
290
+ `${BASE}/questions/${questionId}/context-files/${fileId}`,
291
+ {
292
+ method: "PATCH",
293
+ headers: { "Content-Type": "application/json" },
294
+ body: JSON.stringify({ label }),
295
+ },
296
+ );
297
+ if (!res.ok) throw new Error(await res.text());
298
+ return res.json();
299
+ }
300
+
166
301
  // --- Code Context ---
167
302
 
168
303
  export async function fetchCodeContextTree(): Promise<string[]> {
@@ -21,8 +21,10 @@ const BASELINE: AiSettings = {
21
21
  concise: { maxOutputTokens: 1000, maxSteps: 3 },
22
22
  moderate: { maxOutputTokens: 1000, maxSteps: 5 },
23
23
  normal: { maxOutputTokens: 3000, maxSteps: 5 },
24
+ brief: { maxOutputTokens: 10000, maxSteps: 5 },
24
25
  },
25
26
  vizGuide: "",
27
+ alwaysSendPrefsDefault: false,
26
28
  promptGroups: {
27
29
  length: {
28
30
  label: "Response Length",
@@ -355,6 +357,15 @@ export default function AiSettingsModal() {
355
357
  const [saved, setSaved] = useState(false);
356
358
  const overlayRef = useRef<HTMLDivElement>(null);
357
359
 
360
+ // ── New profile form state ────────────────────────────────────
361
+ const [showNewProfileForm, setShowNewProfileForm] = useState(false);
362
+ const [newProfileKey, setNewProfileKey] = useState("");
363
+
364
+ function resetNewProfileForm() {
365
+ setShowNewProfileForm(false);
366
+ setNewProfileKey("");
367
+ }
368
+
358
369
  // ── New group form state ─────────────────────
359
370
  const [showNewGroupForm, setShowNewGroupForm] = useState(false);
360
371
  const [newGroupKey, setNewGroupKey] = useState("");
@@ -406,6 +417,26 @@ export default function AiSettingsModal() {
406
417
  }));
407
418
  }
408
419
 
420
+ function addProfile(key: string) {
421
+ const k = key.trim().toLowerCase().replace(/\s+/g, "-");
422
+ if (!k || k in draft.responseProfiles) return;
423
+ setDraft((d) => ({
424
+ ...d,
425
+ responseProfiles: {
426
+ ...d.responseProfiles,
427
+ [k]: { maxOutputTokens: 2000, maxSteps: 5 },
428
+ },
429
+ }));
430
+ }
431
+
432
+ function removeProfile(key: string) {
433
+ setDraft((d) => {
434
+ const next = { ...d.responseProfiles };
435
+ delete next[key];
436
+ return { ...d, responseProfiles: next };
437
+ });
438
+ }
439
+
409
440
  function patchGroupOption(groupKey: string, optKey: string, value: string) {
410
441
  setDraft((d) => ({
411
442
  ...d,
@@ -585,12 +616,24 @@ export default function AiSettingsModal() {
585
616
 
586
617
  {/* ── Response Profiles ─────────────────────────────── */}
587
618
  <Section title="Response Profiles (token limits per length setting)">
588
- <div className="grid grid-cols-3 gap-4">
619
+ <div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-4">
589
620
  {profileKeys.map((key) => (
590
621
  <div key={key} className="space-y-3">
591
- <p className="text-xs font-semibold text-cyan-400 capitalize">
592
- {key}
593
- </p>
622
+ <div className="flex items-center justify-between">
623
+ <p className="text-xs font-semibold text-cyan-400 capitalize">
624
+ {key}
625
+ </p>
626
+ {profileKeys.length > 1 && (
627
+ <button
628
+ type="button"
629
+ title={`Remove "${key}" profile`}
630
+ onClick={() => removeProfile(key)}
631
+ className="p-0.5 rounded text-slate-600 hover:text-red-400 hover:bg-red-500/10 transition-colors"
632
+ >
633
+ <Trash2 className="w-3 h-3" />
634
+ </button>
635
+ )}
636
+ </div>
594
637
  <div>
595
638
  <Label>Max Output Tokens</Label>
596
639
  <NumberInput
@@ -615,6 +658,62 @@ export default function AiSettingsModal() {
615
658
  </div>
616
659
  ))}
617
660
  </div>
661
+
662
+ {/* Add profile */}
663
+ {showNewProfileForm ? (
664
+ <div className="mt-4 border border-slate-700 rounded-lg p-3 space-y-2 bg-slate-800/40">
665
+ <p className="text-xs font-medium text-slate-400">
666
+ New profile key
667
+ </p>
668
+ <div className="flex gap-2">
669
+ <input
670
+ type="text"
671
+ value={newProfileKey}
672
+ onChange={(e) =>
673
+ setNewProfileKey(
674
+ e.target.value.toLowerCase().replace(/\s+/g, "-"),
675
+ )
676
+ }
677
+ placeholder="e.g. brief"
678
+ className="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-sm text-slate-200 focus:outline-none focus:border-cyan-500"
679
+ />
680
+ <button
681
+ type="button"
682
+ onClick={() => {
683
+ addProfile(newProfileKey);
684
+ resetNewProfileForm();
685
+ }}
686
+ disabled={
687
+ !newProfileKey.trim() ||
688
+ newProfileKey.trim() in draft.responseProfiles
689
+ }
690
+ className="px-3 py-1 text-xs rounded bg-cyan-700 hover:bg-cyan-600 disabled:opacity-40 text-white transition-colors"
691
+ >
692
+ Add
693
+ </button>
694
+ <button
695
+ type="button"
696
+ onClick={resetNewProfileForm}
697
+ className="px-3 py-1 text-xs rounded text-slate-400 hover:text-slate-200 hover:bg-slate-700 transition-colors"
698
+ >
699
+ Cancel
700
+ </button>
701
+ </div>
702
+ {newProfileKey.trim() in draft.responseProfiles && (
703
+ <p className="text-xs text-red-400">
704
+ A profile with that key already exists.
705
+ </p>
706
+ )}
707
+ </div>
708
+ ) : (
709
+ <button
710
+ type="button"
711
+ onClick={() => setShowNewProfileForm(true)}
712
+ className="mt-3 flex items-center gap-1.5 text-xs text-slate-500 hover:text-cyan-400 transition-colors"
713
+ >
714
+ <Plus className="w-3.5 h-3.5" /> Add profile
715
+ </button>
716
+ )}
618
717
  </Section>
619
718
 
620
719
  {/* ── Prompt Groups ─────────────────────────────────── */}
@@ -638,6 +737,121 @@ export default function AiSettingsModal() {
638
737
  />
639
738
  ))}
640
739
 
740
+ {/* ── Preferences behaviour ─────────────────────────── */}
741
+ <Section title="Preference Sending Behaviour">
742
+ <div className="flex items-center justify-between">
743
+ <div>
744
+ <p className="text-sm text-slate-200">
745
+ Always send preferences
746
+ </p>
747
+ <p className="text-xs text-slate-500 mt-0.5">
748
+ When on, preference prompt texts are appended to every
749
+ message. When off, they are only sent when you change a
750
+ setting (saves tokens).
751
+ </p>
752
+ </div>
753
+ <button
754
+ type="button"
755
+ onClick={() =>
756
+ patchTop(
757
+ "alwaysSendPrefsDefault",
758
+ !draft.alwaysSendPrefsDefault,
759
+ )
760
+ }
761
+ className={`relative shrink-0 w-10 h-5 rounded-full transition-colors ${
762
+ draft.alwaysSendPrefsDefault ? "bg-amber-500" : "bg-slate-700"
763
+ }`}
764
+ >
765
+ <span
766
+ className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${
767
+ draft.alwaysSendPrefsDefault ? "translate-x-5" : ""
768
+ }`}
769
+ />
770
+ </button>
771
+ </div>
772
+ </Section>
773
+
774
+ {/* ── Model / Thinking ───────────────────────────────── */}
775
+ <Section title="Model & Thinking" defaultOpen={false}>
776
+ {/* Current model info (read-only) */}
777
+ <div className="flex gap-3 mb-4">
778
+ <div className="flex-1">
779
+ <Label>Provider</Label>
780
+ <div className="bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm text-slate-400 font-mono">
781
+ {draft.provider || "openai"}
782
+ </div>
783
+ </div>
784
+ <div className="flex-1">
785
+ <Label>Model</Label>
786
+ <div className="bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm text-slate-400 font-mono truncate">
787
+ {draft.model || "(default)"}
788
+ </div>
789
+ </div>
790
+ </div>
791
+
792
+ {/* Thinking budget — only useful for Google/Gemini */}
793
+ {["google", "gemini"].includes(draft.provider ?? "") ? (
794
+ <div>
795
+ <Label>Thinking Budget</Label>
796
+ <p className="text-xs text-slate-500 mb-2">
797
+ Number of tokens Gemini can use for internal reasoning before
798
+ responding. 0 = disabled. Shows a collapsible "Thinking…"
799
+ block in the chat. Recommended: 8000 for medium, 0 to save
800
+ tokens.
801
+ </p>
802
+ <div className="flex items-center gap-2 flex-wrap">
803
+ {[
804
+ { label: "Off", value: 0 },
805
+ { label: "Low", value: 1024 },
806
+ { label: "Medium", value: 8192 },
807
+ { label: "High", value: 24576 },
808
+ ].map((preset) => (
809
+ <button
810
+ key={preset.label}
811
+ type="button"
812
+ onClick={() => patchTop("thinkingBudget", preset.value)}
813
+ className={`px-3 py-1 text-xs rounded-md border transition-colors ${
814
+ (draft.thinkingBudget ?? 0) === preset.value
815
+ ? "bg-cyan-600/30 text-cyan-300 border-cyan-600/50"
816
+ : "text-slate-500 hover:text-slate-300 border-slate-700 hover:border-slate-500"
817
+ }`}
818
+ >
819
+ {preset.label}
820
+ {preset.value > 0 && (
821
+ <span className="ml-1 opacity-60">
822
+ ({preset.value.toLocaleString()})
823
+ </span>
824
+ )}
825
+ </button>
826
+ ))}
827
+ <div className="flex items-center gap-1.5">
828
+ <span className="text-xs text-slate-500">Custom:</span>
829
+ <input
830
+ type="number"
831
+ value={draft.thinkingBudget ?? 0}
832
+ min={0}
833
+ max={32768}
834
+ step={256}
835
+ onChange={(e) =>
836
+ patchTop("thinkingBudget", Number(e.target.value))
837
+ }
838
+ className="w-24 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs text-slate-200 focus:outline-none focus:border-cyan-500"
839
+ />
840
+ </div>
841
+ </div>
842
+ </div>
843
+ ) : (
844
+ <p className="text-xs text-slate-500">
845
+ Thinking / reasoning display is only available for Google /
846
+ Gemini models. Switch{" "}
847
+ <code className="bg-slate-800 px-1 rounded">AI_PROVIDER</code>{" "}
848
+ to <code className="bg-slate-800 px-1 rounded">google</code> in
849
+ your <code className="bg-slate-800 px-1 rounded">.env</code> to
850
+ enable it.
851
+ </p>
852
+ )}
853
+ </Section>
854
+
641
855
  {/* ── Add group form ─────────────────────────────── */}
642
856
  {showNewGroupForm ? (
643
857
  <div className="border border-cyan-600/30 rounded-lg p-4 space-y-4 bg-slate-900/60">
@@ -16,9 +16,7 @@ interface Props {
16
16
  onUpdate: (updated: Annotation) => void;
17
17
  messageContent: string;
18
18
  initialPos?: { x: number; y: number };
19
- responseLength?: string;
20
- responseStyle?: string;
21
- responseAudience?: string;
19
+ preferenceSuffix?: string;
22
20
  }
23
21
 
24
22
  export default function AnnotationDialog({
@@ -27,9 +25,7 @@ export default function AnnotationDialog({
27
25
  onUpdate,
28
26
  messageContent,
29
27
  initialPos,
30
- responseLength,
31
- responseStyle,
32
- responseAudience,
28
+ preferenceSuffix,
33
29
  }: Props) {
34
30
  const [pos, setPos] = useState(() => ({
35
31
  x: initialPos?.x ?? Math.max(0, (window.innerWidth - DEFAULT_W) / 2),
@@ -169,9 +165,7 @@ export default function AnnotationDialog({
169
165
  messageContent,
170
166
  priorResponse: ann.response,
171
167
  followUps: ann.followUps ?? [],
172
- responseLength,
173
- responseStyle,
174
- responseAudience,
168
+ preferenceSuffix,
175
169
  }),
176
170
  });
177
171
  const data = await res.json();