create-interview-cockpit 0.4.0 → 0.6.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 +753 -1
- package/template/client/package.json +4 -0
- package/template/client/src/App.tsx +20 -0
- package/template/client/src/api.ts +455 -3
- package/template/client/src/components/AiSettingsModal.tsx +855 -248
- package/template/client/src/components/AnnotationDialog.tsx +3 -9
- package/template/client/src/components/ChatMessage.tsx +132 -27
- package/template/client/src/components/ChatView.tsx +365 -123
- package/template/client/src/components/CodeContextPanel.tsx +714 -0
- package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
- package/template/client/src/components/CodeRunnerModal.tsx +3030 -0
- package/template/client/src/components/DocRefModal.tsx +551 -0
- package/template/client/src/components/FileAttachments.tsx +128 -12
- package/template/client/src/components/FilePickerModal.tsx +181 -0
- package/template/client/src/components/FileViewerModal.tsx +406 -28
- package/template/client/src/components/InfraLabModal.tsx +1706 -0
- package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
- package/template/client/src/components/MarkdownRenderer.tsx +219 -2
- package/template/client/src/components/NotesModal.tsx +977 -0
- package/template/client/src/components/PlotEmbed.tsx +173 -0
- package/template/client/src/components/Sidebar.tsx +397 -127
- package/template/client/src/components/TextAnnotator.tsx +8 -15
- package/template/client/src/components/VizCraftEmbed.tsx +412 -25
- package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
- package/template/client/src/infraLab.ts +124 -0
- package/template/client/src/reactLab.ts +477 -0
- package/template/client/src/store.ts +416 -2
- package/template/client/src/types.ts +41 -1
- package/template/client/tsconfig.tsbuildinfo +1 -1
- package/template/cockpit.json +1 -1
- package/template/package.json +1 -1
- package/template/server/src/google-drive.ts +144 -2
- package/template/server/src/index.ts +1890 -188
- package/template/server/src/infra-runner.ts +1104 -0
- package/template/server/src/storage.ts +274 -3
|
@@ -16,8 +16,13 @@ import {
|
|
|
16
16
|
Loader2,
|
|
17
17
|
ArrowLeft,
|
|
18
18
|
RefreshCw,
|
|
19
|
+
Globe,
|
|
20
|
+
SlidersHorizontal,
|
|
21
|
+
ArrowRightLeft,
|
|
19
22
|
} from "lucide-react";
|
|
20
23
|
|
|
24
|
+
const ROOT_PARENT_VALUE = "__root__";
|
|
25
|
+
|
|
21
26
|
export default function Sidebar() {
|
|
22
27
|
const {
|
|
23
28
|
topics,
|
|
@@ -30,12 +35,14 @@ export default function Sidebar() {
|
|
|
30
35
|
renameTopic,
|
|
31
36
|
addQuestion,
|
|
32
37
|
addChildQuestion,
|
|
38
|
+
moveQuestion,
|
|
33
39
|
removeQuestion,
|
|
34
40
|
renameQuestion,
|
|
35
41
|
selectQuestion,
|
|
36
42
|
fetchQuestions,
|
|
37
43
|
uploadTopicFiles,
|
|
38
44
|
removeTopicFile,
|
|
45
|
+
linkFileToTopic,
|
|
39
46
|
workspaces,
|
|
40
47
|
activeWorkspaceId,
|
|
41
48
|
driveRootFolders,
|
|
@@ -44,6 +51,10 @@ export default function Sidebar() {
|
|
|
44
51
|
selectDriveSubfolder,
|
|
45
52
|
clearDriveSubfolder,
|
|
46
53
|
syncWorkspace,
|
|
54
|
+
workspaceFiles,
|
|
55
|
+
uploadWorkspaceFiles,
|
|
56
|
+
removeWorkspaceFile,
|
|
57
|
+
updateTopicSystemContext,
|
|
47
58
|
} = useStore();
|
|
48
59
|
|
|
49
60
|
const [newTopicName, setNewTopicName] = useState("");
|
|
@@ -58,6 +69,43 @@ export default function Sidebar() {
|
|
|
58
69
|
null,
|
|
59
70
|
);
|
|
60
71
|
const [editingQuestionTitle, setEditingQuestionTitle] = useState("");
|
|
72
|
+
const [movingQuestionId, setMovingQuestionId] = useState<string | null>(null);
|
|
73
|
+
const [moveTargetParentId, setMoveTargetParentId] =
|
|
74
|
+
useState(ROOT_PARENT_VALUE);
|
|
75
|
+
const [collapsedQuestions, setCollapsedQuestions] = useState<Set<string>>(
|
|
76
|
+
new Set(),
|
|
77
|
+
);
|
|
78
|
+
const [openTopicPrompts, setOpenTopicPrompts] = useState<Set<string>>(
|
|
79
|
+
new Set(),
|
|
80
|
+
);
|
|
81
|
+
const topicPromptTimers = useRef<
|
|
82
|
+
Record<string, ReturnType<typeof setTimeout>>
|
|
83
|
+
>({});
|
|
84
|
+
|
|
85
|
+
const toggleTopicPrompt = (topicId: string) => {
|
|
86
|
+
setOpenTopicPrompts((prev) => {
|
|
87
|
+
const next = new Set(prev);
|
|
88
|
+
if (next.has(topicId)) next.delete(topicId);
|
|
89
|
+
else next.add(topicId);
|
|
90
|
+
return next;
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleTopicPromptChange = (topicId: string, value: string) => {
|
|
95
|
+
clearTimeout(topicPromptTimers.current[topicId]);
|
|
96
|
+
topicPromptTimers.current[topicId] = setTimeout(() => {
|
|
97
|
+
updateTopicSystemContext(topicId, value);
|
|
98
|
+
}, 600);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const toggleQuestionCollapse = (questionId: string) => {
|
|
102
|
+
setCollapsedQuestions((prev) => {
|
|
103
|
+
const next = new Set(prev);
|
|
104
|
+
if (next.has(questionId)) next.delete(questionId);
|
|
105
|
+
else next.add(questionId);
|
|
106
|
+
return next;
|
|
107
|
+
});
|
|
108
|
+
};
|
|
61
109
|
const editInputRef = useRef<HTMLInputElement>(null);
|
|
62
110
|
|
|
63
111
|
// Drive subfolder navigator
|
|
@@ -72,6 +120,7 @@ export default function Sidebar() {
|
|
|
72
120
|
: null;
|
|
73
121
|
const [navigating, setNavigating] = useState(false);
|
|
74
122
|
const [syncing, setSyncing] = useState(false);
|
|
123
|
+
const [wsFilesExpanded, setWsFilesExpanded] = useState(true);
|
|
75
124
|
|
|
76
125
|
// Load root folders whenever a Drive workspace becomes active with no subfolder selected
|
|
77
126
|
useEffect(() => {
|
|
@@ -156,103 +205,334 @@ export default function Sidebar() {
|
|
|
156
205
|
setAddingChildTo(null);
|
|
157
206
|
};
|
|
158
207
|
|
|
208
|
+
const getDescendantIds = (questions: Question[], questionId: string) => {
|
|
209
|
+
const descendants = new Set<string>();
|
|
210
|
+
const visit = (parentId: string) => {
|
|
211
|
+
for (const candidate of questions) {
|
|
212
|
+
if (candidate.parentQuestionId !== parentId) continue;
|
|
213
|
+
if (descendants.has(candidate.id)) continue;
|
|
214
|
+
descendants.add(candidate.id);
|
|
215
|
+
visit(candidate.id);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
visit(questionId);
|
|
219
|
+
return descendants;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const buildMoveParentOptions = (
|
|
223
|
+
questions: Question[],
|
|
224
|
+
excludedIds: Set<string>,
|
|
225
|
+
parentId: string | null,
|
|
226
|
+
depth: number,
|
|
227
|
+
): Array<{ id: string; title: string }> => {
|
|
228
|
+
const siblings = questions.filter(
|
|
229
|
+
(q) => (q.parentQuestionId ?? null) === parentId,
|
|
230
|
+
);
|
|
231
|
+
return siblings.flatMap((q) => {
|
|
232
|
+
if (excludedIds.has(q.id)) return [];
|
|
233
|
+
return [
|
|
234
|
+
{
|
|
235
|
+
id: q.id,
|
|
236
|
+
title: `${"— ".repeat(depth)}${q.title}`,
|
|
237
|
+
},
|
|
238
|
+
...buildMoveParentOptions(questions, excludedIds, q.id, depth + 1),
|
|
239
|
+
];
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const handleMoveQuestion = async (
|
|
244
|
+
topicId: string,
|
|
245
|
+
questionId: string,
|
|
246
|
+
targetParentId: string | null,
|
|
247
|
+
) => {
|
|
248
|
+
await moveQuestion(questionId, topicId, targetParentId);
|
|
249
|
+
setMovingQuestionId(null);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const renderMoveQuestionPicker = (
|
|
253
|
+
questions: Question[],
|
|
254
|
+
q: Question,
|
|
255
|
+
topicId: string,
|
|
256
|
+
depth: number,
|
|
257
|
+
) => {
|
|
258
|
+
if (movingQuestionId !== q.id) return null;
|
|
259
|
+
const excludedIds = getDescendantIds(questions, q.id);
|
|
260
|
+
excludedIds.add(q.id);
|
|
261
|
+
const parentOptions = buildMoveParentOptions(
|
|
262
|
+
questions,
|
|
263
|
+
excludedIds,
|
|
264
|
+
null,
|
|
265
|
+
0,
|
|
266
|
+
);
|
|
267
|
+
const targetParentId =
|
|
268
|
+
moveTargetParentId === ROOT_PARENT_VALUE ? null : moveTargetParentId;
|
|
269
|
+
const unchanged = (q.parentQuestionId ?? null) === targetParentId;
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div
|
|
273
|
+
className="pr-2 py-1.5 animate-fadeIn"
|
|
274
|
+
style={{ paddingLeft: 12 + (depth + 1) * 16 }}
|
|
275
|
+
>
|
|
276
|
+
<div className="rounded border border-slate-700 bg-slate-800/70 p-2 space-y-2">
|
|
277
|
+
<div className="text-[11px] text-slate-500">Move under</div>
|
|
278
|
+
<select
|
|
279
|
+
autoFocus
|
|
280
|
+
value={moveTargetParentId}
|
|
281
|
+
onChange={(e) => setMoveTargetParentId(e.target.value)}
|
|
282
|
+
className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300 focus:outline-none focus:border-cyan-500"
|
|
283
|
+
>
|
|
284
|
+
<option value={ROOT_PARENT_VALUE}>No parent (top level)</option>
|
|
285
|
+
{parentOptions.map((option) => (
|
|
286
|
+
<option key={option.id} value={option.id}>
|
|
287
|
+
{option.title}
|
|
288
|
+
</option>
|
|
289
|
+
))}
|
|
290
|
+
</select>
|
|
291
|
+
<div className="flex items-center justify-end gap-2">
|
|
292
|
+
<button
|
|
293
|
+
type="button"
|
|
294
|
+
onClick={() => setMovingQuestionId(null)}
|
|
295
|
+
className="px-2 py-1 rounded text-[11px] text-slate-400 hover:text-slate-200 hover:bg-slate-700 transition-colors"
|
|
296
|
+
>
|
|
297
|
+
Cancel
|
|
298
|
+
</button>
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
onClick={() => handleMoveQuestion(topicId, q.id, targetParentId)}
|
|
302
|
+
disabled={unchanged}
|
|
303
|
+
className="px-2 py-1 rounded text-[11px] bg-cyan-600 hover:bg-cyan-500 disabled:opacity-40 text-white transition-colors"
|
|
304
|
+
>
|
|
305
|
+
Move
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
};
|
|
312
|
+
|
|
159
313
|
const renderQuestionRow = (
|
|
160
314
|
q: Question,
|
|
161
315
|
topicId: string,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
316
|
+
depth: number,
|
|
317
|
+
hasChildren: boolean,
|
|
318
|
+
isCollapsed: boolean,
|
|
319
|
+
onToggleCollapse: () => void,
|
|
320
|
+
) => {
|
|
321
|
+
// 12px base left padding + 16px per depth level
|
|
322
|
+
const paddingLeft = 12 + depth * 16;
|
|
323
|
+
return (
|
|
324
|
+
<div
|
|
325
|
+
key={q.id}
|
|
326
|
+
className={`group flex items-center gap-1.5 pr-2 py-1 cursor-pointer transition-colors ${
|
|
327
|
+
selectedQuestionId === q.id
|
|
328
|
+
? "bg-cyan-500/10 border-l-2 border-l-cyan-500 -ml-px"
|
|
329
|
+
: "hover:bg-slate-800/30"
|
|
330
|
+
}`}
|
|
331
|
+
style={{ paddingLeft }}
|
|
332
|
+
onClick={() =>
|
|
333
|
+
editingQuestionId !== q.id && selectQuestion(topicId, q.id)
|
|
334
|
+
}
|
|
335
|
+
>
|
|
336
|
+
{depth > 0 && (
|
|
337
|
+
<span className="text-slate-600 text-[11px] shrink-0 leading-none select-none mr-0.5">
|
|
338
|
+
└
|
|
339
|
+
</span>
|
|
340
|
+
)}
|
|
341
|
+
{hasChildren ? (
|
|
342
|
+
<button
|
|
343
|
+
onClick={(e) => {
|
|
344
|
+
e.stopPropagation();
|
|
345
|
+
onToggleCollapse();
|
|
346
|
+
}}
|
|
347
|
+
className="shrink-0 text-slate-500 hover:text-slate-300 p-0.5 -ml-0.5 rounded transition-colors"
|
|
348
|
+
title={isCollapsed ? "Expand" : "Collapse"}
|
|
349
|
+
>
|
|
350
|
+
{isCollapsed ? (
|
|
351
|
+
<ChevronRight className="w-3 h-3" />
|
|
352
|
+
) : (
|
|
353
|
+
<ChevronDown className="w-3 h-3" />
|
|
354
|
+
)}
|
|
355
|
+
</button>
|
|
356
|
+
) : (
|
|
357
|
+
<MessageSquare className="w-3 h-3 text-slate-600 shrink-0" />
|
|
358
|
+
)}
|
|
359
|
+
{editingQuestionId === q.id ? (
|
|
360
|
+
<input
|
|
361
|
+
ref={editInputRef}
|
|
362
|
+
value={editingQuestionTitle}
|
|
363
|
+
onChange={(e) => setEditingQuestionTitle(e.target.value)}
|
|
364
|
+
onKeyDown={(e) => {
|
|
365
|
+
if (e.key === "Enter") commitQuestionRename(q.id, topicId);
|
|
366
|
+
if (e.key === "Escape") setEditingQuestionId(null);
|
|
367
|
+
}}
|
|
368
|
+
onBlur={() => commitQuestionRename(q.id, topicId)}
|
|
369
|
+
onClick={(e) => e.stopPropagation()}
|
|
370
|
+
className="flex-1 min-w-0 bg-slate-700 border border-cyan-500 rounded px-1 py-0 text-xs text-slate-200 focus:outline-none"
|
|
371
|
+
/>
|
|
372
|
+
) : (
|
|
373
|
+
<span
|
|
374
|
+
className="text-xs text-slate-400 truncate flex-1"
|
|
375
|
+
onDoubleClick={(e) => {
|
|
376
|
+
e.stopPropagation();
|
|
377
|
+
setEditingQuestionId(q.id);
|
|
378
|
+
setEditingQuestionTitle(q.title);
|
|
379
|
+
}}
|
|
380
|
+
>
|
|
381
|
+
{q.title}
|
|
382
|
+
</span>
|
|
383
|
+
)}
|
|
384
|
+
<span className="text-[10px] text-slate-700 shrink-0">
|
|
385
|
+
{q.messages.length > 0 ? `${q.messages.length}` : ""}
|
|
206
386
|
</span>
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
387
|
+
{editingQuestionId !== q.id && (
|
|
388
|
+
<button
|
|
389
|
+
onClick={(e) => {
|
|
390
|
+
e.stopPropagation();
|
|
391
|
+
setAddingChildTo(q.id);
|
|
392
|
+
setNewChildTitle("");
|
|
393
|
+
}}
|
|
394
|
+
className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-cyan-400 transition-all"
|
|
395
|
+
title="Add child question"
|
|
396
|
+
>
|
|
397
|
+
<CornerDownRight className="w-2.5 h-2.5" />
|
|
398
|
+
</button>
|
|
399
|
+
)}
|
|
400
|
+
{editingQuestionId !== q.id && (
|
|
401
|
+
<button
|
|
402
|
+
onClick={(e) => {
|
|
403
|
+
e.stopPropagation();
|
|
404
|
+
setEditingQuestionId(q.id);
|
|
405
|
+
setEditingQuestionTitle(q.title);
|
|
406
|
+
}}
|
|
407
|
+
className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-cyan-400 transition-all"
|
|
408
|
+
title="Rename"
|
|
409
|
+
>
|
|
410
|
+
<Pencil className="w-2.5 h-2.5" />
|
|
411
|
+
</button>
|
|
412
|
+
)}
|
|
413
|
+
{editingQuestionId !== q.id && (
|
|
414
|
+
<button
|
|
415
|
+
onClick={(e) => {
|
|
416
|
+
e.stopPropagation();
|
|
417
|
+
setMovingQuestionId((prev) => (prev === q.id ? null : q.id));
|
|
418
|
+
setMoveTargetParentId(q.parentQuestionId ?? ROOT_PARENT_VALUE);
|
|
419
|
+
}}
|
|
420
|
+
className={`p-0.5 rounded opacity-0 group-hover:opacity-100 transition-all ${
|
|
421
|
+
movingQuestionId === q.id
|
|
422
|
+
? "opacity-100 text-cyan-400"
|
|
423
|
+
: "text-slate-600 hover:text-cyan-400"
|
|
424
|
+
}`}
|
|
425
|
+
title="Move to a different parent"
|
|
426
|
+
>
|
|
427
|
+
<ArrowRightLeft className="w-2.5 h-2.5" />
|
|
428
|
+
</button>
|
|
429
|
+
)}
|
|
225
430
|
<button
|
|
226
431
|
onClick={(e) => {
|
|
227
432
|
e.stopPropagation();
|
|
228
|
-
|
|
229
|
-
|
|
433
|
+
if (window.confirm(`Delete "${q.title}"? This cannot be undone.`)) {
|
|
434
|
+
removeQuestion(q.id, topicId);
|
|
435
|
+
}
|
|
230
436
|
}}
|
|
231
|
-
className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-
|
|
232
|
-
title="Rename"
|
|
437
|
+
className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 transition-all"
|
|
233
438
|
>
|
|
234
|
-
<
|
|
439
|
+
<Trash2 className="w-2.5 h-2.5" />
|
|
235
440
|
</button>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
441
|
+
</div>
|
|
442
|
+
);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// Recursively renders a question and all its descendants.
|
|
446
|
+
const renderQuestionSubtree = (
|
|
447
|
+
questions: Question[],
|
|
448
|
+
topicId: string,
|
|
449
|
+
parentId: string | null,
|
|
450
|
+
depth: number,
|
|
451
|
+
): React.ReactNode => {
|
|
452
|
+
const qs = questions.filter(
|
|
453
|
+
(q) => (q.parentQuestionId ?? null) === parentId,
|
|
454
|
+
);
|
|
455
|
+
return qs.map((q) => {
|
|
456
|
+
const hasChildren = questions.some((c) => c.parentQuestionId === q.id);
|
|
457
|
+
const isCollapsed = collapsedQuestions.has(q.id);
|
|
458
|
+
return (
|
|
459
|
+
<Fragment key={q.id}>
|
|
460
|
+
{renderQuestionRow(q, topicId, depth, hasChildren, isCollapsed, () =>
|
|
461
|
+
toggleQuestionCollapse(q.id),
|
|
462
|
+
)}
|
|
463
|
+
{/* Add-child input — indented one level deeper than this question */}
|
|
464
|
+
{addingChildTo === q.id && (
|
|
465
|
+
<div
|
|
466
|
+
className="pr-2 py-1 animate-fadeIn"
|
|
467
|
+
style={{ paddingLeft: 12 + (depth + 1) * 16 }}
|
|
468
|
+
>
|
|
469
|
+
<input
|
|
470
|
+
autoFocus
|
|
471
|
+
value={newChildTitle}
|
|
472
|
+
onChange={(e) => setNewChildTitle(e.target.value)}
|
|
473
|
+
onKeyDown={(e) => {
|
|
474
|
+
if (e.key === "Enter") handleAddChildQuestion(topicId, q.id);
|
|
475
|
+
if (e.key === "Escape") {
|
|
476
|
+
setAddingChildTo(null);
|
|
477
|
+
setNewChildTitle("");
|
|
478
|
+
}
|
|
479
|
+
}}
|
|
480
|
+
onBlur={() => {
|
|
481
|
+
if (!newChildTitle.trim()) setAddingChildTo(null);
|
|
482
|
+
}}
|
|
483
|
+
placeholder="Child question title..."
|
|
484
|
+
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-0.5 text-xs text-slate-300 placeholder-slate-600 focus:outline-none focus:border-cyan-500"
|
|
485
|
+
/>
|
|
486
|
+
</div>
|
|
487
|
+
)}
|
|
488
|
+
{renderMoveQuestionPicker(questions, q, topicId, depth)}
|
|
489
|
+
{/* Recurse into children — hidden when collapsed */}
|
|
490
|
+
{!isCollapsed &&
|
|
491
|
+
renderQuestionSubtree(questions, topicId, q.id, depth + 1)}
|
|
492
|
+
</Fragment>
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
};
|
|
250
496
|
|
|
251
497
|
return (
|
|
252
498
|
<aside className="w-72 border-r border-slate-800 flex flex-col bg-slate-900/50 shrink-0">
|
|
253
499
|
{/* Workspace switcher */}
|
|
254
500
|
<WorkspaceSwitcher />
|
|
255
501
|
|
|
502
|
+
{/* Workspace-level files — apply to all topics */}
|
|
503
|
+
<div className="border-b border-slate-800 shrink-0">
|
|
504
|
+
<button
|
|
505
|
+
onClick={() => setWsFilesExpanded((v) => !v)}
|
|
506
|
+
className="w-full flex items-center gap-1.5 px-3 py-1.5 hover:bg-slate-800/40 transition-colors"
|
|
507
|
+
>
|
|
508
|
+
{wsFilesExpanded ? (
|
|
509
|
+
<ChevronDown className="w-3 h-3 text-slate-500 shrink-0" />
|
|
510
|
+
) : (
|
|
511
|
+
<ChevronRight className="w-3 h-3 text-slate-500 shrink-0" />
|
|
512
|
+
)}
|
|
513
|
+
<Globe className="w-3 h-3 text-violet-400 shrink-0" />
|
|
514
|
+
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-500 flex-1 text-left">
|
|
515
|
+
Workspace Files
|
|
516
|
+
</span>
|
|
517
|
+
{workspaceFiles.length > 0 && (
|
|
518
|
+
<span className="text-[10px] text-slate-600">
|
|
519
|
+
{workspaceFiles.length}
|
|
520
|
+
</span>
|
|
521
|
+
)}
|
|
522
|
+
</button>
|
|
523
|
+
{wsFilesExpanded && (
|
|
524
|
+
<div className="px-3 pb-2">
|
|
525
|
+
<FileAttachments
|
|
526
|
+
files={workspaceFiles}
|
|
527
|
+
onUpload={(files) => uploadWorkspaceFiles(files)}
|
|
528
|
+
onRemove={(fileId) => removeWorkspaceFile(fileId)}
|
|
529
|
+
downloadBase="/api/workspace/context-files"
|
|
530
|
+
label="workspace"
|
|
531
|
+
/>
|
|
532
|
+
</div>
|
|
533
|
+
)}
|
|
534
|
+
</div>
|
|
535
|
+
|
|
256
536
|
{/* Header — Drive breadcrumb when inside a subfolder, else normal Topics header */}
|
|
257
537
|
<div className="h-12 border-b border-slate-800 flex items-center justify-between px-3 shrink-0">
|
|
258
538
|
{isDriveWs && currentSubFolder ? (
|
|
@@ -482,57 +762,47 @@ export default function Sidebar() {
|
|
|
482
762
|
onRemove={(fileId) =>
|
|
483
763
|
removeTopicFile(topic.id, fileId)
|
|
484
764
|
}
|
|
765
|
+
onLink={(fileId, originalName) =>
|
|
766
|
+
linkFileToTopic(topic.id, fileId, originalName)
|
|
767
|
+
}
|
|
768
|
+
downloadBase={`/api/topics/${topic.id}/context-files`}
|
|
485
769
|
label="topic"
|
|
486
770
|
compact
|
|
487
771
|
/>
|
|
488
772
|
</div>
|
|
489
773
|
|
|
490
|
-
{/*
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
{
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}}
|
|
523
|
-
placeholder="Child question title..."
|
|
524
|
-
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-0.5 text-xs text-slate-300 placeholder-slate-600 focus:outline-none focus:border-cyan-500"
|
|
525
|
-
/>
|
|
526
|
-
</div>
|
|
527
|
-
)}
|
|
528
|
-
{/* Children */}
|
|
529
|
-
{children.map((c) =>
|
|
530
|
-
renderQuestionRow(c, topic.id, true),
|
|
531
|
-
)}
|
|
532
|
-
</Fragment>
|
|
533
|
-
);
|
|
534
|
-
});
|
|
535
|
-
})()}
|
|
774
|
+
{/* Topic-wide system prompt */}
|
|
775
|
+
<div className="pl-3 pr-2 py-1 border-b border-slate-800/50">
|
|
776
|
+
<button
|
|
777
|
+
onClick={() => toggleTopicPrompt(topic.id)}
|
|
778
|
+
className="flex items-center gap-1.5 text-xs text-slate-500 hover:text-cyan-400 transition-colors w-full"
|
|
779
|
+
>
|
|
780
|
+
<SlidersHorizontal className="w-3 h-3" />
|
|
781
|
+
<span>Topic prompt</span>
|
|
782
|
+
<span className="ml-auto flex items-center gap-1">
|
|
783
|
+
{topic.systemContext && (
|
|
784
|
+
<span className="w-1.5 h-1.5 rounded-full bg-cyan-500" />
|
|
785
|
+
)}
|
|
786
|
+
<ChevronDown
|
|
787
|
+
className={`w-3 h-3 transition-transform ${openTopicPrompts.has(topic.id) ? "" : "-rotate-90"}`}
|
|
788
|
+
/>
|
|
789
|
+
</span>
|
|
790
|
+
</button>
|
|
791
|
+
{openTopicPrompts.has(topic.id) && (
|
|
792
|
+
<textarea
|
|
793
|
+
defaultValue={topic.systemContext ?? ""}
|
|
794
|
+
onChange={(e) =>
|
|
795
|
+
handleTopicPromptChange(topic.id, e.target.value)
|
|
796
|
+
}
|
|
797
|
+
placeholder="System context added to every question in this topic…"
|
|
798
|
+
rows={4}
|
|
799
|
+
className="mt-1.5 w-full bg-slate-800/60 border border-slate-700 rounded px-2 py-1.5 text-xs text-slate-300 placeholder-slate-600 focus:outline-none focus:border-cyan-500 resize-y leading-relaxed"
|
|
800
|
+
/>
|
|
801
|
+
)}
|
|
802
|
+
</div>
|
|
803
|
+
|
|
804
|
+
{/* Questions — recursive tree (unlimited depth) */}
|
|
805
|
+
{renderQuestionSubtree(questions, topic.id, null, 0)}
|
|
536
806
|
|
|
537
807
|
{/* Add question input */}
|
|
538
808
|
{addingQuestionTo === topic.id ? (
|
|
@@ -12,9 +12,8 @@ interface Props {
|
|
|
12
12
|
onAnnotationUpdate: (updated: Annotation) => void;
|
|
13
13
|
bookmarkedBlockIndex?: number;
|
|
14
14
|
onBookmarkBlock?: (blockIndex: number) => void;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
responseAudience?: string;
|
|
15
|
+
preferenceSuffix?: string;
|
|
16
|
+
onSpecRefined?: (originalSpec: string, newSpec: string) => void;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
type Phase = "idle" | "button" | "input" | "loading";
|
|
@@ -119,9 +118,8 @@ export default function TextAnnotator({
|
|
|
119
118
|
onAnnotationUpdate,
|
|
120
119
|
bookmarkedBlockIndex,
|
|
121
120
|
onBookmarkBlock,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
responseAudience,
|
|
121
|
+
preferenceSuffix,
|
|
122
|
+
onSpecRefined,
|
|
125
123
|
}: Props) {
|
|
126
124
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
127
125
|
const annotationsRef = useRef(annotations);
|
|
@@ -207,9 +205,7 @@ export default function TextAnnotator({
|
|
|
207
205
|
selectedText,
|
|
208
206
|
prompt: inputValue.trim(),
|
|
209
207
|
messageContent: content,
|
|
210
|
-
|
|
211
|
-
responseStyle,
|
|
212
|
-
responseAudience,
|
|
208
|
+
preferenceSuffix,
|
|
213
209
|
}),
|
|
214
210
|
});
|
|
215
211
|
const data = await res.json();
|
|
@@ -248,9 +244,7 @@ export default function TextAnnotator({
|
|
|
248
244
|
messageContent: content,
|
|
249
245
|
priorResponse: annotation.response,
|
|
250
246
|
followUps: annotation.followUps ?? [],
|
|
251
|
-
|
|
252
|
-
responseStyle,
|
|
253
|
-
responseAudience,
|
|
247
|
+
preferenceSuffix,
|
|
254
248
|
}),
|
|
255
249
|
});
|
|
256
250
|
const data = await res.json();
|
|
@@ -288,6 +282,7 @@ export default function TextAnnotator({
|
|
|
288
282
|
onAnnotationClick={handleAnnotationClick}
|
|
289
283
|
bookmarkedBlockIndex={bookmarkedBlockIndex}
|
|
290
284
|
onBookmarkBlock={onBookmarkBlock}
|
|
285
|
+
onSpecRefined={onSpecRefined}
|
|
291
286
|
/>
|
|
292
287
|
|
|
293
288
|
{/* Annotation dialog — opened by clicking an underlined annotation link */}
|
|
@@ -298,9 +293,7 @@ export default function TextAnnotator({
|
|
|
298
293
|
onUpdate={onAnnotationUpdate}
|
|
299
294
|
messageContent={content}
|
|
300
295
|
initialPos={dialogPos}
|
|
301
|
-
|
|
302
|
-
responseStyle={responseStyle}
|
|
303
|
-
responseAudience={responseAudience}
|
|
296
|
+
preferenceSuffix={preferenceSuffix}
|
|
304
297
|
/>
|
|
305
298
|
)}
|
|
306
299
|
|