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.
- package/README.md +62 -0
- package/index.js +302 -0
- package/package.json +44 -0
- package/template/.env.example +14 -0
- package/template/client/index.html +12 -0
- package/template/client/package-lock.json +6012 -0
- package/template/client/package.json +34 -0
- package/template/client/postcss.config.cjs +6 -0
- package/template/client/src/App.tsx +120 -0
- package/template/client/src/api.ts +132 -0
- package/template/client/src/components/AnnotationDialog.tsx +307 -0
- package/template/client/src/components/ChatMessage.tsx +89 -0
- package/template/client/src/components/ChatView.tsx +763 -0
- package/template/client/src/components/CodeContextPanel.tsx +470 -0
- package/template/client/src/components/FileAttachments.tsx +107 -0
- package/template/client/src/components/FileViewerModal.tsx +470 -0
- package/template/client/src/components/MarkdownRenderer.tsx +333 -0
- package/template/client/src/components/MermaidDiagram.tsx +157 -0
- package/template/client/src/components/Sidebar.tsx +419 -0
- package/template/client/src/components/TextAnnotator.tsx +476 -0
- package/template/client/src/index.css +61 -0
- package/template/client/src/main.tsx +10 -0
- package/template/client/src/store.ts +321 -0
- package/template/client/src/types.ts +65 -0
- package/template/client/src/vite-env.d.ts +1 -0
- package/template/client/tailwind.config.cjs +8 -0
- package/template/client/tsconfig.json +16 -0
- package/template/client/tsconfig.tsbuildinfo +1 -0
- package/template/client/vite.config.ts +12 -0
- package/template/cockpit.json +3 -0
- package/template/data/context-files/.gitkeep +0 -0
- package/template/data/questions/.gitkeep +0 -0
- package/template/data/topics.json +1 -0
- package/template/package.json +14 -0
- package/template/server/package-lock.json +2266 -0
- package/template/server/package.json +31 -0
- package/template/server/src/index.ts +758 -0
- package/template/server/src/storage.ts +303 -0
- package/template/server/tsconfig.json +14 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const DATA_DIR = path.join(__dirname, "../../data");
|
|
7
|
+
const TOPICS_FILE = path.join(DATA_DIR, "topics.json");
|
|
8
|
+
const QUESTIONS_DIR = path.join(DATA_DIR, "questions");
|
|
9
|
+
const CONTEXT_FILES_DIR = path.join(DATA_DIR, "context-files");
|
|
10
|
+
|
|
11
|
+
async function ensureDirs() {
|
|
12
|
+
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
13
|
+
await fs.mkdir(QUESTIONS_DIR, { recursive: true });
|
|
14
|
+
await fs.mkdir(CONTEXT_FILES_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Topic {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
contextFiles: ContextFile[];
|
|
21
|
+
createdAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ContextFile {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
originalName: string;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Message {
|
|
32
|
+
id: string;
|
|
33
|
+
role: string;
|
|
34
|
+
content: string;
|
|
35
|
+
createdAt?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AnnotationFollowUp {
|
|
39
|
+
id: string;
|
|
40
|
+
prompt: string;
|
|
41
|
+
response: string;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Annotation {
|
|
46
|
+
id: string;
|
|
47
|
+
messageId: string;
|
|
48
|
+
selectedText: string;
|
|
49
|
+
prompt: string;
|
|
50
|
+
response: string;
|
|
51
|
+
followUps?: AnnotationFollowUp[];
|
|
52
|
+
createdAt: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ReadingBookmark {
|
|
56
|
+
messageId: string;
|
|
57
|
+
blockIndex: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Question {
|
|
61
|
+
id: string;
|
|
62
|
+
topicId: string;
|
|
63
|
+
parentQuestionId?: string;
|
|
64
|
+
title: string;
|
|
65
|
+
systemContext: string;
|
|
66
|
+
codeContextFiles: string[];
|
|
67
|
+
contextFiles: ContextFile[];
|
|
68
|
+
messages: Message[];
|
|
69
|
+
annotations?: Annotation[];
|
|
70
|
+
readingBookmark?: ReadingBookmark;
|
|
71
|
+
createdAt: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Topics ---
|
|
75
|
+
|
|
76
|
+
export async function getTopics(): Promise<Topic[]> {
|
|
77
|
+
await ensureDirs();
|
|
78
|
+
try {
|
|
79
|
+
const data = await fs.readFile(TOPICS_FILE, "utf-8");
|
|
80
|
+
const topics: Topic[] = JSON.parse(data);
|
|
81
|
+
// Backfill missing fields from older data
|
|
82
|
+
for (const t of topics) {
|
|
83
|
+
if (!t.contextFiles) t.contextFiles = [];
|
|
84
|
+
}
|
|
85
|
+
return topics;
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function saveTopic(topic: Topic): Promise<Topic> {
|
|
92
|
+
const topics = await getTopics();
|
|
93
|
+
const idx = topics.findIndex((t) => t.id === topic.id);
|
|
94
|
+
if (idx !== -1) {
|
|
95
|
+
topics[idx] = topic;
|
|
96
|
+
} else {
|
|
97
|
+
topics.push(topic);
|
|
98
|
+
}
|
|
99
|
+
await fs.writeFile(TOPICS_FILE, JSON.stringify(topics, null, 2));
|
|
100
|
+
return topic;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function deleteTopic(id: string): Promise<void> {
|
|
104
|
+
const topics = await getTopics();
|
|
105
|
+
const topic = topics.find((t) => t.id === id);
|
|
106
|
+
// Clean up context files on disk
|
|
107
|
+
if (topic?.contextFiles) {
|
|
108
|
+
for (const cf of topic.contextFiles) {
|
|
109
|
+
try {
|
|
110
|
+
await fs.unlink(path.join(CONTEXT_FILES_DIR, cf.id));
|
|
111
|
+
} catch {
|
|
112
|
+
/* already gone */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const remaining = topics.filter((t) => t.id !== id);
|
|
117
|
+
await fs.writeFile(TOPICS_FILE, JSON.stringify(remaining, null, 2));
|
|
118
|
+
const questions = await getQuestionsByTopic(id);
|
|
119
|
+
for (const q of questions) {
|
|
120
|
+
await deleteQuestion(q.id);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function updateTopic(
|
|
125
|
+
id: string,
|
|
126
|
+
patch: Partial<Topic>,
|
|
127
|
+
): Promise<Topic | null> {
|
|
128
|
+
const topics = await getTopics();
|
|
129
|
+
const idx = topics.findIndex((t) => t.id === id);
|
|
130
|
+
if (idx === -1) return null;
|
|
131
|
+
Object.assign(topics[idx], patch);
|
|
132
|
+
await fs.writeFile(TOPICS_FILE, JSON.stringify(topics, null, 2));
|
|
133
|
+
return topics[idx];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- Context Files ---
|
|
137
|
+
|
|
138
|
+
export function getContextFilesDir(): string {
|
|
139
|
+
return CONTEXT_FILES_DIR;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function saveContextFile(
|
|
143
|
+
topicId: string,
|
|
144
|
+
fileId: string,
|
|
145
|
+
originalName: string,
|
|
146
|
+
buffer: Buffer,
|
|
147
|
+
): Promise<ContextFile> {
|
|
148
|
+
await ensureDirs();
|
|
149
|
+
await fs.writeFile(path.join(CONTEXT_FILES_DIR, fileId), buffer);
|
|
150
|
+
const cf: ContextFile = {
|
|
151
|
+
id: fileId,
|
|
152
|
+
name: originalName,
|
|
153
|
+
originalName,
|
|
154
|
+
createdAt: new Date().toISOString(),
|
|
155
|
+
};
|
|
156
|
+
const topics = await getTopics();
|
|
157
|
+
const topic = topics.find((t) => t.id === topicId);
|
|
158
|
+
if (topic) {
|
|
159
|
+
if (!topic.contextFiles) topic.contextFiles = [];
|
|
160
|
+
topic.contextFiles.push(cf);
|
|
161
|
+
await fs.writeFile(TOPICS_FILE, JSON.stringify(topics, null, 2));
|
|
162
|
+
}
|
|
163
|
+
return cf;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function deleteContextFile(
|
|
167
|
+
topicId: string,
|
|
168
|
+
fileId: string,
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
try {
|
|
171
|
+
await fs.unlink(path.join(CONTEXT_FILES_DIR, fileId));
|
|
172
|
+
} catch {
|
|
173
|
+
/* already gone */
|
|
174
|
+
}
|
|
175
|
+
const topics = await getTopics();
|
|
176
|
+
const topic = topics.find((t) => t.id === topicId);
|
|
177
|
+
if (topic) {
|
|
178
|
+
topic.contextFiles = (topic.contextFiles || []).filter(
|
|
179
|
+
(f) => f.id !== fileId,
|
|
180
|
+
);
|
|
181
|
+
await fs.writeFile(TOPICS_FILE, JSON.stringify(topics, null, 2));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function readContextFileContent(fileId: string): Promise<string> {
|
|
186
|
+
return fs.readFile(path.join(CONTEXT_FILES_DIR, fileId), "utf-8");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- Questions ---
|
|
190
|
+
|
|
191
|
+
export async function getQuestion(id: string): Promise<Question | null> {
|
|
192
|
+
await ensureDirs();
|
|
193
|
+
try {
|
|
194
|
+
const data = await fs.readFile(
|
|
195
|
+
path.join(QUESTIONS_DIR, `${id}.json`),
|
|
196
|
+
"utf-8",
|
|
197
|
+
);
|
|
198
|
+
const q: Question = JSON.parse(data);
|
|
199
|
+
if (!q.contextFiles) q.contextFiles = [];
|
|
200
|
+
if (!q.annotations) q.annotations = [];
|
|
201
|
+
return q;
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function getQuestionsByTopic(
|
|
208
|
+
topicId: string,
|
|
209
|
+
): Promise<Question[]> {
|
|
210
|
+
await ensureDirs();
|
|
211
|
+
try {
|
|
212
|
+
const files = await fs.readdir(QUESTIONS_DIR);
|
|
213
|
+
const questions: Question[] = [];
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
if (!file.endsWith(".json")) continue;
|
|
216
|
+
const data = await fs.readFile(path.join(QUESTIONS_DIR, file), "utf-8");
|
|
217
|
+
const q: Question = JSON.parse(data);
|
|
218
|
+
if (q.topicId === topicId) {
|
|
219
|
+
questions.push(q);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return questions.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
223
|
+
} catch {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function saveQuestion(question: Question): Promise<Question> {
|
|
229
|
+
await ensureDirs();
|
|
230
|
+
await fs.writeFile(
|
|
231
|
+
path.join(QUESTIONS_DIR, `${question.id}.json`),
|
|
232
|
+
JSON.stringify(question, null, 2),
|
|
233
|
+
);
|
|
234
|
+
return question;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function deleteQuestion(id: string): Promise<void> {
|
|
238
|
+
// Clean up question-level context files
|
|
239
|
+
const q = await getQuestion(id);
|
|
240
|
+
if (q?.contextFiles) {
|
|
241
|
+
for (const cf of q.contextFiles) {
|
|
242
|
+
try {
|
|
243
|
+
await fs.unlink(path.join(CONTEXT_FILES_DIR, cf.id));
|
|
244
|
+
} catch {
|
|
245
|
+
/* ok */
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
await fs.unlink(path.join(QUESTIONS_DIR, `${id}.json`));
|
|
251
|
+
} catch {
|
|
252
|
+
// ignore if already deleted
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function saveQuestionContextFile(
|
|
257
|
+
questionId: string,
|
|
258
|
+
fileId: string,
|
|
259
|
+
originalName: string,
|
|
260
|
+
buffer: Buffer,
|
|
261
|
+
): Promise<ContextFile> {
|
|
262
|
+
await ensureDirs();
|
|
263
|
+
await fs.writeFile(path.join(CONTEXT_FILES_DIR, fileId), buffer);
|
|
264
|
+
const cf: ContextFile = {
|
|
265
|
+
id: fileId,
|
|
266
|
+
name: originalName,
|
|
267
|
+
originalName,
|
|
268
|
+
createdAt: new Date().toISOString(),
|
|
269
|
+
};
|
|
270
|
+
const q = await getQuestion(questionId);
|
|
271
|
+
if (q) {
|
|
272
|
+
if (!q.contextFiles) q.contextFiles = [];
|
|
273
|
+
q.contextFiles.push(cf);
|
|
274
|
+
await saveQuestion(q);
|
|
275
|
+
}
|
|
276
|
+
return cf;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function deleteQuestionContextFile(
|
|
280
|
+
questionId: string,
|
|
281
|
+
fileId: string,
|
|
282
|
+
): Promise<void> {
|
|
283
|
+
try {
|
|
284
|
+
await fs.unlink(path.join(CONTEXT_FILES_DIR, fileId));
|
|
285
|
+
} catch {
|
|
286
|
+
/* ok */
|
|
287
|
+
}
|
|
288
|
+
const q = await getQuestion(questionId);
|
|
289
|
+
if (q) {
|
|
290
|
+
q.contextFiles = (q.contextFiles || []).filter((f) => f.id !== fileId);
|
|
291
|
+
await saveQuestion(q);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function updateQuestionMessages(
|
|
296
|
+
questionId: string,
|
|
297
|
+
messages: Message[],
|
|
298
|
+
): Promise<void> {
|
|
299
|
+
const q = await getQuestion(questionId);
|
|
300
|
+
if (!q) throw new Error("Question not found");
|
|
301
|
+
q.messages = messages;
|
|
302
|
+
await saveQuestion(q);
|
|
303
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "src",
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|