create-interview-cockpit 0.3.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.
Files changed (28) hide show
  1. package/README.md +23 -0
  2. package/package.json +1 -1
  3. package/template/client/package-lock.json +42 -0
  4. package/template/client/package.json +5 -0
  5. package/template/client/src/App.tsx +45 -12
  6. package/template/client/src/api.ts +174 -0
  7. package/template/client/src/components/AiSettingsModal.tsx +1041 -0
  8. package/template/client/src/components/AnnotationDialog.tsx +3 -9
  9. package/template/client/src/components/ChatMessage.tsx +110 -27
  10. package/template/client/src/components/ChatView.tsx +239 -137
  11. package/template/client/src/components/CodeContextPanel.tsx +297 -0
  12. package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
  13. package/template/client/src/components/CodeRunnerModal.tsx +1549 -0
  14. package/template/client/src/components/DocRefModal.tsx +502 -0
  15. package/template/client/src/components/FileAttachments.tsx +109 -9
  16. package/template/client/src/components/FilePickerModal.tsx +181 -0
  17. package/template/client/src/components/FileViewerModal.tsx +406 -28
  18. package/template/client/src/components/MarkdownRenderer.tsx +210 -2
  19. package/template/client/src/components/Sidebar.tsx +213 -125
  20. package/template/client/src/components/TextAnnotator.tsx +8 -15
  21. package/template/client/src/components/VizCraftEmbed.tsx +645 -0
  22. package/template/client/src/store.ts +275 -0
  23. package/template/client/src/types.ts +9 -0
  24. package/template/cockpit.json +1 -1
  25. package/template/data/ai-settings.json +49 -0
  26. package/template/server/src/google-drive.ts +109 -1
  27. package/template/server/src/index.ts +1187 -76
  28. package/template/server/src/storage.ts +359 -2
package/README.md CHANGED
@@ -57,6 +57,29 @@ AI_MODEL=gemini-2.5-flash
57
57
  GOOGLE_API_KEY=your-key-here
58
58
  ```
59
59
 
60
+ ## Upgrading
61
+
62
+ When a new version of the template is released, run this from inside your cockpit project directory:
63
+
64
+ ```bash
65
+ npx create-interview-cockpit upgrade
66
+ ```
67
+
68
+ The CLI will:
69
+ - Compare your installed version (`cockpit.json`) against the latest
70
+ - Overwrite `client/` and `server/` with the updated template files
71
+ - Merge any new scripts and dependencies into your `package.json` (your pinned versions are preserved)
72
+ - **Leave untouched:** `.env`, `data/` (your topics, questions, and context files), and `node_modules/`
73
+
74
+ After upgrading, reinstall dependencies and restart:
75
+
76
+ ```bash
77
+ npm install
78
+ cd client && npm install && cd ..
79
+ cd server && npm install && cd ..
80
+ npm run dev
81
+ ```
82
+
60
83
  ## License
61
84
 
62
85
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-interview-cockpit",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Scaffold a personal AI-powered interview prep cockpit",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,14 +7,19 @@
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",
21
+ "vizcraft": "^1.17.0",
22
+ "yaml": "^2.8.3",
18
23
  "zustand": "^5.0.0"
19
24
  },
20
25
  "devDependencies": {
@@ -1747,6 +1752,12 @@
1747
1752
  "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
1748
1753
  "license": "MIT"
1749
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
+ },
1750
1761
  "node_modules/@types/react": {
1751
1762
  "version": "19.2.14",
1752
1763
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
@@ -4913,6 +4924,16 @@
4913
4924
  "node": ">=0.10.0"
4914
4925
  }
4915
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
+ },
4916
4937
  "node_modules/react-syntax-highlighter": {
4917
4938
  "version": "15.6.6",
4918
4939
  "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz",
@@ -5894,6 +5915,12 @@
5894
5915
  "url": "https://github.com/sponsors/jonschlinkert"
5895
5916
  }
5896
5917
  },
5918
+ "node_modules/vizcraft": {
5919
+ "version": "1.17.0",
5920
+ "resolved": "https://registry.npmjs.org/vizcraft/-/vizcraft-1.17.0.tgz",
5921
+ "integrity": "sha512-BCXjnWWEfMHWnAYD6DQvVXJFz0Q/Da1ukULXdMKIrZiNfblVVyYO/b/MWPO54cRzJESJZle4U52G1v2U/zRqQg==",
5922
+ "license": "MIT"
5923
+ },
5897
5924
  "node_modules/vscode-jsonrpc": {
5898
5925
  "version": "8.2.0",
5899
5926
  "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
@@ -5959,6 +5986,21 @@
5959
5986
  "dev": true,
5960
5987
  "license": "ISC"
5961
5988
  },
5989
+ "node_modules/yaml": {
5990
+ "version": "2.8.3",
5991
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
5992
+ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
5993
+ "license": "ISC",
5994
+ "bin": {
5995
+ "yaml": "bin.mjs"
5996
+ },
5997
+ "engines": {
5998
+ "node": ">= 14.6"
5999
+ },
6000
+ "funding": {
6001
+ "url": "https://github.com/sponsors/eemeli"
6002
+ }
6003
+ },
5962
6004
  "node_modules/zod": {
5963
6005
  "version": "4.3.6",
5964
6006
  "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
@@ -9,14 +9,19 @@
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
+ "vizcraft": "^1.17.0",
24
+ "yaml": "^2.8.3",
20
25
  "zustand": "^5.0.0"
21
26
  },
22
27
  "devDependencies": {
@@ -4,12 +4,17 @@ 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 { Code, Plane, PanelLeftClose, PanelLeft } from "lucide-react";
7
+ import DocRefModal from "./components/DocRefModal";
8
+ import AiSettingsModal from "./components/AiSettingsModal";
9
+ import CodeRunnerModal from "./components/CodeRunnerModal";
10
+ import { Code, Plane, PanelLeftClose, PanelLeft, Settings } from "lucide-react";
8
11
 
9
12
  export default function App() {
10
13
  const {
11
14
  fetchTopics,
12
15
  fetchWorkspaces,
16
+ fetchAiSettings,
17
+ fetchWorkspaceFiles,
13
18
  fetchQuestions,
14
19
  selectQuestion,
15
20
  currentQuestion,
@@ -19,12 +24,21 @@ export default function App() {
19
24
  toggleSidebar,
20
25
  viewingFile,
21
26
  closeFileViewer,
27
+ viewingDoc,
28
+ closeDocViewer,
29
+ showSettings,
30
+ openSettings,
31
+ closeSettings,
32
+ showCodeRunner,
33
+ closeCodeRunner,
22
34
  } = useStore();
23
35
 
24
36
  useEffect(() => {
25
37
  const init = async () => {
26
38
  await fetchWorkspaces();
27
39
  await fetchTopics();
40
+ fetchAiSettings();
41
+ fetchWorkspaceFiles();
28
42
  // Restore last-viewed question after page refresh
29
43
  const topicId = sessionStorage.getItem("lastTopicId");
30
44
  const questionId = sessionStorage.getItem("lastQuestionId");
@@ -71,17 +85,26 @@ export default function App() {
71
85
  {currentQuestion ? currentQuestion.title : "Interview Cockpit"}
72
86
  </span>
73
87
  </div>
74
- <button
75
- onClick={toggleCodePanel}
76
- className={`p-1.5 rounded transition-colors ${
77
- showCodePanel
78
- ? "bg-cyan-500/20 text-cyan-400"
79
- : "text-slate-500 hover:text-slate-300"
80
- }`}
81
- title="Toggle code context"
82
- >
83
- <Code className="w-4 h-4" />
84
- </button>
88
+ <div className="flex items-center gap-1">
89
+ <button
90
+ onClick={openSettings}
91
+ className="p-1.5 rounded transition-colors text-slate-500 hover:text-slate-300 hover:bg-slate-800"
92
+ title="AI Settings"
93
+ >
94
+ <Settings className="w-4 h-4" />
95
+ </button>
96
+ <button
97
+ onClick={toggleCodePanel}
98
+ className={`p-1.5 rounded transition-colors ${
99
+ showCodePanel
100
+ ? "bg-cyan-500/20 text-cyan-400"
101
+ : "text-slate-500 hover:text-slate-300"
102
+ }`}
103
+ title="Toggle code context"
104
+ >
105
+ <Code className="w-4 h-4" />
106
+ </button>
107
+ </div>
85
108
  </header>
86
109
 
87
110
  {/* Content area */}
@@ -124,6 +147,16 @@ export default function App() {
124
147
  {viewingFile && (
125
148
  <FileViewerModal filePath={viewingFile} onClose={closeFileViewer} />
126
149
  )}
150
+ {viewingDoc && (
151
+ <DocRefModal
152
+ fileId={viewingDoc.fileId}
153
+ quote={viewingDoc.quote}
154
+ fileName={viewingDoc.fileName}
155
+ onClose={closeDocViewer}
156
+ />
157
+ )}
158
+ {showSettings && <AiSettingsModal />}
159
+ {showCodeRunner && <CodeRunnerModal />}
127
160
  </div>
128
161
  );
129
162
  }
@@ -2,6 +2,52 @@ import type { Topic, Question, ContextFile, WorkspacesRegistry } from "./types";
2
2
 
3
3
  const BASE = "/api";
4
4
 
5
+ export interface PromptGroup {
6
+ /** Display name shown in the settings UI. */
7
+ label: string;
8
+ /** Optional helper text shown below the section header. */
9
+ description?: string;
10
+ /** Which option key is active by default in new chats. */
11
+ default: string;
12
+ /** Map of option key -> prompt text appended to the user message. */
13
+ options: Record<string, string>;
14
+ }
15
+
16
+ export interface AiSettings {
17
+ systemPrompt: string;
18
+ responseProfiles: Record<
19
+ string,
20
+ { maxOutputTokens: number; maxSteps: number }
21
+ >;
22
+ vizGuide: string;
23
+ /** All user-selectable prompt groups. Add new entries here to extend the UI. */
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;
33
+ }
34
+
35
+ export async function fetchAiSettings(): Promise<AiSettings> {
36
+ const res = await fetch(`${BASE}/settings`);
37
+ return res.json();
38
+ }
39
+
40
+ export async function saveAiSettings(
41
+ patch: Partial<AiSettings>,
42
+ ): Promise<AiSettings> {
43
+ const res = await fetch(`${BASE}/settings`, {
44
+ method: "PATCH",
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify(patch),
47
+ });
48
+ return res.json();
49
+ }
50
+
5
51
  export async function fetchTopics(): Promise<Topic[]> {
6
52
  const res = await fetch(`${BASE}/topics`);
7
53
  return res.json();
@@ -44,6 +90,10 @@ export async function uploadTopicFiles(
44
90
  method: "POST",
45
91
  body: form,
46
92
  });
93
+ if (!res.ok) {
94
+ const err = await res.json().catch(() => ({ error: "Upload failed" }));
95
+ throw new Error(err.error ?? "Upload failed");
96
+ }
47
97
  return res.json();
48
98
  }
49
99
 
@@ -56,6 +106,35 @@ export async function deleteTopicFile(
56
106
  });
57
107
  }
58
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
+
59
138
  // --- Questions ---
60
139
 
61
140
  export async function fetchQuestions(topicId: string): Promise<Question[]> {
@@ -81,6 +160,7 @@ export async function createQuestion(
81
160
 
82
161
  export async function fetchQuestion(id: string): Promise<Question> {
83
162
  const res = await fetch(`${BASE}/questions/${id}`);
163
+ if (!res.ok) throw new Error(`Question not found: ${id}`);
84
164
  return res.json();
85
165
  }
86
166
 
@@ -112,6 +192,10 @@ export async function uploadQuestionFiles(
112
192
  method: "POST",
113
193
  body: form,
114
194
  });
195
+ if (!res.ok) {
196
+ const err = await res.json().catch(() => ({ error: "Upload failed" }));
197
+ throw new Error(err.error ?? "Upload failed");
198
+ }
115
199
  return res.json();
116
200
  }
117
201
 
@@ -124,6 +208,96 @@ export async function deleteQuestionFile(
124
208
  });
125
209
  }
126
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
+
127
301
  // --- Code Context ---
128
302
 
129
303
  export async function fetchCodeContextTree(): Promise<string[]> {