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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/template/client/package-lock.json +753 -1
  3. package/template/client/package.json +4 -0
  4. package/template/client/src/App.tsx +20 -0
  5. package/template/client/src/api.ts +455 -3
  6. package/template/client/src/components/AiSettingsModal.tsx +855 -248
  7. package/template/client/src/components/AnnotationDialog.tsx +3 -9
  8. package/template/client/src/components/ChatMessage.tsx +132 -27
  9. package/template/client/src/components/ChatView.tsx +365 -123
  10. package/template/client/src/components/CodeContextPanel.tsx +714 -0
  11. package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
  12. package/template/client/src/components/CodeRunnerModal.tsx +3030 -0
  13. package/template/client/src/components/DocRefModal.tsx +551 -0
  14. package/template/client/src/components/FileAttachments.tsx +128 -12
  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/InfraLabModal.tsx +1706 -0
  18. package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
  19. package/template/client/src/components/MarkdownRenderer.tsx +219 -2
  20. package/template/client/src/components/NotesModal.tsx +977 -0
  21. package/template/client/src/components/PlotEmbed.tsx +173 -0
  22. package/template/client/src/components/Sidebar.tsx +397 -127
  23. package/template/client/src/components/TextAnnotator.tsx +8 -15
  24. package/template/client/src/components/VizCraftEmbed.tsx +412 -25
  25. package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
  26. package/template/client/src/infraLab.ts +124 -0
  27. package/template/client/src/reactLab.ts +477 -0
  28. package/template/client/src/store.ts +416 -2
  29. package/template/client/src/types.ts +41 -1
  30. package/template/client/tsconfig.tsbuildinfo +1 -1
  31. package/template/cockpit.json +1 -1
  32. package/template/package.json +1 -1
  33. package/template/server/src/google-drive.ts +144 -2
  34. package/template/server/src/index.ts +1890 -188
  35. package/template/server/src/infra-runner.ts +1104 -0
  36. package/template/server/src/storage.ts +274 -3
@@ -9,14 +9,18 @@
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",
23
+ "vega-embed": "^7.1.0",
20
24
  "vizcraft": "^1.17.0",
21
25
  "yaml": "^2.8.3",
22
26
  "zustand": "^5.0.0"
@@ -4,7 +4,10 @@ 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";
10
+ import InfraLabModal from "./components/InfraLabModal";
8
11
  import { Code, Plane, PanelLeftClose, PanelLeft, Settings } from "lucide-react";
9
12
 
10
13
  export default function App() {
@@ -12,6 +15,7 @@ export default function App() {
12
15
  fetchTopics,
13
16
  fetchWorkspaces,
14
17
  fetchAiSettings,
18
+ fetchWorkspaceFiles,
15
19
  fetchQuestions,
16
20
  selectQuestion,
17
21
  currentQuestion,
@@ -21,9 +25,14 @@ export default function App() {
21
25
  toggleSidebar,
22
26
  viewingFile,
23
27
  closeFileViewer,
28
+ viewingDoc,
29
+ closeDocViewer,
24
30
  showSettings,
25
31
  openSettings,
26
32
  closeSettings,
33
+ showCodeRunner,
34
+ showInfraLab,
35
+ closeCodeRunner,
27
36
  } = useStore();
28
37
 
29
38
  useEffect(() => {
@@ -31,6 +40,7 @@ export default function App() {
31
40
  await fetchWorkspaces();
32
41
  await fetchTopics();
33
42
  fetchAiSettings();
43
+ fetchWorkspaceFiles();
34
44
  // Restore last-viewed question after page refresh
35
45
  const topicId = sessionStorage.getItem("lastTopicId");
36
46
  const questionId = sessionStorage.getItem("lastQuestionId");
@@ -139,7 +149,17 @@ export default function App() {
139
149
  {viewingFile && (
140
150
  <FileViewerModal filePath={viewingFile} onClose={closeFileViewer} />
141
151
  )}
152
+ {viewingDoc && (
153
+ <DocRefModal
154
+ fileId={viewingDoc.fileId}
155
+ quote={viewingDoc.quote}
156
+ fileName={viewingDoc.fileName}
157
+ onClose={closeDocViewer}
158
+ />
159
+ )}
142
160
  {showSettings && <AiSettingsModal />}
161
+ {showCodeRunner && <CodeRunnerModal />}
162
+ {showInfraLab && <InfraLabModal />}
143
163
  </div>
144
164
  );
145
165
  }
@@ -1,4 +1,10 @@
1
- import type { Topic, Question, ContextFile, WorkspacesRegistry } from "./types";
1
+ import type {
2
+ Topic,
3
+ Question,
4
+ ContextFile,
5
+ WorkspacesRegistry,
6
+ InfraLabWorkspace,
7
+ } from "./types";
2
8
 
3
9
  const BASE = "/api";
4
10
 
@@ -20,10 +26,78 @@ export interface AiSettings {
20
26
  { maxOutputTokens: number; maxSteps: number }
21
27
  >;
22
28
  vizGuide: string;
29
+ plotGuide: string;
23
30
  /** All user-selectable prompt groups. Add new entries here to extend the UI. */
24
31
  promptGroups: Record<string, PromptGroup>;
32
+ /** When true, preference prompt texts are appended to every message (not just on change). */
33
+ alwaysSendPrefsDefault?: boolean;
34
+ /** Gemini thinking budget in tokens. 0 = disabled. Only applies when provider is google/gemini. */
35
+ thinkingBudget?: number;
36
+ /** Read-only: current AI provider from .env (openai | google | anthropic). */
37
+ provider?: string;
38
+ /** Read-only: current model name from .env. */
39
+ model?: string;
25
40
  }
26
41
 
42
+ export type InfraRunAction = "validate" | "plan" | "command";
43
+ export type InfraRunStatus = "completed" | "failed";
44
+
45
+ export interface InfraRunArtifact {
46
+ key: string;
47
+ label: string;
48
+ kind: "text" | "json";
49
+ fileName: string;
50
+ }
51
+
52
+ export interface InfraDiagnostic {
53
+ severity: string;
54
+ summary: string;
55
+ detail?: string;
56
+ address?: string;
57
+ filename?: string;
58
+ line?: number;
59
+ }
60
+
61
+ export interface InfraPlanSummary {
62
+ add: number;
63
+ change: number;
64
+ destroy: number;
65
+ replace: number;
66
+ resourceCount: number;
67
+ outputCount: number;
68
+ }
69
+
70
+ export interface InfraRunListItem {
71
+ id: string;
72
+ fileId?: string;
73
+ questionId?: string;
74
+ label: string;
75
+ action: InfraRunAction;
76
+ command?: string;
77
+ status: InfraRunStatus;
78
+ startedAt: string;
79
+ completedAt: string;
80
+ durationMs: number;
81
+ provider: "aws";
82
+ executionMode: "plan-only" | "localstack";
83
+ diagnostics: InfraDiagnostic[];
84
+ planSummary?: InfraPlanSummary;
85
+ error?: string;
86
+ artifacts: InfraRunArtifact[];
87
+ }
88
+
89
+ export interface InfraRunDetails extends InfraRunListItem {
90
+ logs: string;
91
+ validationJson?: unknown;
92
+ planJson?: unknown;
93
+ workspaceSnapshot?: InfraLabWorkspace;
94
+ }
95
+
96
+ export type InfraCommandStreamMessage =
97
+ | { type: "output"; kind: "stdout" | "stderr" | "info"; text: string }
98
+ | { type: "complete"; run: InfraRunDetails }
99
+ | { type: "error"; error: string };
100
+
27
101
  export async function fetchAiSettings(): Promise<AiSettings> {
28
102
  const res = await fetch(`${BASE}/settings`);
29
103
  return res.json();
@@ -60,7 +134,7 @@ export async function deleteTopic(id: string): Promise<void> {
60
134
 
61
135
  export async function updateTopic(
62
136
  id: string,
63
- data: { name?: string },
137
+ data: { name?: string; systemContext?: string },
64
138
  ): Promise<Topic> {
65
139
  const res = await fetch(`${BASE}/topics/${id}`, {
66
140
  method: "PATCH",
@@ -82,6 +156,10 @@ export async function uploadTopicFiles(
82
156
  method: "POST",
83
157
  body: form,
84
158
  });
159
+ if (!res.ok) {
160
+ const err = await res.json().catch(() => ({ error: "Upload failed" }));
161
+ throw new Error(err.error ?? "Upload failed");
162
+ }
85
163
  return res.json();
86
164
  }
87
165
 
@@ -94,6 +172,35 @@ export async function deleteTopicFile(
94
172
  });
95
173
  }
96
174
 
175
+ // --- Workspace Context Files ---
176
+
177
+ export async function fetchWorkspaceFiles(): Promise<ContextFile[]> {
178
+ const res = await fetch(`${BASE}/workspace/context-files`);
179
+ return res.json();
180
+ }
181
+
182
+ export async function uploadWorkspaceFiles(
183
+ files: FileList | File[],
184
+ ): Promise<ContextFile[]> {
185
+ const form = new FormData();
186
+ for (const file of files) form.append("files", file);
187
+ const res = await fetch(`${BASE}/workspace/context-files`, {
188
+ method: "POST",
189
+ body: form,
190
+ });
191
+ if (!res.ok) {
192
+ const err = await res.json().catch(() => ({ error: "Upload failed" }));
193
+ throw new Error(err.error ?? "Upload failed");
194
+ }
195
+ return res.json();
196
+ }
197
+
198
+ export async function deleteWorkspaceFile(fileId: string): Promise<void> {
199
+ await fetch(`${BASE}/workspace/context-files/${fileId}`, {
200
+ method: "DELETE",
201
+ });
202
+ }
203
+
97
204
  // --- Questions ---
98
205
 
99
206
  export async function fetchQuestions(topicId: string): Promise<Question[]> {
@@ -151,6 +258,10 @@ export async function uploadQuestionFiles(
151
258
  method: "POST",
152
259
  body: form,
153
260
  });
261
+ if (!res.ok) {
262
+ const err = await res.json().catch(() => ({ error: "Upload failed" }));
263
+ throw new Error(err.error ?? "Upload failed");
264
+ }
154
265
  return res.json();
155
266
  }
156
267
 
@@ -163,6 +274,307 @@ export async function deleteQuestionFile(
163
274
  });
164
275
  }
165
276
 
277
+ export interface PickableFile {
278
+ fileId: string;
279
+ originalName: string;
280
+ source: "workspace" | "topic" | "question";
281
+ sourceName: string;
282
+ }
283
+
284
+ export async function fetchAllContextFiles(): Promise<PickableFile[]> {
285
+ const res = await fetch(`${BASE}/context-files/all`);
286
+ return res.json();
287
+ }
288
+
289
+ export async function linkFileToTopic(
290
+ topicId: string,
291
+ fileId: string,
292
+ originalName: string,
293
+ ): Promise<ContextFile> {
294
+ const res = await fetch(`${BASE}/topics/${topicId}/context-files/link`, {
295
+ method: "POST",
296
+ headers: { "Content-Type": "application/json" },
297
+ body: JSON.stringify({ fileId, originalName }),
298
+ });
299
+ return res.json();
300
+ }
301
+
302
+ export async function linkFileToQuestion(
303
+ questionId: string,
304
+ fileId: string,
305
+ originalName: string,
306
+ ): Promise<ContextFile> {
307
+ const res = await fetch(
308
+ `${BASE}/questions/${questionId}/context-files/link`,
309
+ {
310
+ method: "POST",
311
+ headers: { "Content-Type": "application/json" },
312
+ body: JSON.stringify({ fileId, originalName }),
313
+ },
314
+ );
315
+ return res.json();
316
+ }
317
+
318
+ export async function saveCodeSnippet(
319
+ questionId: string,
320
+ code: string,
321
+ language: string,
322
+ label: string,
323
+ origin: "user" | "ai" | "sandbox" | "infra" | "react" | "nextjs",
324
+ ): Promise<ContextFile> {
325
+ const res = await fetch(`${BASE}/questions/${questionId}/save-code-snippet`, {
326
+ method: "POST",
327
+ headers: { "Content-Type": "application/json" },
328
+ body: JSON.stringify({ code, language, label, origin }),
329
+ });
330
+ if (!res.ok) throw new Error(await res.text());
331
+ return res.json();
332
+ }
333
+
334
+ export async function overwriteContextFileContent(
335
+ questionId: string,
336
+ fileId: string,
337
+ content: string,
338
+ ): Promise<void> {
339
+ const res = await fetch(
340
+ `${BASE}/questions/${questionId}/context-files/${fileId}/content`,
341
+ {
342
+ method: "PUT",
343
+ headers: { "Content-Type": "application/json" },
344
+ body: JSON.stringify({ code: content }),
345
+ },
346
+ );
347
+ if (!res.ok) throw new Error(await res.text());
348
+ }
349
+
350
+ export async function renameContextFile(
351
+ questionId: string,
352
+ fileId: string,
353
+ label: string,
354
+ ): Promise<ContextFile> {
355
+ const res = await fetch(
356
+ `${BASE}/questions/${questionId}/context-files/${fileId}`,
357
+ {
358
+ method: "PATCH",
359
+ headers: { "Content-Type": "application/json" },
360
+ body: JSON.stringify({ label }),
361
+ },
362
+ );
363
+ if (!res.ok) throw new Error(await res.text());
364
+ return res.json();
365
+ }
366
+
367
+ export async function runInfraAction(input: {
368
+ questionId?: string;
369
+ fileId?: string;
370
+ label?: string;
371
+ action: "validate" | "plan";
372
+ workspace: InfraLabWorkspace;
373
+ }): Promise<InfraRunDetails> {
374
+ const res = await fetch(`${BASE}/infra/run`, {
375
+ method: "POST",
376
+ headers: { "Content-Type": "application/json" },
377
+ body: JSON.stringify(input),
378
+ });
379
+ if (!res.ok) {
380
+ const err = await res.json().catch(() => ({ error: "Infra run failed" }));
381
+ throw new Error(err.error ?? "Infra run failed");
382
+ }
383
+ return res.json();
384
+ }
385
+
386
+ export async function fetchInfraRuns(
387
+ fileId: string,
388
+ ): Promise<InfraRunListItem[]> {
389
+ const res = await fetch(
390
+ `${BASE}/infra/runs?fileId=${encodeURIComponent(fileId)}`,
391
+ );
392
+ if (!res.ok) throw new Error(await res.text());
393
+ return res.json();
394
+ }
395
+
396
+ export async function streamInfraCommand(
397
+ input: {
398
+ questionId?: string;
399
+ fileId?: string;
400
+ label?: string;
401
+ command: string;
402
+ workspace: InfraLabWorkspace;
403
+ },
404
+ onMessage: (message: InfraCommandStreamMessage) => void,
405
+ ): Promise<void> {
406
+ const res = await fetch(`${BASE}/infra/command-stream`, {
407
+ method: "POST",
408
+ headers: { "Content-Type": "application/json" },
409
+ body: JSON.stringify(input),
410
+ });
411
+
412
+ if (!res.ok || !res.body) {
413
+ const error = await res.text().catch(() => "Infra command failed");
414
+ throw new Error(error || "Infra command failed");
415
+ }
416
+
417
+ const reader = res.body.getReader();
418
+ const decoder = new TextDecoder();
419
+ let buffer = "";
420
+
421
+ const flushBuffer = () => {
422
+ const events = buffer.replace(/\r/g, "").split("\n\n");
423
+ buffer = events.pop() ?? "";
424
+
425
+ for (const event of events) {
426
+ if (!event.trim()) continue;
427
+ const payload = event
428
+ .split("\n")
429
+ .filter((line) => line.startsWith("data:"))
430
+ .map((line) => line.slice(5).trimStart())
431
+ .join("\n");
432
+
433
+ if (!payload) continue;
434
+ onMessage(JSON.parse(payload) as InfraCommandStreamMessage);
435
+ }
436
+ };
437
+
438
+ while (true) {
439
+ const { value, done } = await reader.read();
440
+ buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done });
441
+ flushBuffer();
442
+ if (done) break;
443
+ }
444
+
445
+ if (buffer.trim()) {
446
+ flushBuffer();
447
+ }
448
+ }
449
+
450
+ export async function fetchInfraRun(runId: string): Promise<InfraRunDetails> {
451
+ const res = await fetch(`${BASE}/infra/runs/${encodeURIComponent(runId)}`);
452
+ if (!res.ok) throw new Error(await res.text());
453
+ return res.json();
454
+ }
455
+
456
+ export async function streamInfraAsk(
457
+ input: {
458
+ messages: Array<{ role: "user" | "assistant"; content: string }>;
459
+ workspace: Record<string, string>;
460
+ questionId?: string;
461
+ },
462
+ onDelta: (chunk: string) => void,
463
+ ): Promise<void> {
464
+ const res = await fetch(`${BASE}/infra/ask`, {
465
+ method: "POST",
466
+ headers: { "Content-Type": "application/json" },
467
+ body: JSON.stringify(input),
468
+ });
469
+
470
+ if (!res.ok || !res.body) {
471
+ const err = await res.text().catch(() => "Infra ask failed");
472
+ throw new Error(err || "Infra ask failed");
473
+ }
474
+
475
+ const reader = res.body.getReader();
476
+ const decoder = new TextDecoder();
477
+
478
+ while (true) {
479
+ const { value, done } = await reader.read();
480
+ if (done) break;
481
+ const chunk = decoder.decode(value, { stream: true });
482
+ // AI SDK UIMessageStream emits SSE lines like: data: {"type":"text-delta","id":"0","delta":"..."}
483
+ for (const line of chunk.split("\n")) {
484
+ if (!line.startsWith("data: ")) continue;
485
+ try {
486
+ const json = JSON.parse(line.slice(6));
487
+ if (json.type === "text-delta" && typeof json.delta === "string") {
488
+ onDelta(json.delta);
489
+ }
490
+ } catch {
491
+ // ignore malformed chunk
492
+ }
493
+ }
494
+ }
495
+ }
496
+
497
+ export async function streamNotesAsk(
498
+ input: {
499
+ messages: Array<{ role: "user" | "assistant"; content: string }>;
500
+ noteContent?: string;
501
+ noteName?: string;
502
+ },
503
+ onDelta: (chunk: string) => void,
504
+ ): Promise<void> {
505
+ const res = await fetch(`${BASE}/notes/ask`, {
506
+ method: "POST",
507
+ headers: { "Content-Type": "application/json" },
508
+ body: JSON.stringify(input),
509
+ });
510
+
511
+ if (!res.ok || !res.body) {
512
+ const err = await res.text().catch(() => "Notes ask failed");
513
+ throw new Error(err || "Notes ask failed");
514
+ }
515
+
516
+ const reader = res.body.getReader();
517
+ const decoder = new TextDecoder();
518
+
519
+ while (true) {
520
+ const { value, done } = await reader.read();
521
+ if (done) break;
522
+ const chunk = decoder.decode(value, { stream: true });
523
+ for (const line of chunk.split("\n")) {
524
+ if (!line.startsWith("data: ")) continue;
525
+ try {
526
+ const json = JSON.parse(line.slice(6));
527
+ if (json.type === "text-delta" && typeof json.delta === "string") {
528
+ onDelta(json.delta);
529
+ }
530
+ } catch {
531
+ // ignore malformed chunk
532
+ }
533
+ }
534
+ }
535
+ }
536
+
537
+ export async function streamFrontendLabAsk(
538
+ input: {
539
+ messages: Array<{ role: "user" | "assistant"; content: string }>;
540
+ workspace: Record<string, string>;
541
+ labType: "react" | "nextjs";
542
+ questionId?: string;
543
+ },
544
+ onDelta: (chunk: string) => void,
545
+ ): Promise<void> {
546
+ const res = await fetch(`${BASE}/frontend-lab/ask`, {
547
+ method: "POST",
548
+ headers: { "Content-Type": "application/json" },
549
+ body: JSON.stringify(input),
550
+ });
551
+
552
+ if (!res.ok || !res.body) {
553
+ const err = await res.text().catch(() => "Frontend lab ask failed");
554
+ throw new Error(err || "Frontend lab ask failed");
555
+ }
556
+
557
+ const reader = res.body.getReader();
558
+ const decoder = new TextDecoder();
559
+
560
+ while (true) {
561
+ const { value, done } = await reader.read();
562
+ if (done) break;
563
+ const chunk = decoder.decode(value, { stream: true });
564
+ for (const line of chunk.split("\n")) {
565
+ if (!line.startsWith("data: ")) continue;
566
+ try {
567
+ const json = JSON.parse(line.slice(6));
568
+ if (json.type === "text-delta" && typeof json.delta === "string") {
569
+ onDelta(json.delta);
570
+ }
571
+ } catch {
572
+ // ignore malformed chunk
573
+ }
574
+ }
575
+ }
576
+ }
577
+
166
578
  // --- Code Context ---
167
579
 
168
580
  export async function fetchCodeContextTree(): Promise<string[]> {
@@ -282,7 +694,7 @@ export async function fetchDriveSubfolders(
282
694
  export async function createDriveSubfolder(
283
695
  workspaceId: string,
284
696
  name: string,
285
- ): Promise<DriveFolder> {
697
+ ): Promise<DriveFolder | { needsAuth: true; authUrl: string }> {
286
698
  const res = await fetch(
287
699
  `${BASE}/workspaces/${workspaceId}/drive-subfolders`,
288
700
  {
@@ -358,3 +770,43 @@ export async function attachDriveFolder(
358
770
  const data = await res.json();
359
771
  return data.registry;
360
772
  }
773
+
774
+ // ── Next.js real-server sandbox ──────────────────────────────────────────────
775
+
776
+ export interface NextjsSandboxInfo {
777
+ id: string;
778
+ port: number;
779
+ url: string;
780
+ }
781
+
782
+ export async function startNextjsSandbox(
783
+ files: Record<string, string>,
784
+ ): Promise<NextjsSandboxInfo> {
785
+ const res = await fetch(`${BASE}/nextjs/start`, {
786
+ method: "POST",
787
+ headers: { "Content-Type": "application/json" },
788
+ body: JSON.stringify({ files }),
789
+ });
790
+ if (!res.ok) {
791
+ const body = await res.json().catch(() => ({}));
792
+ throw new Error(
793
+ (body as any).error || `Failed to start Next.js (${res.status})`,
794
+ );
795
+ }
796
+ return res.json();
797
+ }
798
+
799
+ export async function updateNextjsFiles(
800
+ id: string,
801
+ files: Record<string, string>,
802
+ ): Promise<void> {
803
+ await fetch(`${BASE}/nextjs/${id}/update-files`, {
804
+ method: "POST",
805
+ headers: { "Content-Type": "application/json" },
806
+ body: JSON.stringify({ files }),
807
+ });
808
+ }
809
+
810
+ export async function stopNextjsSandbox(id: string): Promise<void> {
811
+ await fetch(`${BASE}/nextjs/${id}`, { method: "DELETE" });
812
+ }