create-interview-cockpit 0.1.0 → 0.2.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/index.js +13 -3
- package/package.json +1 -1
- package/template/client/src/App.tsx +7 -2
- package/template/client/src/api.ts +190 -1
- package/template/client/src/components/ChatMessage.tsx +27 -1
- package/template/client/src/components/ChatView.tsx +110 -6
- package/template/client/src/components/Sidebar.tsx +342 -182
- package/template/client/src/components/WorkspaceSwitcher.tsx +891 -0
- package/template/client/src/store.ts +188 -1
- package/template/client/src/types.ts +20 -0
- package/template/cockpit.json +1 -1
- package/template/server/package-lock.json +286 -0
- package/template/server/package.json +1 -0
- package/template/server/src/google-drive.ts +714 -0
- package/template/server/src/index.ts +193 -4
- package/template/server/src/storage.ts +332 -32
|
@@ -4,14 +4,38 @@ import { fileURLToPath } from "url";
|
|
|
4
4
|
|
|
5
5
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
const DATA_DIR = path.join(__dirname, "../../data");
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const CONTEXT_FILES_DIR = path.join(DATA_DIR, "context-files");
|
|
7
|
+
const WORKSPACES_DIR = path.join(DATA_DIR, "workspaces");
|
|
8
|
+
const WORKSPACES_FILE = path.join(DATA_DIR, "workspaces.json");
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
// ── Active workspace ──────────────────────────────────────────
|
|
11
|
+
let _activeWorkspaceId = "default";
|
|
12
|
+
|
|
13
|
+
export function getActiveWorkspaceId(): string {
|
|
14
|
+
return _activeWorkspaceId;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Directly set the active workspace without side effects (no disk write, no dir creation). */
|
|
18
|
+
export function setActiveWorkspaceId(id: string): void {
|
|
19
|
+
_activeWorkspaceId = id;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Workspace-scoped path helpers ─────────────────────────────
|
|
23
|
+
function workspaceDir(id = _activeWorkspaceId) {
|
|
24
|
+
return path.join(WORKSPACES_DIR, id);
|
|
25
|
+
}
|
|
26
|
+
function topicsFilePath(id = _activeWorkspaceId) {
|
|
27
|
+
return path.join(workspaceDir(id), "topics.json");
|
|
28
|
+
}
|
|
29
|
+
function questionsDirPath(id = _activeWorkspaceId) {
|
|
30
|
+
return path.join(workspaceDir(id), "questions");
|
|
31
|
+
}
|
|
32
|
+
function contextFilesDirPath(id = _activeWorkspaceId) {
|
|
33
|
+
return path.join(workspaceDir(id), "context-files");
|
|
34
|
+
}
|
|
35
|
+
async function ensureWorkspaceDirs(id = _activeWorkspaceId) {
|
|
36
|
+
await fs.mkdir(workspaceDir(id), { recursive: true });
|
|
37
|
+
await fs.mkdir(questionsDirPath(id), { recursive: true });
|
|
38
|
+
await fs.mkdir(contextFilesDirPath(id), { recursive: true });
|
|
15
39
|
}
|
|
16
40
|
|
|
17
41
|
export interface Topic {
|
|
@@ -25,6 +49,7 @@ export interface ContextFile {
|
|
|
25
49
|
id: string;
|
|
26
50
|
name: string;
|
|
27
51
|
originalName: string;
|
|
52
|
+
driveFileId?: string;
|
|
28
53
|
createdAt: string;
|
|
29
54
|
}
|
|
30
55
|
|
|
@@ -71,12 +96,226 @@ export interface Question {
|
|
|
71
96
|
createdAt: string;
|
|
72
97
|
}
|
|
73
98
|
|
|
99
|
+
// ── Workspace registry types ──────────────────────────────────────────
|
|
100
|
+
export interface DriveConfig {
|
|
101
|
+
folderId: string;
|
|
102
|
+
folderName: string;
|
|
103
|
+
lastSyncedAt?: string;
|
|
104
|
+
subFolderId?: string;
|
|
105
|
+
subFolderName?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface WorkspaceMeta {
|
|
109
|
+
id: string;
|
|
110
|
+
name: string;
|
|
111
|
+
type: "local" | "google_drive";
|
|
112
|
+
driveConfig?: DriveConfig;
|
|
113
|
+
createdAt: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface WorkspacesRegistry {
|
|
117
|
+
activeWorkspaceId: string;
|
|
118
|
+
workspaces: WorkspaceMeta[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Migration: flat data/ → data/workspaces/default/ ────────────────────
|
|
122
|
+
// Call once on server startup. No-op if already migrated.
|
|
123
|
+
export async function migrateToWorkspaces(): Promise<void> {
|
|
124
|
+
try {
|
|
125
|
+
await fs.access(WORKSPACES_FILE);
|
|
126
|
+
// Already migrated — load the persisted active workspace ID
|
|
127
|
+
const data = await fs.readFile(WORKSPACES_FILE, "utf-8");
|
|
128
|
+
const registry: WorkspacesRegistry = JSON.parse(data);
|
|
129
|
+
_activeWorkspaceId = registry.activeWorkspaceId || "default";
|
|
130
|
+
return;
|
|
131
|
+
} catch {
|
|
132
|
+
// No registry yet — perform first-time migration
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const defaultId = "default";
|
|
136
|
+
await ensureWorkspaceDirs(defaultId);
|
|
137
|
+
|
|
138
|
+
// Copy existing flat topics.json
|
|
139
|
+
try {
|
|
140
|
+
await fs.copyFile(
|
|
141
|
+
path.join(DATA_DIR, "topics.json"),
|
|
142
|
+
topicsFilePath(defaultId),
|
|
143
|
+
);
|
|
144
|
+
} catch {
|
|
145
|
+
await fs.writeFile(topicsFilePath(defaultId), JSON.stringify([], null, 2));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Copy existing questions/*.json
|
|
149
|
+
try {
|
|
150
|
+
const files = await fs.readdir(path.join(DATA_DIR, "questions"));
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
if (!file.endsWith(".json")) continue;
|
|
153
|
+
await fs.copyFile(
|
|
154
|
+
path.join(DATA_DIR, "questions", file),
|
|
155
|
+
path.join(questionsDirPath(defaultId), file),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
/* ok if not present */
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Copy existing context-files/*
|
|
163
|
+
try {
|
|
164
|
+
const files = await fs.readdir(path.join(DATA_DIR, "context-files"));
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
await fs.copyFile(
|
|
167
|
+
path.join(DATA_DIR, "context-files", file),
|
|
168
|
+
path.join(contextFilesDirPath(defaultId), file),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
/* ok if not present */
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const registry: WorkspacesRegistry = {
|
|
176
|
+
activeWorkspaceId: defaultId,
|
|
177
|
+
workspaces: [
|
|
178
|
+
{
|
|
179
|
+
id: defaultId,
|
|
180
|
+
name: "Local",
|
|
181
|
+
type: "local",
|
|
182
|
+
createdAt: new Date().toISOString(),
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
await fs.writeFile(WORKSPACES_FILE, JSON.stringify(registry, null, 2));
|
|
187
|
+
_activeWorkspaceId = defaultId;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Workspace CRUD ───────────────────────────────────────────────
|
|
191
|
+
export async function getWorkspaces(): Promise<WorkspacesRegistry> {
|
|
192
|
+
try {
|
|
193
|
+
const data = await fs.readFile(WORKSPACES_FILE, "utf-8");
|
|
194
|
+
const registry = JSON.parse(data) as WorkspacesRegistry;
|
|
195
|
+
// Keep the in-memory active ID in sync with what's persisted
|
|
196
|
+
if (registry.activeWorkspaceId) {
|
|
197
|
+
_activeWorkspaceId = registry.activeWorkspaceId;
|
|
198
|
+
}
|
|
199
|
+
return registry;
|
|
200
|
+
} catch {
|
|
201
|
+
return { activeWorkspaceId: _activeWorkspaceId, workspaces: [] };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function saveWorkspace(ws: WorkspaceMeta): Promise<WorkspaceMeta> {
|
|
206
|
+
const registry = await getWorkspaces();
|
|
207
|
+
const idx = registry.workspaces.findIndex((w) => w.id === ws.id);
|
|
208
|
+
if (idx !== -1) {
|
|
209
|
+
registry.workspaces[idx] = ws;
|
|
210
|
+
} else {
|
|
211
|
+
registry.workspaces.push(ws);
|
|
212
|
+
await ensureWorkspaceDirs(ws.id);
|
|
213
|
+
}
|
|
214
|
+
await fs.writeFile(WORKSPACES_FILE, JSON.stringify(registry, null, 2));
|
|
215
|
+
return ws;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function updateWorkspace(
|
|
219
|
+
id: string,
|
|
220
|
+
patch: Partial<WorkspaceMeta>,
|
|
221
|
+
): Promise<WorkspaceMeta | null> {
|
|
222
|
+
const registry = await getWorkspaces();
|
|
223
|
+
const idx = registry.workspaces.findIndex((w) => w.id === id);
|
|
224
|
+
if (idx === -1) return null;
|
|
225
|
+
// Deep-merge driveConfig if provided
|
|
226
|
+
if (patch.driveConfig && registry.workspaces[idx].driveConfig) {
|
|
227
|
+
patch.driveConfig = {
|
|
228
|
+
...registry.workspaces[idx].driveConfig,
|
|
229
|
+
...patch.driveConfig,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
Object.assign(registry.workspaces[idx], patch);
|
|
233
|
+
await fs.writeFile(WORKSPACES_FILE, JSON.stringify(registry, null, 2));
|
|
234
|
+
return registry.workspaces[idx];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function deleteWorkspace(id: string): Promise<void> {
|
|
238
|
+
const registry = await getWorkspaces();
|
|
239
|
+
if (registry.workspaces.length <= 1) {
|
|
240
|
+
throw new Error("Cannot delete the last workspace");
|
|
241
|
+
}
|
|
242
|
+
registry.workspaces = registry.workspaces.filter((w) => w.id !== id);
|
|
243
|
+
if (registry.activeWorkspaceId === id) {
|
|
244
|
+
registry.activeWorkspaceId = registry.workspaces[0].id;
|
|
245
|
+
_activeWorkspaceId = registry.activeWorkspaceId;
|
|
246
|
+
}
|
|
247
|
+
await fs.writeFile(WORKSPACES_FILE, JSON.stringify(registry, null, 2));
|
|
248
|
+
try {
|
|
249
|
+
await fs.rm(workspaceDir(id), { recursive: true });
|
|
250
|
+
} catch {
|
|
251
|
+
/* ok */
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function activateWorkspace(
|
|
256
|
+
id: string,
|
|
257
|
+
): Promise<WorkspacesRegistry> {
|
|
258
|
+
const registry = await getWorkspaces();
|
|
259
|
+
const ws = registry.workspaces.find((w) => w.id === id);
|
|
260
|
+
if (!ws) throw new Error(`Workspace "${id}" not found`);
|
|
261
|
+
registry.activeWorkspaceId = id;
|
|
262
|
+
_activeWorkspaceId = id;
|
|
263
|
+
await ensureWorkspaceDirs(id);
|
|
264
|
+
await fs.writeFile(WORKSPACES_FILE, JSON.stringify(registry, null, 2));
|
|
265
|
+
return registry;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Workspace-scoped read helpers used by the Drive export path
|
|
269
|
+
// (avoids mutating the module-level active workspace)
|
|
270
|
+
export async function getTopicsForWorkspace(
|
|
271
|
+
workspaceId: string,
|
|
272
|
+
): Promise<Topic[]> {
|
|
273
|
+
await ensureWorkspaceDirs(workspaceId);
|
|
274
|
+
try {
|
|
275
|
+
const data = await fs.readFile(topicsFilePath(workspaceId), "utf-8");
|
|
276
|
+
const topics: Topic[] = JSON.parse(data);
|
|
277
|
+
for (const t of topics) {
|
|
278
|
+
if (!t.contextFiles) t.contextFiles = [];
|
|
279
|
+
}
|
|
280
|
+
return topics;
|
|
281
|
+
} catch {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function getQuestionsByTopicForWorkspace(
|
|
287
|
+
workspaceId: string,
|
|
288
|
+
topicId: string,
|
|
289
|
+
): Promise<Question[]> {
|
|
290
|
+
await ensureWorkspaceDirs(workspaceId);
|
|
291
|
+
try {
|
|
292
|
+
const files = await fs.readdir(questionsDirPath(workspaceId));
|
|
293
|
+
const questions: Question[] = [];
|
|
294
|
+
for (const file of files) {
|
|
295
|
+
if (!file.endsWith(".json")) continue;
|
|
296
|
+
const data = await fs.readFile(
|
|
297
|
+
path.join(questionsDirPath(workspaceId), file),
|
|
298
|
+
"utf-8",
|
|
299
|
+
);
|
|
300
|
+
const q: Question = JSON.parse(data);
|
|
301
|
+
if (q.topicId === topicId) questions.push(q);
|
|
302
|
+
}
|
|
303
|
+
return questions.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
304
|
+
} catch {
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function getContextFilesDirForWorkspace(workspaceId: string): string {
|
|
310
|
+
return contextFilesDirPath(workspaceId);
|
|
311
|
+
}
|
|
312
|
+
|
|
74
313
|
// --- Topics ---
|
|
75
314
|
|
|
76
315
|
export async function getTopics(): Promise<Topic[]> {
|
|
77
|
-
await
|
|
316
|
+
await ensureWorkspaceDirs();
|
|
78
317
|
try {
|
|
79
|
-
const data = await fs.readFile(
|
|
318
|
+
const data = await fs.readFile(topicsFilePath(), "utf-8");
|
|
80
319
|
const topics: Topic[] = JSON.parse(data);
|
|
81
320
|
// Backfill missing fields from older data
|
|
82
321
|
for (const t of topics) {
|
|
@@ -96,7 +335,7 @@ export async function saveTopic(topic: Topic): Promise<Topic> {
|
|
|
96
335
|
} else {
|
|
97
336
|
topics.push(topic);
|
|
98
337
|
}
|
|
99
|
-
await fs.writeFile(
|
|
338
|
+
await fs.writeFile(topicsFilePath(), JSON.stringify(topics, null, 2));
|
|
100
339
|
return topic;
|
|
101
340
|
}
|
|
102
341
|
|
|
@@ -107,14 +346,14 @@ export async function deleteTopic(id: string): Promise<void> {
|
|
|
107
346
|
if (topic?.contextFiles) {
|
|
108
347
|
for (const cf of topic.contextFiles) {
|
|
109
348
|
try {
|
|
110
|
-
await fs.unlink(path.join(
|
|
349
|
+
await fs.unlink(path.join(contextFilesDirPath(), cf.id));
|
|
111
350
|
} catch {
|
|
112
351
|
/* already gone */
|
|
113
352
|
}
|
|
114
353
|
}
|
|
115
354
|
}
|
|
116
355
|
const remaining = topics.filter((t) => t.id !== id);
|
|
117
|
-
await fs.writeFile(
|
|
356
|
+
await fs.writeFile(topicsFilePath(), JSON.stringify(remaining, null, 2));
|
|
118
357
|
const questions = await getQuestionsByTopic(id);
|
|
119
358
|
for (const q of questions) {
|
|
120
359
|
await deleteQuestion(q.id);
|
|
@@ -129,14 +368,54 @@ export async function updateTopic(
|
|
|
129
368
|
const idx = topics.findIndex((t) => t.id === id);
|
|
130
369
|
if (idx === -1) return null;
|
|
131
370
|
Object.assign(topics[idx], patch);
|
|
132
|
-
await fs.writeFile(
|
|
371
|
+
await fs.writeFile(topicsFilePath(), JSON.stringify(topics, null, 2));
|
|
133
372
|
return topics[idx];
|
|
134
373
|
}
|
|
135
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Replace the entire topic list in one write (used by sync to avoid N read+write cycles).
|
|
377
|
+
*/
|
|
378
|
+
export async function replaceAllTopics(topics: Topic[]): Promise<void> {
|
|
379
|
+
await ensureWorkspaceDirs();
|
|
380
|
+
await fs.writeFile(topicsFilePath(), JSON.stringify(topics, null, 2));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Wipe all topics, questions, and context-file blobs for a workspace in parallel.
|
|
385
|
+
* Much faster than calling deleteTopic() in a loop.
|
|
386
|
+
*/
|
|
387
|
+
export async function clearWorkspaceData(workspaceId: string): Promise<void> {
|
|
388
|
+
await ensureWorkspaceDirs(workspaceId);
|
|
389
|
+
const wid = workspaceId;
|
|
390
|
+
await fs.writeFile(topicsFilePath(wid), JSON.stringify([], null, 2));
|
|
391
|
+
await Promise.all([
|
|
392
|
+
fs
|
|
393
|
+
.readdir(questionsDirPath(wid))
|
|
394
|
+
.then((files) =>
|
|
395
|
+
Promise.all(
|
|
396
|
+
files.map((f) =>
|
|
397
|
+
fs.unlink(path.join(questionsDirPath(wid), f)).catch(() => {}),
|
|
398
|
+
),
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
.catch(() => {}),
|
|
402
|
+
fs
|
|
403
|
+
.readdir(contextFilesDirPath(wid))
|
|
404
|
+
.then((files) =>
|
|
405
|
+
Promise.all(
|
|
406
|
+
files.map((f) =>
|
|
407
|
+
fs.unlink(path.join(contextFilesDirPath(wid), f)).catch(() => {}),
|
|
408
|
+
),
|
|
409
|
+
),
|
|
410
|
+
)
|
|
411
|
+
.catch(() => {}),
|
|
412
|
+
]);
|
|
413
|
+
}
|
|
414
|
+
|
|
136
415
|
// --- Context Files ---
|
|
137
416
|
|
|
138
417
|
export function getContextFilesDir(): string {
|
|
139
|
-
return
|
|
418
|
+
return contextFilesDirPath();
|
|
140
419
|
}
|
|
141
420
|
|
|
142
421
|
export async function saveContextFile(
|
|
@@ -144,13 +423,15 @@ export async function saveContextFile(
|
|
|
144
423
|
fileId: string,
|
|
145
424
|
originalName: string,
|
|
146
425
|
buffer: Buffer,
|
|
426
|
+
driveFileId?: string,
|
|
147
427
|
): Promise<ContextFile> {
|
|
148
|
-
await
|
|
149
|
-
await fs.writeFile(path.join(
|
|
428
|
+
await ensureWorkspaceDirs();
|
|
429
|
+
await fs.writeFile(path.join(contextFilesDirPath(), fileId), buffer);
|
|
150
430
|
const cf: ContextFile = {
|
|
151
431
|
id: fileId,
|
|
152
432
|
name: originalName,
|
|
153
433
|
originalName,
|
|
434
|
+
...(driveFileId ? { driveFileId } : {}),
|
|
154
435
|
createdAt: new Date().toISOString(),
|
|
155
436
|
};
|
|
156
437
|
const topics = await getTopics();
|
|
@@ -158,17 +439,33 @@ export async function saveContextFile(
|
|
|
158
439
|
if (topic) {
|
|
159
440
|
if (!topic.contextFiles) topic.contextFiles = [];
|
|
160
441
|
topic.contextFiles.push(cf);
|
|
161
|
-
await fs.writeFile(
|
|
442
|
+
await fs.writeFile(topicsFilePath(), JSON.stringify(topics, null, 2));
|
|
162
443
|
}
|
|
163
444
|
return cf;
|
|
164
445
|
}
|
|
165
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Write only the binary blob for a context file — does NOT update topics.json.
|
|
449
|
+
* Use this when you want to batch-update topics.json separately (e.g. during sync).
|
|
450
|
+
*/
|
|
451
|
+
export async function writeContextFileBlob(
|
|
452
|
+
workspaceId: string,
|
|
453
|
+
fileId: string,
|
|
454
|
+
buffer: Buffer,
|
|
455
|
+
): Promise<void> {
|
|
456
|
+
await ensureWorkspaceDirs(workspaceId);
|
|
457
|
+
await fs.writeFile(
|
|
458
|
+
path.join(contextFilesDirPath(workspaceId), fileId),
|
|
459
|
+
buffer,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
166
463
|
export async function deleteContextFile(
|
|
167
464
|
topicId: string,
|
|
168
465
|
fileId: string,
|
|
169
466
|
): Promise<void> {
|
|
170
467
|
try {
|
|
171
|
-
await fs.unlink(path.join(
|
|
468
|
+
await fs.unlink(path.join(contextFilesDirPath(), fileId));
|
|
172
469
|
} catch {
|
|
173
470
|
/* already gone */
|
|
174
471
|
}
|
|
@@ -178,21 +475,21 @@ export async function deleteContextFile(
|
|
|
178
475
|
topic.contextFiles = (topic.contextFiles || []).filter(
|
|
179
476
|
(f) => f.id !== fileId,
|
|
180
477
|
);
|
|
181
|
-
await fs.writeFile(
|
|
478
|
+
await fs.writeFile(topicsFilePath(), JSON.stringify(topics, null, 2));
|
|
182
479
|
}
|
|
183
480
|
}
|
|
184
481
|
|
|
185
482
|
export async function readContextFileContent(fileId: string): Promise<string> {
|
|
186
|
-
return fs.readFile(path.join(
|
|
483
|
+
return fs.readFile(path.join(contextFilesDirPath(), fileId), "utf-8");
|
|
187
484
|
}
|
|
188
485
|
|
|
189
486
|
// --- Questions ---
|
|
190
487
|
|
|
191
488
|
export async function getQuestion(id: string): Promise<Question | null> {
|
|
192
|
-
await
|
|
489
|
+
await ensureWorkspaceDirs();
|
|
193
490
|
try {
|
|
194
491
|
const data = await fs.readFile(
|
|
195
|
-
path.join(
|
|
492
|
+
path.join(questionsDirPath(), `${id}.json`),
|
|
196
493
|
"utf-8",
|
|
197
494
|
);
|
|
198
495
|
const q: Question = JSON.parse(data);
|
|
@@ -207,13 +504,16 @@ export async function getQuestion(id: string): Promise<Question | null> {
|
|
|
207
504
|
export async function getQuestionsByTopic(
|
|
208
505
|
topicId: string,
|
|
209
506
|
): Promise<Question[]> {
|
|
210
|
-
await
|
|
507
|
+
await ensureWorkspaceDirs();
|
|
211
508
|
try {
|
|
212
|
-
const files = await fs.readdir(
|
|
509
|
+
const files = await fs.readdir(questionsDirPath());
|
|
213
510
|
const questions: Question[] = [];
|
|
214
511
|
for (const file of files) {
|
|
215
512
|
if (!file.endsWith(".json")) continue;
|
|
216
|
-
const data = await fs.readFile(
|
|
513
|
+
const data = await fs.readFile(
|
|
514
|
+
path.join(questionsDirPath(), file),
|
|
515
|
+
"utf-8",
|
|
516
|
+
);
|
|
217
517
|
const q: Question = JSON.parse(data);
|
|
218
518
|
if (q.topicId === topicId) {
|
|
219
519
|
questions.push(q);
|
|
@@ -226,9 +526,9 @@ export async function getQuestionsByTopic(
|
|
|
226
526
|
}
|
|
227
527
|
|
|
228
528
|
export async function saveQuestion(question: Question): Promise<Question> {
|
|
229
|
-
await
|
|
529
|
+
await ensureWorkspaceDirs();
|
|
230
530
|
await fs.writeFile(
|
|
231
|
-
path.join(
|
|
531
|
+
path.join(questionsDirPath(), `${question.id}.json`),
|
|
232
532
|
JSON.stringify(question, null, 2),
|
|
233
533
|
);
|
|
234
534
|
return question;
|
|
@@ -240,14 +540,14 @@ export async function deleteQuestion(id: string): Promise<void> {
|
|
|
240
540
|
if (q?.contextFiles) {
|
|
241
541
|
for (const cf of q.contextFiles) {
|
|
242
542
|
try {
|
|
243
|
-
await fs.unlink(path.join(
|
|
543
|
+
await fs.unlink(path.join(contextFilesDirPath(), cf.id));
|
|
244
544
|
} catch {
|
|
245
545
|
/* ok */
|
|
246
546
|
}
|
|
247
547
|
}
|
|
248
548
|
}
|
|
249
549
|
try {
|
|
250
|
-
await fs.unlink(path.join(
|
|
550
|
+
await fs.unlink(path.join(questionsDirPath(), `${id}.json`));
|
|
251
551
|
} catch {
|
|
252
552
|
// ignore if already deleted
|
|
253
553
|
}
|
|
@@ -259,8 +559,8 @@ export async function saveQuestionContextFile(
|
|
|
259
559
|
originalName: string,
|
|
260
560
|
buffer: Buffer,
|
|
261
561
|
): Promise<ContextFile> {
|
|
262
|
-
await
|
|
263
|
-
await fs.writeFile(path.join(
|
|
562
|
+
await ensureWorkspaceDirs();
|
|
563
|
+
await fs.writeFile(path.join(contextFilesDirPath(), fileId), buffer);
|
|
264
564
|
const cf: ContextFile = {
|
|
265
565
|
id: fileId,
|
|
266
566
|
name: originalName,
|
|
@@ -281,7 +581,7 @@ export async function deleteQuestionContextFile(
|
|
|
281
581
|
fileId: string,
|
|
282
582
|
): Promise<void> {
|
|
283
583
|
try {
|
|
284
|
-
await fs.unlink(path.join(
|
|
584
|
+
await fs.unlink(path.join(contextFilesDirPath(), fileId));
|
|
285
585
|
} catch {
|
|
286
586
|
/* ok */
|
|
287
587
|
}
|