create-interview-cockpit 0.1.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 (39) hide show
  1. package/README.md +62 -0
  2. package/index.js +302 -0
  3. package/package.json +44 -0
  4. package/template/.env.example +14 -0
  5. package/template/client/index.html +12 -0
  6. package/template/client/package-lock.json +6012 -0
  7. package/template/client/package.json +34 -0
  8. package/template/client/postcss.config.cjs +6 -0
  9. package/template/client/src/App.tsx +120 -0
  10. package/template/client/src/api.ts +132 -0
  11. package/template/client/src/components/AnnotationDialog.tsx +307 -0
  12. package/template/client/src/components/ChatMessage.tsx +89 -0
  13. package/template/client/src/components/ChatView.tsx +763 -0
  14. package/template/client/src/components/CodeContextPanel.tsx +470 -0
  15. package/template/client/src/components/FileAttachments.tsx +107 -0
  16. package/template/client/src/components/FileViewerModal.tsx +470 -0
  17. package/template/client/src/components/MarkdownRenderer.tsx +333 -0
  18. package/template/client/src/components/MermaidDiagram.tsx +157 -0
  19. package/template/client/src/components/Sidebar.tsx +419 -0
  20. package/template/client/src/components/TextAnnotator.tsx +476 -0
  21. package/template/client/src/index.css +61 -0
  22. package/template/client/src/main.tsx +10 -0
  23. package/template/client/src/store.ts +321 -0
  24. package/template/client/src/types.ts +65 -0
  25. package/template/client/src/vite-env.d.ts +1 -0
  26. package/template/client/tailwind.config.cjs +8 -0
  27. package/template/client/tsconfig.json +16 -0
  28. package/template/client/tsconfig.tsbuildinfo +1 -0
  29. package/template/client/vite.config.ts +12 -0
  30. package/template/cockpit.json +3 -0
  31. package/template/data/context-files/.gitkeep +0 -0
  32. package/template/data/questions/.gitkeep +0 -0
  33. package/template/data/topics.json +1 -0
  34. package/template/package.json +14 -0
  35. package/template/server/package-lock.json +2266 -0
  36. package/template/server/package.json +31 -0
  37. package/template/server/src/index.ts +758 -0
  38. package/template/server/src/storage.ts +303 -0
  39. package/template/server/tsconfig.json +14 -0
@@ -0,0 +1,321 @@
1
+ import { create } from "zustand";
2
+ import type { Topic, Question, CodeSnippet } from "./types";
3
+ import * as api from "./api";
4
+
5
+ interface Store {
6
+ topics: Topic[];
7
+ questionsByTopic: Record<string, Question[]>;
8
+ selectedTopicId: string | null;
9
+ selectedQuestionId: string | null;
10
+ currentQuestion: Question | null;
11
+ expandedTopics: string[];
12
+ availableFiles: string[];
13
+ showCodePanel: boolean;
14
+ showSidebar: boolean;
15
+ viewingFile: string | null;
16
+
17
+ fetchTopics: () => Promise<void>;
18
+ fetchQuestions: (topicId: string) => Promise<void>;
19
+ addTopic: (name: string) => Promise<void>;
20
+ removeTopic: (id: string) => Promise<void>;
21
+ renameTopic: (id: string, name: string) => Promise<void>;
22
+ addQuestion: (topicId: string, title: string) => Promise<void>;
23
+ addChildQuestion: (
24
+ topicId: string,
25
+ parentQuestionId: string,
26
+ title: string,
27
+ ) => Promise<void>;
28
+ removeQuestion: (questionId: string, topicId: string) => Promise<void>;
29
+ renameQuestion: (
30
+ questionId: string,
31
+ topicId: string,
32
+ title: string,
33
+ ) => Promise<void>;
34
+ selectQuestion: (topicId: string, questionId: string) => Promise<void>;
35
+ toggleTopic: (topicId: string) => void;
36
+ toggleCodePanel: () => void;
37
+ toggleSidebar: () => void;
38
+ fetchAvailableFiles: () => Promise<void>;
39
+ updateCodeContext: (questionId: string, files: string[]) => Promise<void>;
40
+ refreshCurrentQuestion: () => Promise<void>;
41
+ uploadTopicFiles: (
42
+ topicId: string,
43
+ files: FileList | File[],
44
+ ) => Promise<void>;
45
+ removeTopicFile: (topicId: string, fileId: string) => Promise<void>;
46
+ uploadQuestionFiles: (
47
+ questionId: string,
48
+ files: FileList | File[],
49
+ ) => Promise<void>;
50
+ removeQuestionFile: (questionId: string, fileId: string) => Promise<void>;
51
+ clearMessages: (questionId: string) => Promise<void>;
52
+ updateQuestionSystemContext: (
53
+ questionId: string,
54
+ systemContext: string,
55
+ ) => Promise<void>;
56
+ openFileViewer: (path: string) => void;
57
+ closeFileViewer: () => void;
58
+ codeSnippets: CodeSnippet[];
59
+ addSnippet: (snippet: CodeSnippet) => void;
60
+ removeSnippet: (id: string) => void;
61
+ clearSnippets: () => void;
62
+ }
63
+
64
+ export const useStore = create<Store>((set, get) => ({
65
+ topics: [],
66
+ questionsByTopic: {},
67
+ selectedTopicId: null,
68
+ selectedQuestionId: null,
69
+ currentQuestion: null,
70
+ expandedTopics: [],
71
+ availableFiles: [],
72
+ showCodePanel: false,
73
+ showSidebar: true,
74
+ viewingFile: null,
75
+ codeSnippets: [],
76
+
77
+ fetchTopics: async () => {
78
+ const topics = await api.fetchTopics();
79
+ set({ topics });
80
+ },
81
+
82
+ fetchQuestions: async (topicId) => {
83
+ const questions = await api.fetchQuestions(topicId);
84
+ set((s) => ({
85
+ questionsByTopic: { ...s.questionsByTopic, [topicId]: questions },
86
+ }));
87
+ },
88
+
89
+ addTopic: async (name) => {
90
+ const topic = await api.createTopic(name);
91
+ set((s) => ({
92
+ topics: [...s.topics, topic],
93
+ questionsByTopic: { ...s.questionsByTopic, [topic.id]: [] },
94
+ expandedTopics: [...s.expandedTopics, topic.id],
95
+ }));
96
+ },
97
+
98
+ removeTopic: async (id) => {
99
+ await api.deleteTopic(id);
100
+ set((s) => ({
101
+ topics: s.topics.filter((t) => t.id !== id),
102
+ selectedTopicId: s.selectedTopicId === id ? null : s.selectedTopicId,
103
+ selectedQuestionId:
104
+ s.selectedTopicId === id ? null : s.selectedQuestionId,
105
+ currentQuestion: s.selectedTopicId === id ? null : s.currentQuestion,
106
+ }));
107
+ },
108
+
109
+ renameTopic: async (id, name) => {
110
+ const updated = await api.updateTopic(id, { name });
111
+ set((s) => ({
112
+ topics: s.topics.map((t) =>
113
+ t.id === id ? { ...t, name: updated.name } : t,
114
+ ),
115
+ }));
116
+ },
117
+
118
+ addQuestion: async (topicId, title) => {
119
+ const question = await api.createQuestion(topicId, title);
120
+ set((s) => ({
121
+ questionsByTopic: {
122
+ ...s.questionsByTopic,
123
+ [topicId]: [...(s.questionsByTopic[topicId] || []), question],
124
+ },
125
+ }));
126
+ },
127
+
128
+ addChildQuestion: async (topicId, parentQuestionId, title) => {
129
+ const question = await api.createQuestion(topicId, title, parentQuestionId);
130
+ set((s) => ({
131
+ questionsByTopic: {
132
+ ...s.questionsByTopic,
133
+ [topicId]: [...(s.questionsByTopic[topicId] || []), question],
134
+ },
135
+ }));
136
+ },
137
+
138
+ removeQuestion: async (questionId, topicId) => {
139
+ await api.deleteQuestion(questionId);
140
+ set((s) => ({
141
+ questionsByTopic: {
142
+ ...s.questionsByTopic,
143
+ [topicId]: (s.questionsByTopic[topicId] || []).filter(
144
+ (q) => q.id !== questionId,
145
+ ),
146
+ },
147
+ selectedQuestionId:
148
+ s.selectedQuestionId === questionId ? null : s.selectedQuestionId,
149
+ currentQuestion:
150
+ s.selectedQuestionId === questionId ? null : s.currentQuestion,
151
+ }));
152
+ },
153
+
154
+ renameQuestion: async (questionId, topicId, title) => {
155
+ const updated = await api.updateQuestion(questionId, { title });
156
+ set((s) => ({
157
+ questionsByTopic: {
158
+ ...s.questionsByTopic,
159
+ [topicId]: (s.questionsByTopic[topicId] || []).map((q) =>
160
+ q.id === questionId ? { ...q, title: updated.title } : q,
161
+ ),
162
+ },
163
+ currentQuestion:
164
+ s.currentQuestion?.id === questionId
165
+ ? { ...s.currentQuestion, title: updated.title }
166
+ : s.currentQuestion,
167
+ }));
168
+ },
169
+
170
+ selectQuestion: async (topicId, questionId) => {
171
+ const question = await api.fetchQuestion(questionId);
172
+ set({
173
+ selectedTopicId: topicId,
174
+ selectedQuestionId: questionId,
175
+ currentQuestion: question,
176
+ codeSnippets: [],
177
+ });
178
+ },
179
+
180
+ addSnippet: (snippet) =>
181
+ set((s) => ({ codeSnippets: [...s.codeSnippets, snippet] })),
182
+ removeSnippet: (id) =>
183
+ set((s) => ({ codeSnippets: s.codeSnippets.filter((s) => s.id !== id) })),
184
+ clearSnippets: () => set({ codeSnippets: [] }),
185
+
186
+ toggleTopic: (topicId) => {
187
+ set((s) => ({
188
+ expandedTopics: s.expandedTopics.includes(topicId)
189
+ ? s.expandedTopics.filter((id) => id !== topicId)
190
+ : [...s.expandedTopics, topicId],
191
+ }));
192
+ const { questionsByTopic, fetchQuestions } = get();
193
+ if (!questionsByTopic[topicId]) {
194
+ fetchQuestions(topicId);
195
+ }
196
+ },
197
+
198
+ toggleSidebar: () => {
199
+ set((s) => ({ showSidebar: !s.showSidebar }));
200
+ },
201
+
202
+ toggleCodePanel: () => {
203
+ set((s) => ({ showCodePanel: !s.showCodePanel }));
204
+ const { availableFiles, fetchAvailableFiles } = get();
205
+ if (availableFiles.length === 0) {
206
+ fetchAvailableFiles();
207
+ }
208
+ },
209
+
210
+ fetchAvailableFiles: async () => {
211
+ const files = await api.fetchCodeContextTree();
212
+ set({ availableFiles: files });
213
+ },
214
+
215
+ updateCodeContext: async (questionId, files) => {
216
+ await api.updateQuestion(questionId, { codeContextFiles: files });
217
+ set((s) => ({
218
+ currentQuestion: s.currentQuestion
219
+ ? { ...s.currentQuestion, codeContextFiles: files }
220
+ : null,
221
+ }));
222
+ },
223
+
224
+ refreshCurrentQuestion: async () => {
225
+ const { selectedQuestionId } = get();
226
+ if (selectedQuestionId) {
227
+ const question = await api.fetchQuestion(selectedQuestionId);
228
+ set({ currentQuestion: question });
229
+ }
230
+ },
231
+
232
+ uploadTopicFiles: async (topicId, files) => {
233
+ const uploaded = await api.uploadTopicFiles(topicId, files);
234
+ set((s) => ({
235
+ topics: s.topics.map((t) =>
236
+ t.id === topicId
237
+ ? { ...t, contextFiles: [...(t.contextFiles || []), ...uploaded] }
238
+ : t,
239
+ ),
240
+ }));
241
+ },
242
+
243
+ removeTopicFile: async (topicId, fileId) => {
244
+ await api.deleteTopicFile(topicId, fileId);
245
+ set((s) => ({
246
+ topics: s.topics.map((t) =>
247
+ t.id === topicId
248
+ ? {
249
+ ...t,
250
+ contextFiles: (t.contextFiles || []).filter(
251
+ (f) => f.id !== fileId,
252
+ ),
253
+ }
254
+ : t,
255
+ ),
256
+ }));
257
+ },
258
+
259
+ uploadQuestionFiles: async (questionId, files) => {
260
+ const uploaded = await api.uploadQuestionFiles(questionId, files);
261
+ set((s) => ({
262
+ currentQuestion:
263
+ s.currentQuestion?.id === questionId
264
+ ? {
265
+ ...s.currentQuestion,
266
+ contextFiles: [
267
+ ...(s.currentQuestion.contextFiles || []),
268
+ ...uploaded,
269
+ ],
270
+ }
271
+ : s.currentQuestion,
272
+ }));
273
+ },
274
+
275
+ removeQuestionFile: async (questionId, fileId) => {
276
+ await api.deleteQuestionFile(questionId, fileId);
277
+ set((s) => ({
278
+ currentQuestion:
279
+ s.currentQuestion?.id === questionId
280
+ ? {
281
+ ...s.currentQuestion,
282
+ contextFiles: (s.currentQuestion.contextFiles || []).filter(
283
+ (f) => f.id !== fileId,
284
+ ),
285
+ }
286
+ : s.currentQuestion,
287
+ }));
288
+ },
289
+
290
+ clearMessages: async (questionId) => {
291
+ await api.updateQuestion(questionId, { messages: [] });
292
+ set((s) => ({
293
+ currentQuestion:
294
+ s.currentQuestion?.id === questionId
295
+ ? { ...s.currentQuestion, messages: [] }
296
+ : s.currentQuestion,
297
+ }));
298
+ },
299
+
300
+ updateQuestionSystemContext: async (questionId, systemContext) => {
301
+ const updated = await api.updateQuestion(questionId, { systemContext });
302
+ set((s) => ({
303
+ questionsByTopic: {
304
+ ...s.questionsByTopic,
305
+ [updated.topicId]: (s.questionsByTopic[updated.topicId] || []).map(
306
+ (question) =>
307
+ question.id === questionId
308
+ ? { ...question, systemContext: updated.systemContext }
309
+ : question,
310
+ ),
311
+ },
312
+ currentQuestion:
313
+ s.currentQuestion?.id === questionId
314
+ ? { ...s.currentQuestion, systemContext: updated.systemContext }
315
+ : s.currentQuestion,
316
+ }));
317
+ },
318
+
319
+ openFileViewer: (path) => set({ viewingFile: path }),
320
+ closeFileViewer: () => set({ viewingFile: null }),
321
+ }));
@@ -0,0 +1,65 @@
1
+ export interface ContextFile {
2
+ id: string;
3
+ name: string;
4
+ originalName: string;
5
+ createdAt: string;
6
+ }
7
+
8
+ export interface Topic {
9
+ id: string;
10
+ name: string;
11
+ contextFiles: ContextFile[];
12
+ createdAt: string;
13
+ }
14
+
15
+ export interface Message {
16
+ id: string;
17
+ role: "user" | "assistant";
18
+ content: string;
19
+ createdAt?: string;
20
+ }
21
+
22
+ export interface AnnotationFollowUp {
23
+ id: string;
24
+ prompt: string;
25
+ response: string;
26
+ createdAt: string;
27
+ }
28
+
29
+ export interface Annotation {
30
+ id: string;
31
+ messageId: string;
32
+ selectedText: string;
33
+ prompt: string;
34
+ response: string;
35
+ followUps?: AnnotationFollowUp[];
36
+ createdAt: string;
37
+ }
38
+
39
+ export interface CodeSnippet {
40
+ id: string;
41
+ filePath: string;
42
+ fileName: string;
43
+ startLine: number;
44
+ endLine: number;
45
+ code: string;
46
+ }
47
+
48
+ export interface ReadingBookmark {
49
+ messageId: string;
50
+ blockIndex: number;
51
+ }
52
+
53
+ export interface Question {
54
+ id: string;
55
+ topicId: string;
56
+ parentQuestionId?: string;
57
+ title: string;
58
+ systemContext: string;
59
+ codeContextFiles: string[];
60
+ contextFiles: ContextFile[];
61
+ messages: Message[];
62
+ annotations?: Annotation[];
63
+ readingBookmark?: ReadingBookmark;
64
+ createdAt: string;
65
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,8 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4
+ theme: {
5
+ extend: {},
6
+ },
7
+ plugins: [require('@tailwindcss/typography')],
8
+ };
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "noEmit": true
14
+ },
15
+ "include": ["src"]
16
+ }
@@ -0,0 +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"}
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ proxy: {
9
+ "/api": "http://localhost:3001",
10
+ },
11
+ },
12
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "version": "0.1.0"
3
+ }
File without changes
File without changes
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "interview-cockpit",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "concurrently -n server,client -c blue,green \"npm run dev --prefix server\" \"npm run dev --prefix client\"",
7
+ "build": "npm run build --prefix client",
8
+ "start": "npm run start --prefix server",
9
+ "sync:template": "node scripts/sync-template.js"
10
+ },
11
+ "devDependencies": {
12
+ "concurrently": "^9.1.0"
13
+ }
14
+ }