create-interview-cockpit 0.5.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 (29) 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 +321 -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 +419 -2
  10. package/template/client/src/components/CodeRunnerModal.tsx +1601 -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 +477 -0
  23. package/template/client/src/store.ts +219 -6
  24. package/template/client/src/types.ts +35 -3
  25. package/template/client/tsconfig.tsbuildinfo +1 -1
  26. package/template/server/src/google-drive.ts +37 -3
  27. package/template/server/src/index.ts +693 -52
  28. package/template/server/src/infra-runner.ts +1104 -0
  29. package/template/server/src/storage.ts +13 -3
@@ -1,8 +1,36 @@
1
1
  import { create } from "zustand";
2
- import type { Topic, Question, CodeSnippet, WorkspaceMeta } from "./types";
2
+ import type {
3
+ Topic,
4
+ Question,
5
+ CodeSnippet,
6
+ WorkspaceMeta,
7
+ InfraLabWorkspace,
8
+ FrontendLabWorkspace,
9
+ } from "./types";
3
10
  import type { AiSettings } from "./api";
4
11
  import * as api from "./api";
5
12
 
13
+ // Default Express server used when opening a React/Next.js sandbox with no prior server code
14
+ const DEFAULT_SERVER_CODE = `import express from 'express';
15
+ const app = express();
16
+ app.use(express.json());
17
+ // Allow requests from the React preview iframe
18
+ app.use((_req, res, next) => {
19
+ res.setHeader('Access-Control-Allow-Origin', '*');
20
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
21
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
22
+ if (_req.method === 'OPTIONS') { res.sendStatus(204); return; }
23
+ next();
24
+ });
25
+
26
+ app.get('/api/hello', (_req, res) => {
27
+ res.json({ message: 'Hello from Express!', time: Date.now() });
28
+ });
29
+
30
+ const port = Number(process.env.PORT);
31
+ app.listen(port, () => console.log('Server on :' + port));
32
+ `;
33
+
6
34
  const DEFAULT_AI_SETTINGS: AiSettings = {
7
35
  systemPrompt: "",
8
36
  responseProfiles: {
@@ -11,6 +39,7 @@ const DEFAULT_AI_SETTINGS: AiSettings = {
11
39
  normal: { maxOutputTokens: 3000, maxSteps: 5 },
12
40
  },
13
41
  vizGuide: "",
42
+ plotGuide: "",
14
43
  alwaysSendPrefsDefault: false,
15
44
  thinkingBudget: 0,
16
45
  promptGroups: {
@@ -52,6 +81,19 @@ const DEFAULT_AI_SETTINGS: AiSettings = {
52
81
  "When using technical terms or abbreviations, immediately expand their meaning in square brackets right after the term — e.g. 'TCP [Transmission Control Protocol — a connection-oriented transport protocol]' or 'idempotent [an operation that produces the same result no matter how many times it is applied]'. Do this throughout your response so I never need to look anything up.",
53
82
  },
54
83
  },
84
+ "diagram-use": {
85
+ label: "Diagram Usage",
86
+ description: "Which visual formats to use and how often.",
87
+ default: "none",
88
+ options: {
89
+ none: "",
90
+ vizcraft:
91
+ "Prioritize using vizcraft (viz) diagrams as much as possible. *NB:* Character limits do not apply to these diagrams",
92
+ mermaid:
93
+ "Prioritize using mermaid diagrams as much as possible. *NB:* Character limits do not apply to these diagrams",
94
+ plot: "Prioritize using plot blocks for graphs, curves, distributions, and data charts when they would make the explanation clearer. *NB:* Character limits do not apply to these plots",
95
+ },
96
+ },
55
97
  },
56
98
  };
57
99
 
@@ -108,19 +150,30 @@ interface Store {
108
150
  createDriveSubfolder: (
109
151
  id: string,
110
152
  name: string,
111
- ) => Promise<import("./api").DriveFolder>;
153
+ ) => Promise<
154
+ import("./api").DriveFolder | { needsAuth: true; authUrl: string }
155
+ >;
112
156
 
113
157
  fetchTopics: () => Promise<void>;
114
158
  fetchQuestions: (topicId: string) => Promise<void>;
115
159
  addTopic: (name: string) => Promise<void>;
116
160
  removeTopic: (id: string) => Promise<void>;
117
161
  renameTopic: (id: string, name: string) => Promise<void>;
162
+ updateTopicSystemContext: (
163
+ id: string,
164
+ systemContext: string,
165
+ ) => Promise<void>;
118
166
  addQuestion: (topicId: string, title: string) => Promise<void>;
119
167
  addChildQuestion: (
120
168
  topicId: string,
121
169
  parentQuestionId: string,
122
170
  title: string,
123
171
  ) => Promise<void>;
172
+ moveQuestion: (
173
+ questionId: string,
174
+ topicId: string,
175
+ parentQuestionId: string | null,
176
+ ) => Promise<void>;
124
177
  removeQuestion: (questionId: string, topicId: string) => Promise<void>;
125
178
  renameQuestion: (
126
179
  questionId: string,
@@ -159,10 +212,14 @@ interface Store {
159
212
  code: string,
160
213
  language: string,
161
214
  label: string,
162
- origin: "user" | "ai" | "sandbox",
215
+ origin: "user" | "ai" | "sandbox" | "infra" | "react" | "nextjs",
163
216
  ) => Promise<import("./types").ContextFile>;
164
217
  clearMessages: (questionId: string) => Promise<void>;
165
218
 
219
+ // ── Linked Conversations ─────────────────────────────────────
220
+ linkConversation: (questionId: string, linkedId: string) => Promise<void>;
221
+ unlinkConversation: (questionId: string, linkedId: string) => Promise<void>;
222
+
166
223
  // ── Workspace Context Files ──────────────────────────────────
167
224
  workspaceFiles: import("./types").ContextFile[];
168
225
  fetchWorkspaceFiles: () => Promise<void>;
@@ -215,14 +272,25 @@ interface Store {
215
272
  clientCode: string;
216
273
  clientLang: string;
217
274
  fileId?: string;
275
+ /** If set, the client panel opens in React or Next.js preview mode instead of script mode */
276
+ clientType?: "script" | "react" | "nextjs";
277
+ reactFiles?: Record<string, string> | null;
278
+ reactActiveFile?: string | null;
218
279
  } | null;
219
- openCodeRunner: (code?: string, language?: string) => void;
280
+ /** When set, the script runner opens with this file pre-loaded — Save will overwrite it */
281
+ runnerInitialFileId: string | null;
282
+ openCodeRunner: (code?: string, language?: string, fileId?: string) => void;
220
283
  openSandbox: (
221
284
  serverCode: string,
222
285
  serverLang: string,
223
286
  clientCode: string,
224
287
  clientLang: string,
225
288
  fileId?: string,
289
+ opts?: {
290
+ clientType?: "script" | "react" | "nextjs";
291
+ reactFiles?: Record<string, string>;
292
+ reactActiveFile?: string;
293
+ },
226
294
  ) => void;
227
295
  overwriteContextFileContent: (
228
296
  questionId: string,
@@ -235,6 +303,27 @@ interface Store {
235
303
  label: string,
236
304
  ) => Promise<void>;
237
305
  closeCodeRunner: () => void;
306
+
307
+ // ── Infra Lab ────────────────────────────────────────────────
308
+ showInfraLab: boolean;
309
+ runnerInitialInfra: InfraLabWorkspace | null;
310
+ runnerInitialInfraFileId: string | null;
311
+ openInfraLab: (workspace?: InfraLabWorkspace, fileId?: string) => void;
312
+ closeInfraLab: () => void;
313
+
314
+ // ── Frontend Labs (React / Next.js) — open inside the sandbox ──
315
+ openReactLab: (
316
+ workspace?: FrontendLabWorkspace,
317
+ fileId?: string,
318
+ serverCode?: string,
319
+ serverLang?: string,
320
+ ) => void;
321
+ openNextLab: (
322
+ workspace?: FrontendLabWorkspace,
323
+ fileId?: string,
324
+ serverCode?: string,
325
+ serverLang?: string,
326
+ ) => void;
238
327
  }
239
328
 
240
329
  export const useStore = create<Store>((set, get) => ({
@@ -259,6 +348,10 @@ export const useStore = create<Store>((set, get) => ({
259
348
  runnerInitialCode: "",
260
349
  runnerInitialLanguage: "typescript",
261
350
  runnerInitialSandbox: null,
351
+ runnerInitialFileId: null,
352
+ showInfraLab: false,
353
+ runnerInitialInfra: null,
354
+ runnerInitialInfraFileId: null,
262
355
 
263
356
  // ── Workspaces ───────────────────────────────────────────────
264
357
  workspaces: [],
@@ -448,6 +541,13 @@ export const useStore = create<Store>((set, get) => ({
448
541
  }));
449
542
  },
450
543
 
544
+ updateTopicSystemContext: async (id, systemContext) => {
545
+ await api.updateTopic(id, { systemContext });
546
+ set((s) => ({
547
+ topics: s.topics.map((t) => (t.id === id ? { ...t, systemContext } : t)),
548
+ }));
549
+ },
550
+
451
551
  addQuestion: async (topicId, title) => {
452
552
  const question = await api.createQuestion(topicId, title);
453
553
  set((s) => ({
@@ -468,6 +568,29 @@ export const useStore = create<Store>((set, get) => ({
468
568
  }));
469
569
  },
470
570
 
571
+ moveQuestion: async (questionId, topicId, parentQuestionId) => {
572
+ await api.updateQuestion(questionId, {
573
+ parentQuestionId,
574
+ });
575
+ set((s) => ({
576
+ questionsByTopic: {
577
+ ...s.questionsByTopic,
578
+ [topicId]: (s.questionsByTopic[topicId] || []).map((q) =>
579
+ q.id === questionId
580
+ ? { ...q, parentQuestionId: parentQuestionId ?? undefined }
581
+ : q,
582
+ ),
583
+ },
584
+ currentQuestion:
585
+ s.currentQuestion?.id === questionId
586
+ ? {
587
+ ...s.currentQuestion,
588
+ parentQuestionId: parentQuestionId ?? undefined,
589
+ }
590
+ : s.currentQuestion,
591
+ }));
592
+ },
593
+
471
594
  removeQuestion: async (questionId, topicId) => {
472
595
  await api.deleteQuestion(questionId);
473
596
  set((s) => ({
@@ -682,6 +805,38 @@ export const useStore = create<Store>((set, get) => ({
682
805
  }));
683
806
  },
684
807
 
808
+ linkConversation: async (questionId, linkedId) => {
809
+ const q = get().currentQuestion;
810
+ const current =
811
+ q?.id === questionId ? q : await api.fetchQuestion(questionId);
812
+ const existing = current.linkedConversationIds ?? [];
813
+ if (existing.includes(linkedId)) return;
814
+ const updated = [...existing, linkedId];
815
+ await api.updateQuestion(questionId, { linkedConversationIds: updated });
816
+ set((s) => ({
817
+ currentQuestion:
818
+ s.currentQuestion?.id === questionId
819
+ ? { ...s.currentQuestion, linkedConversationIds: updated }
820
+ : s.currentQuestion,
821
+ }));
822
+ },
823
+
824
+ unlinkConversation: async (questionId, linkedId) => {
825
+ const q = get().currentQuestion;
826
+ const current =
827
+ q?.id === questionId ? q : await api.fetchQuestion(questionId);
828
+ const updated = (current.linkedConversationIds ?? []).filter(
829
+ (id) => id !== linkedId,
830
+ );
831
+ await api.updateQuestion(questionId, { linkedConversationIds: updated });
832
+ set((s) => ({
833
+ currentQuestion:
834
+ s.currentQuestion?.id === questionId
835
+ ? { ...s.currentQuestion, linkedConversationIds: updated }
836
+ : s.currentQuestion,
837
+ }));
838
+ },
839
+
685
840
  fetchWorkspaceFiles: async () => {
686
841
  const files = await api.fetchWorkspaceFiles();
687
842
  set({ workspaceFiles: files });
@@ -727,16 +882,26 @@ export const useStore = create<Store>((set, get) => ({
727
882
  openDocViewer: (fileId, quote, fileName) =>
728
883
  set({ viewingDoc: { fileId, quote, fileName } }),
729
884
  closeDocViewer: () => set({ viewingDoc: null }),
730
- openCodeRunner: (code = "", language = "typescript") =>
885
+ openCodeRunner: (code = "", language = "typescript", fileId?) =>
731
886
  set({
732
887
  showCodeRunner: true,
888
+ showInfraLab: false,
733
889
  runnerInitialCode: code,
734
890
  runnerInitialLanguage: language,
735
891
  runnerInitialSandbox: null,
892
+ runnerInitialFileId: fileId ?? null,
736
893
  }),
737
- openSandbox: (serverCode, serverLang, clientCode, clientLang, fileId?) =>
894
+ openSandbox: (
895
+ serverCode,
896
+ serverLang,
897
+ clientCode,
898
+ clientLang,
899
+ fileId?,
900
+ opts?,
901
+ ) =>
738
902
  set({
739
903
  showCodeRunner: true,
904
+ showInfraLab: false,
740
905
  runnerInitialCode: "",
741
906
  runnerInitialLanguage: "typescript",
742
907
  runnerInitialSandbox: {
@@ -745,7 +910,54 @@ export const useStore = create<Store>((set, get) => ({
745
910
  clientCode,
746
911
  clientLang,
747
912
  fileId,
913
+ clientType: opts?.clientType,
914
+ reactFiles: opts?.reactFiles,
915
+ reactActiveFile: opts?.reactActiveFile,
916
+ },
917
+ runnerInitialFileId: null,
918
+ }),
919
+ openInfraLab: (workspace, fileId?) =>
920
+ set({
921
+ showInfraLab: true,
922
+ showCodeRunner: false,
923
+ runnerInitialInfra: workspace ?? null,
924
+ runnerInitialInfraFileId: fileId ?? null,
925
+ }),
926
+ openReactLab: (workspace?, fileId?, serverCode?, serverLang?) =>
927
+ set({
928
+ showCodeRunner: true,
929
+ showInfraLab: false,
930
+ runnerInitialCode: "",
931
+ runnerInitialLanguage: "typescript",
932
+ runnerInitialSandbox: {
933
+ serverCode: serverCode ?? DEFAULT_SERVER_CODE,
934
+ serverLang: serverLang ?? "typescript",
935
+ clientCode: "",
936
+ clientLang: "javascript",
937
+ fileId,
938
+ clientType: "react",
939
+ reactFiles: workspace?.files ?? null,
940
+ reactActiveFile: workspace?.activeFile ?? null,
941
+ },
942
+ runnerInitialFileId: null,
943
+ }),
944
+ openNextLab: (workspace?, fileId?, serverCode?, serverLang?) =>
945
+ set({
946
+ showCodeRunner: true,
947
+ showInfraLab: false,
948
+ runnerInitialCode: "",
949
+ runnerInitialLanguage: "typescript",
950
+ runnerInitialSandbox: {
951
+ serverCode: serverCode ?? DEFAULT_SERVER_CODE,
952
+ serverLang: serverLang ?? "typescript",
953
+ clientCode: "",
954
+ clientLang: "javascript",
955
+ fileId,
956
+ clientType: "nextjs",
957
+ reactFiles: workspace?.files ?? null,
958
+ reactActiveFile: workspace?.activeFile ?? null,
748
959
  },
960
+ runnerInitialFileId: null,
749
961
  }),
750
962
 
751
963
  overwriteContextFileContent: async (questionId, fileId, content) => {
@@ -767,6 +979,7 @@ export const useStore = create<Store>((set, get) => ({
767
979
  }));
768
980
  },
769
981
  closeCodeRunner: () => set({ showCodeRunner: false }),
982
+ closeInfraLab: () => set({ showInfraLab: false }),
770
983
 
771
984
  fetchAiSettings: async () => {
772
985
  const settings = await api.fetchAiSettings();
@@ -1,3 +1,12 @@
1
+ export type ContextFileOrigin =
2
+ | "user"
3
+ | "ai"
4
+ | "upload"
5
+ | "sandbox"
6
+ | "infra"
7
+ | "react"
8
+ | "nextjs";
9
+
1
10
  export interface ContextFile {
2
11
  id: string;
3
12
  name: string;
@@ -6,14 +15,33 @@ export interface ContextFile {
6
15
  createdAt: string;
7
16
  /** Distinguishes how this file was added. 'upload' = user-uploaded doc,
8
17
  * 'user' = code saved from Code Runner, 'ai' = AI-generated code block,
9
- * 'sandbox' = paired server+client sandbox saved as JSON. */
10
- origin?: "user" | "ai" | "upload" | "sandbox";
18
+ * 'sandbox' = paired server+client sandbox saved as JSON,
19
+ * 'infra' = Terraform-style infra lab workspace saved as JSON. */
20
+ origin?: ContextFileOrigin;
11
21
  /** Language hint for code snippets (e.g. 'typescript', 'javascript'). */
12
22
  language?: string;
13
23
  /** Short display label for code snippets. */
14
24
  label?: string;
15
25
  }
16
26
 
27
+ export interface FrontendLabWorkspace {
28
+ version: 1;
29
+ label: string;
30
+ /** Determines defaults, entry file, and file-tree conventions. */
31
+ type: "react" | "nextjs";
32
+ activeFile: string;
33
+ files: Record<string, string>;
34
+ }
35
+
36
+ export interface InfraLabWorkspace {
37
+ version: 1;
38
+ label: string;
39
+ provider: "aws";
40
+ executionMode: "plan-only" | "localstack";
41
+ activeFile: string;
42
+ files: Record<string, string>;
43
+ }
44
+
17
45
  export interface WorkspaceMeta {
18
46
  id: string;
19
47
  name: string;
@@ -37,6 +65,8 @@ export interface Topic {
37
65
  id: string;
38
66
  name: string;
39
67
  contextFiles: ContextFile[];
68
+ /** Topic-wide system prompt prepended to every question's context in this topic. */
69
+ systemContext?: string;
40
70
  createdAt: string;
41
71
  }
42
72
 
@@ -82,7 +112,7 @@ export interface ReadingBookmark {
82
112
  export interface Question {
83
113
  id: string;
84
114
  topicId: string;
85
- parentQuestionId?: string;
115
+ parentQuestionId?: string | null;
86
116
  title: string;
87
117
  systemContext: string;
88
118
  codeContextFiles: string[];
@@ -90,5 +120,7 @@ export interface Question {
90
120
  messages: Message[];
91
121
  annotations?: Annotation[];
92
122
  readingBookmark?: ReadingBookmark;
123
+ /** IDs of other questions in the same topic whose conversation history is injected as context. */
124
+ linkedConversationIds?: string[];
93
125
  createdAt: string;
94
126
  }
@@ -1 +1 @@
1
- {"root":["./src/app.tsx","./src/api.ts","./src/main.tsx","./src/store.ts","./src/types.ts","./src/vite-env.d.ts","./src/components/chatmessage.tsx","./src/components/chatview.tsx","./src/components/codecontextpanel.tsx","./src/components/fileattachments.tsx","./src/components/fileviewermodal.tsx","./src/components/markdownrenderer.tsx","./src/components/mermaiddiagram.tsx","./src/components/sidebar.tsx","./src/components/textannotator.tsx"],"version":"5.9.3"}
1
+ {"root":["./src/app.tsx","./src/api.ts","./src/main.tsx","./src/store.ts","./src/types.ts","./src/vite-env.d.ts","./src/components/aisettingsmodal.tsx","./src/components/annotationdialog.tsx","./src/components/chatmessage.tsx","./src/components/chatview.tsx","./src/components/codecontextpanel.tsx","./src/components/codelineannotationpopup.tsx","./src/components/coderunnermodal.tsx","./src/components/docrefmodal.tsx","./src/components/fileattachments.tsx","./src/components/filepickermodal.tsx","./src/components/fileviewermodal.tsx","./src/components/linkedconvospicker.tsx","./src/components/markdownrenderer.tsx","./src/components/mermaiddiagram.tsx","./src/components/plotembed.tsx","./src/components/sidebar.tsx","./src/components/textannotator.tsx","./src/components/vizcraftembed.tsx","./src/components/workspaceswitcher.tsx"],"version":"5.9.3"}
@@ -357,7 +357,10 @@ export async function syncWorkspace(
357
357
  cs.content &&
358
358
  (cs.origin === "user" ||
359
359
  cs.origin === "ai" ||
360
- cs.origin === "sandbox")
360
+ cs.origin === "sandbox" ||
361
+ cs.origin === "react" ||
362
+ cs.origin === "nextjs" ||
363
+ cs.origin === "infra")
361
364
  ) {
362
365
  try {
363
366
  await fs.mkdir(ctxDir, { recursive: true });
@@ -562,7 +565,35 @@ async function getExportDriveClient(): Promise<drive_v3.Drive> {
562
565
  /* ok */
563
566
  }
564
567
  });
565
- return google.drive({ version: "v3", auth: client });
568
+
569
+ // Wrap the drive client so any invalid_grant errors clear the stored token
570
+ // and surface a friendlier error that the caller can detect.
571
+ const drive = google.drive({ version: "v3", auth: client });
572
+ const originalFiles = drive.files as any;
573
+ const patchMethod = (obj: any, method: string) => {
574
+ const orig = obj[method].bind(obj);
575
+ obj[method] = async (...args: any[]) => {
576
+ try {
577
+ return await orig(...args);
578
+ } catch (err: any) {
579
+ if (err?.message?.includes("invalid_grant") || err?.code === 400) {
580
+ try {
581
+ await fs.unlink(EXPORT_TOKENS_FILE);
582
+ } catch {
583
+ /* already gone */
584
+ }
585
+ const e = new Error("NEEDS_REAUTH");
586
+ (e as any).needsReauth = true;
587
+ throw e;
588
+ }
589
+ throw err;
590
+ }
591
+ };
592
+ };
593
+ ["create", "update", "delete", "list", "get"].forEach((m) =>
594
+ patchMethod(originalFiles, m),
595
+ );
596
+ return drive;
566
597
  }
567
598
 
568
599
  export interface ExportResult {
@@ -736,7 +767,10 @@ export async function exportWorkspace(
736
767
  if (
737
768
  cf.origin === "user" ||
738
769
  cf.origin === "ai" ||
739
- cf.origin === "sandbox"
770
+ cf.origin === "sandbox" ||
771
+ cf.origin === "react" ||
772
+ cf.origin === "nextjs" ||
773
+ cf.origin === "infra"
740
774
  ) {
741
775
  try {
742
776
  const content = await fs.readFile(