create-interview-cockpit 0.5.0 → 0.7.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 (30) hide show
  1. package/package.json +1 -1
  2. package/template/client/package-lock.json +734 -1
  3. package/template/client/package.json +1 -0
  4. package/template/client/src/App.tsx +3 -0
  5. package/template/client/src/api.ts +384 -4
  6. package/template/client/src/components/AiSettingsModal.tsx +818 -425
  7. package/template/client/src/components/ChatMessage.tsx +34 -12
  8. package/template/client/src/components/ChatView.tsx +298 -121
  9. package/template/client/src/components/CodeContextPanel.tsx +530 -2
  10. package/template/client/src/components/CodeRunnerModal.tsx +1895 -120
  11. package/template/client/src/components/DocRefModal.tsx +55 -6
  12. package/template/client/src/components/FileAttachments.tsx +20 -4
  13. package/template/client/src/components/InfraLabModal.tsx +1706 -0
  14. package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
  15. package/template/client/src/components/MarkdownRenderer.tsx +22 -8
  16. package/template/client/src/components/NotesModal.tsx +977 -0
  17. package/template/client/src/components/PlotEmbed.tsx +173 -0
  18. package/template/client/src/components/Sidebar.tsx +184 -0
  19. package/template/client/src/components/VizCraftEmbed.tsx +257 -13
  20. package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
  21. package/template/client/src/infraLab.ts +124 -0
  22. package/template/client/src/reactLab.ts +960 -0
  23. package/template/client/src/store.ts +250 -6
  24. package/template/client/src/types.ts +36 -3
  25. package/template/client/tsconfig.tsbuildinfo +1 -1
  26. package/template/cockpit.json +1 -1
  27. package/template/server/src/google-drive.ts +39 -3
  28. package/template/server/src/index.ts +954 -52
  29. package/template/server/src/infra-runner.ts +1104 -0
  30. package/template/server/src/storage.ts +22 -3
@@ -20,6 +20,7 @@
20
20
  "react-simple-code-editor": "^0.14.1",
21
21
  "react-syntax-highlighter": "^15.6.1",
22
22
  "remark-gfm": "^4.0.0",
23
+ "vega-embed": "^7.1.0",
23
24
  "vizcraft": "^1.17.0",
24
25
  "yaml": "^2.8.3",
25
26
  "zustand": "^5.0.0"
@@ -7,6 +7,7 @@ import FileViewerModal from "./components/FileViewerModal";
7
7
  import DocRefModal from "./components/DocRefModal";
8
8
  import AiSettingsModal from "./components/AiSettingsModal";
9
9
  import CodeRunnerModal from "./components/CodeRunnerModal";
10
+ import InfraLabModal from "./components/InfraLabModal";
10
11
  import { Code, Plane, PanelLeftClose, PanelLeft, Settings } from "lucide-react";
11
12
 
12
13
  export default function App() {
@@ -30,6 +31,7 @@ export default function App() {
30
31
  openSettings,
31
32
  closeSettings,
32
33
  showCodeRunner,
34
+ showInfraLab,
33
35
  closeCodeRunner,
34
36
  } = useStore();
35
37
 
@@ -157,6 +159,7 @@ export default function App() {
157
159
  )}
158
160
  {showSettings && <AiSettingsModal />}
159
161
  {showCodeRunner && <CodeRunnerModal />}
162
+ {showInfraLab && <InfraLabModal />}
160
163
  </div>
161
164
  );
162
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,6 +26,7 @@ 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>;
25
32
  /** When true, preference prompt texts are appended to every message (not just on change). */
@@ -32,6 +39,65 @@ export interface AiSettings {
32
39
  model?: string;
33
40
  }
34
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
+
35
101
  export async function fetchAiSettings(): Promise<AiSettings> {
36
102
  const res = await fetch(`${BASE}/settings`);
37
103
  return res.json();
@@ -68,7 +134,7 @@ export async function deleteTopic(id: string): Promise<void> {
68
134
 
69
135
  export async function updateTopic(
70
136
  id: string,
71
- data: { name?: string },
137
+ data: { name?: string; systemContext?: string },
72
138
  ): Promise<Topic> {
73
139
  const res = await fetch(`${BASE}/topics/${id}`, {
74
140
  method: "PATCH",
@@ -254,7 +320,14 @@ export async function saveCodeSnippet(
254
320
  code: string,
255
321
  language: string,
256
322
  label: string,
257
- origin: "user" | "ai" | "sandbox",
323
+ origin:
324
+ | "user"
325
+ | "ai"
326
+ | "sandbox"
327
+ | "infra"
328
+ | "react"
329
+ | "nextjs"
330
+ | "module-federation",
258
331
  ): Promise<ContextFile> {
259
332
  const res = await fetch(`${BASE}/questions/${questionId}/save-code-snippet`, {
260
333
  method: "POST",
@@ -298,6 +371,217 @@ export async function renameContextFile(
298
371
  return res.json();
299
372
  }
300
373
 
374
+ export async function runInfraAction(input: {
375
+ questionId?: string;
376
+ fileId?: string;
377
+ label?: string;
378
+ action: "validate" | "plan";
379
+ workspace: InfraLabWorkspace;
380
+ }): Promise<InfraRunDetails> {
381
+ const res = await fetch(`${BASE}/infra/run`, {
382
+ method: "POST",
383
+ headers: { "Content-Type": "application/json" },
384
+ body: JSON.stringify(input),
385
+ });
386
+ if (!res.ok) {
387
+ const err = await res.json().catch(() => ({ error: "Infra run failed" }));
388
+ throw new Error(err.error ?? "Infra run failed");
389
+ }
390
+ return res.json();
391
+ }
392
+
393
+ export async function fetchInfraRuns(
394
+ fileId: string,
395
+ ): Promise<InfraRunListItem[]> {
396
+ const res = await fetch(
397
+ `${BASE}/infra/runs?fileId=${encodeURIComponent(fileId)}`,
398
+ );
399
+ if (!res.ok) throw new Error(await res.text());
400
+ return res.json();
401
+ }
402
+
403
+ export async function streamInfraCommand(
404
+ input: {
405
+ questionId?: string;
406
+ fileId?: string;
407
+ label?: string;
408
+ command: string;
409
+ workspace: InfraLabWorkspace;
410
+ },
411
+ onMessage: (message: InfraCommandStreamMessage) => void,
412
+ ): Promise<void> {
413
+ const res = await fetch(`${BASE}/infra/command-stream`, {
414
+ method: "POST",
415
+ headers: { "Content-Type": "application/json" },
416
+ body: JSON.stringify(input),
417
+ });
418
+
419
+ if (!res.ok || !res.body) {
420
+ const error = await res.text().catch(() => "Infra command failed");
421
+ throw new Error(error || "Infra command failed");
422
+ }
423
+
424
+ const reader = res.body.getReader();
425
+ const decoder = new TextDecoder();
426
+ let buffer = "";
427
+
428
+ const flushBuffer = () => {
429
+ const events = buffer.replace(/\r/g, "").split("\n\n");
430
+ buffer = events.pop() ?? "";
431
+
432
+ for (const event of events) {
433
+ if (!event.trim()) continue;
434
+ const payload = event
435
+ .split("\n")
436
+ .filter((line) => line.startsWith("data:"))
437
+ .map((line) => line.slice(5).trimStart())
438
+ .join("\n");
439
+
440
+ if (!payload) continue;
441
+ onMessage(JSON.parse(payload) as InfraCommandStreamMessage);
442
+ }
443
+ };
444
+
445
+ while (true) {
446
+ const { value, done } = await reader.read();
447
+ buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done });
448
+ flushBuffer();
449
+ if (done) break;
450
+ }
451
+
452
+ if (buffer.trim()) {
453
+ flushBuffer();
454
+ }
455
+ }
456
+
457
+ export async function fetchInfraRun(runId: string): Promise<InfraRunDetails> {
458
+ const res = await fetch(`${BASE}/infra/runs/${encodeURIComponent(runId)}`);
459
+ if (!res.ok) throw new Error(await res.text());
460
+ return res.json();
461
+ }
462
+
463
+ export async function streamInfraAsk(
464
+ input: {
465
+ messages: Array<{ role: "user" | "assistant"; content: string }>;
466
+ workspace: Record<string, string>;
467
+ questionId?: string;
468
+ },
469
+ onDelta: (chunk: string) => void,
470
+ ): Promise<void> {
471
+ const res = await fetch(`${BASE}/infra/ask`, {
472
+ method: "POST",
473
+ headers: { "Content-Type": "application/json" },
474
+ body: JSON.stringify(input),
475
+ });
476
+
477
+ if (!res.ok || !res.body) {
478
+ const err = await res.text().catch(() => "Infra ask failed");
479
+ throw new Error(err || "Infra ask failed");
480
+ }
481
+
482
+ const reader = res.body.getReader();
483
+ const decoder = new TextDecoder();
484
+
485
+ while (true) {
486
+ const { value, done } = await reader.read();
487
+ if (done) break;
488
+ const chunk = decoder.decode(value, { stream: true });
489
+ // AI SDK UIMessageStream emits SSE lines like: data: {"type":"text-delta","id":"0","delta":"..."}
490
+ for (const line of chunk.split("\n")) {
491
+ if (!line.startsWith("data: ")) continue;
492
+ try {
493
+ const json = JSON.parse(line.slice(6));
494
+ if (json.type === "text-delta" && typeof json.delta === "string") {
495
+ onDelta(json.delta);
496
+ }
497
+ } catch {
498
+ // ignore malformed chunk
499
+ }
500
+ }
501
+ }
502
+ }
503
+
504
+ export async function streamNotesAsk(
505
+ input: {
506
+ messages: Array<{ role: "user" | "assistant"; content: string }>;
507
+ noteContent?: string;
508
+ noteName?: string;
509
+ },
510
+ onDelta: (chunk: string) => void,
511
+ ): Promise<void> {
512
+ const res = await fetch(`${BASE}/notes/ask`, {
513
+ method: "POST",
514
+ headers: { "Content-Type": "application/json" },
515
+ body: JSON.stringify(input),
516
+ });
517
+
518
+ if (!res.ok || !res.body) {
519
+ const err = await res.text().catch(() => "Notes ask failed");
520
+ throw new Error(err || "Notes ask failed");
521
+ }
522
+
523
+ const reader = res.body.getReader();
524
+ const decoder = new TextDecoder();
525
+
526
+ while (true) {
527
+ const { value, done } = await reader.read();
528
+ if (done) break;
529
+ const chunk = decoder.decode(value, { stream: true });
530
+ for (const line of chunk.split("\n")) {
531
+ if (!line.startsWith("data: ")) continue;
532
+ try {
533
+ const json = JSON.parse(line.slice(6));
534
+ if (json.type === "text-delta" && typeof json.delta === "string") {
535
+ onDelta(json.delta);
536
+ }
537
+ } catch {
538
+ // ignore malformed chunk
539
+ }
540
+ }
541
+ }
542
+ }
543
+
544
+ export async function streamFrontendLabAsk(
545
+ input: {
546
+ messages: Array<{ role: "user" | "assistant"; content: string }>;
547
+ workspace: Record<string, string>;
548
+ labType: "react" | "nextjs" | "module-federation";
549
+ questionId?: string;
550
+ },
551
+ onDelta: (chunk: string) => void,
552
+ ): Promise<void> {
553
+ const res = await fetch(`${BASE}/frontend-lab/ask`, {
554
+ method: "POST",
555
+ headers: { "Content-Type": "application/json" },
556
+ body: JSON.stringify(input),
557
+ });
558
+
559
+ if (!res.ok || !res.body) {
560
+ const err = await res.text().catch(() => "Frontend lab ask failed");
561
+ throw new Error(err || "Frontend lab ask failed");
562
+ }
563
+
564
+ const reader = res.body.getReader();
565
+ const decoder = new TextDecoder();
566
+
567
+ while (true) {
568
+ const { value, done } = await reader.read();
569
+ if (done) break;
570
+ const chunk = decoder.decode(value, { stream: true });
571
+ for (const line of chunk.split("\n")) {
572
+ if (!line.startsWith("data: ")) continue;
573
+ try {
574
+ const json = JSON.parse(line.slice(6));
575
+ if (json.type === "text-delta" && typeof json.delta === "string") {
576
+ onDelta(json.delta);
577
+ }
578
+ } catch {
579
+ // ignore malformed chunk
580
+ }
581
+ }
582
+ }
583
+ }
584
+
301
585
  // --- Code Context ---
302
586
 
303
587
  export async function fetchCodeContextTree(): Promise<string[]> {
@@ -417,7 +701,7 @@ export async function fetchDriveSubfolders(
417
701
  export async function createDriveSubfolder(
418
702
  workspaceId: string,
419
703
  name: string,
420
- ): Promise<DriveFolder> {
704
+ ): Promise<DriveFolder | { needsAuth: true; authUrl: string }> {
421
705
  const res = await fetch(
422
706
  `${BASE}/workspaces/${workspaceId}/drive-subfolders`,
423
707
  {
@@ -493,3 +777,99 @@ export async function attachDriveFolder(
493
777
  const data = await res.json();
494
778
  return data.registry;
495
779
  }
780
+
781
+ // ── Next.js real-server sandbox ──────────────────────────────────────────────
782
+
783
+ export interface NextjsSandboxInfo {
784
+ id: string;
785
+ port: number;
786
+ url: string;
787
+ }
788
+
789
+ export interface ModuleFederationSandboxInfo {
790
+ id: string;
791
+ hostUrl: string;
792
+ appUrls: Record<string, string>;
793
+ }
794
+
795
+ export interface ModuleFederationSandboxStatus {
796
+ running: boolean;
797
+ ready?: boolean;
798
+ hostUrl?: string;
799
+ appUrls?: Record<string, string>;
800
+ logs?: string[];
801
+ }
802
+
803
+ export async function startNextjsSandbox(
804
+ files: Record<string, string>,
805
+ ): Promise<NextjsSandboxInfo> {
806
+ const res = await fetch(`${BASE}/nextjs/start`, {
807
+ method: "POST",
808
+ headers: { "Content-Type": "application/json" },
809
+ body: JSON.stringify({ files }),
810
+ });
811
+ if (!res.ok) {
812
+ const body = await res.json().catch(() => ({}));
813
+ throw new Error(
814
+ (body as any).error || `Failed to start Next.js (${res.status})`,
815
+ );
816
+ }
817
+ return res.json();
818
+ }
819
+
820
+ export async function updateNextjsFiles(
821
+ id: string,
822
+ files: Record<string, string>,
823
+ ): Promise<void> {
824
+ await fetch(`${BASE}/nextjs/${id}/update-files`, {
825
+ method: "POST",
826
+ headers: { "Content-Type": "application/json" },
827
+ body: JSON.stringify({ files }),
828
+ });
829
+ }
830
+
831
+ export async function stopNextjsSandbox(id: string): Promise<void> {
832
+ await fetch(`${BASE}/nextjs/${id}`, { method: "DELETE" });
833
+ }
834
+
835
+ export async function startModuleFederationSandbox(
836
+ files: Record<string, string>,
837
+ ): Promise<ModuleFederationSandboxInfo> {
838
+ const res = await fetch(`${BASE}/module-federation/start`, {
839
+ method: "POST",
840
+ headers: { "Content-Type": "application/json" },
841
+ body: JSON.stringify({ files }),
842
+ });
843
+ if (!res.ok) {
844
+ const body = await res.json().catch(() => ({}));
845
+ throw new Error(
846
+ (body as any).error ||
847
+ `Failed to start webpack module federation lab (${res.status})`,
848
+ );
849
+ }
850
+ return res.json();
851
+ }
852
+
853
+ export async function updateModuleFederationFiles(
854
+ id: string,
855
+ files: Record<string, string>,
856
+ ): Promise<void> {
857
+ const res = await fetch(`${BASE}/module-federation/${id}/update-files`, {
858
+ method: "POST",
859
+ headers: { "Content-Type": "application/json" },
860
+ body: JSON.stringify({ files }),
861
+ });
862
+ if (!res.ok) throw new Error(await res.text());
863
+ }
864
+
865
+ export async function fetchModuleFederationStatus(
866
+ id: string,
867
+ ): Promise<ModuleFederationSandboxStatus> {
868
+ const res = await fetch(`${BASE}/module-federation/${id}/status`);
869
+ if (!res.ok) throw new Error(await res.text());
870
+ return res.json();
871
+ }
872
+
873
+ export async function stopModuleFederationSandbox(id: string): Promise<void> {
874
+ await fetch(`${BASE}/module-federation/${id}`, { method: "DELETE" });
875
+ }