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.
- package/package.json +1 -1
- package/template/client/package-lock.json +19 -0
- package/template/client/package.json +3 -0
- package/template/client/src/App.tsx +17 -0
- package/template/client/src/api.ts +135 -0
- package/template/client/src/components/AiSettingsModal.tsx +218 -4
- package/template/client/src/components/AnnotationDialog.tsx +3 -9
- package/template/client/src/components/ChatMessage.tsx +110 -27
- package/template/client/src/components/ChatView.tsx +69 -4
- package/template/client/src/components/CodeContextPanel.tsx +297 -0
- package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
- package/template/client/src/components/CodeRunnerModal.tsx +1549 -0
- package/template/client/src/components/DocRefModal.tsx +502 -0
- package/template/client/src/components/FileAttachments.tsx +109 -9
- package/template/client/src/components/FilePickerModal.tsx +181 -0
- package/template/client/src/components/FileViewerModal.tsx +406 -28
- package/template/client/src/components/MarkdownRenderer.tsx +205 -2
- package/template/client/src/components/Sidebar.tsx +213 -127
- package/template/client/src/components/TextAnnotator.tsx +8 -15
- package/template/client/src/components/VizCraftEmbed.tsx +162 -19
- package/template/client/src/store.ts +201 -0
- package/template/client/src/types.ts +8 -0
- package/template/cockpit.json +1 -1
- package/template/package.json +1 -1
- package/template/server/src/google-drive.ts +109 -1
- package/template/server/src/index.ts +1107 -46
- package/template/server/src/storage.ts +263 -2
package/package.json
CHANGED
|
@@ -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-
|
|
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
|
-
<
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
responseStyle,
|
|
174
|
-
responseAudience,
|
|
168
|
+
preferenceSuffix,
|
|
175
169
|
}),
|
|
176
170
|
});
|
|
177
171
|
const data = await res.json();
|