create-interview-cockpit 0.4.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.
- package/package.json +1 -1
- package/template/client/package-lock.json +753 -1
- package/template/client/package.json +4 -0
- package/template/client/src/App.tsx +20 -0
- package/template/client/src/api.ts +455 -3
- package/template/client/src/components/AiSettingsModal.tsx +855 -248
- package/template/client/src/components/AnnotationDialog.tsx +3 -9
- package/template/client/src/components/ChatMessage.tsx +132 -27
- package/template/client/src/components/ChatView.tsx +365 -123
- package/template/client/src/components/CodeContextPanel.tsx +714 -0
- package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
- package/template/client/src/components/CodeRunnerModal.tsx +3030 -0
- package/template/client/src/components/DocRefModal.tsx +551 -0
- package/template/client/src/components/FileAttachments.tsx +128 -12
- package/template/client/src/components/FilePickerModal.tsx +181 -0
- package/template/client/src/components/FileViewerModal.tsx +406 -28
- package/template/client/src/components/InfraLabModal.tsx +1706 -0
- package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
- package/template/client/src/components/MarkdownRenderer.tsx +219 -2
- package/template/client/src/components/NotesModal.tsx +977 -0
- package/template/client/src/components/PlotEmbed.tsx +173 -0
- package/template/client/src/components/Sidebar.tsx +397 -127
- package/template/client/src/components/TextAnnotator.tsx +8 -15
- package/template/client/src/components/VizCraftEmbed.tsx +412 -25
- package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
- package/template/client/src/infraLab.ts +124 -0
- package/template/client/src/reactLab.ts +477 -0
- package/template/client/src/store.ts +416 -2
- package/template/client/src/types.ts +41 -1
- package/template/client/tsconfig.tsbuildinfo +1 -1
- package/template/cockpit.json +1 -1
- package/template/package.json +1 -1
- package/template/server/src/google-drive.ts +144 -2
- package/template/server/src/index.ts +1890 -188
- package/template/server/src/infra-runner.ts +1104 -0
- package/template/server/src/storage.ts +274 -3
|
@@ -313,6 +313,14 @@ export async function syncWorkspace(
|
|
|
313
313
|
const questions = pending.filter((p) => !p.isContextFile);
|
|
314
314
|
const contextFiles = pending.filter((p) => p.isContextFile);
|
|
315
315
|
|
|
316
|
+
// Collect parent-title links to re-apply after all questions are saved
|
|
317
|
+
// (IDs are regenerated on import, so we must link by title within a topic)
|
|
318
|
+
const parentLinks: Array<{
|
|
319
|
+
questionId: string;
|
|
320
|
+
topicId: string;
|
|
321
|
+
parentTitle: string;
|
|
322
|
+
}> = [];
|
|
323
|
+
|
|
316
324
|
await Promise.all(
|
|
317
325
|
questions.map(async ({ topicId, filename, buffer }) => {
|
|
318
326
|
try {
|
|
@@ -320,6 +328,9 @@ export async function syncWorkspace(
|
|
|
320
328
|
let systemContext = "";
|
|
321
329
|
let messages: storage.Question["messages"] = [];
|
|
322
330
|
let codeContextFiles: string[] = [];
|
|
331
|
+
let codeAnnotations: storage.Question["codeAnnotations"] = {};
|
|
332
|
+
let parentTitle: string | null = null;
|
|
333
|
+
const restoredContextFiles: storage.ContextFile[] = [];
|
|
323
334
|
|
|
324
335
|
if (filename.endsWith(".json")) {
|
|
325
336
|
// Our own exported JSON — restore fields directly, no text extraction
|
|
@@ -329,6 +340,45 @@ export async function syncWorkspace(
|
|
|
329
340
|
systemContext = parsed.systemContext || "";
|
|
330
341
|
messages = parsed.messages || [];
|
|
331
342
|
codeContextFiles = parsed.codeContextFiles || [];
|
|
343
|
+
codeAnnotations = parsed.codeAnnotations || {};
|
|
344
|
+
parentTitle = parsed.parentTitle || null;
|
|
345
|
+
|
|
346
|
+
// Restore code snippet context files (user/ai origin)
|
|
347
|
+
if (
|
|
348
|
+
Array.isArray(parsed.codeSnippets) &&
|
|
349
|
+
parsed.codeSnippets.length > 0
|
|
350
|
+
) {
|
|
351
|
+
const ctxDir =
|
|
352
|
+
storage.getContextFilesDirForWorkspace(workspaceId);
|
|
353
|
+
await Promise.all(
|
|
354
|
+
parsed.codeSnippets.map(
|
|
355
|
+
async (cs: storage.ContextFile & { content?: string }) => {
|
|
356
|
+
if (
|
|
357
|
+
cs.content &&
|
|
358
|
+
(cs.origin === "user" ||
|
|
359
|
+
cs.origin === "ai" ||
|
|
360
|
+
cs.origin === "sandbox" ||
|
|
361
|
+
cs.origin === "react" ||
|
|
362
|
+
cs.origin === "nextjs" ||
|
|
363
|
+
cs.origin === "infra")
|
|
364
|
+
) {
|
|
365
|
+
try {
|
|
366
|
+
await fs.mkdir(ctxDir, { recursive: true });
|
|
367
|
+
await fs.writeFile(
|
|
368
|
+
path.join(ctxDir, cs.id),
|
|
369
|
+
Buffer.from(cs.content, "utf-8"),
|
|
370
|
+
);
|
|
371
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
372
|
+
const { content: _content, ...cfMeta } = cs;
|
|
373
|
+
restoredContextFiles.push(cfMeta);
|
|
374
|
+
} catch {
|
|
375
|
+
/* skip bad blobs */
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
),
|
|
380
|
+
);
|
|
381
|
+
}
|
|
332
382
|
} catch {
|
|
333
383
|
// Malformed JSON — fall through to raw text
|
|
334
384
|
systemContext = buffer.toString("utf-8");
|
|
@@ -344,18 +394,47 @@ export async function syncWorkspace(
|
|
|
344
394
|
title,
|
|
345
395
|
systemContext,
|
|
346
396
|
codeContextFiles,
|
|
347
|
-
contextFiles:
|
|
397
|
+
contextFiles: restoredContextFiles,
|
|
348
398
|
messages,
|
|
399
|
+
codeAnnotations,
|
|
349
400
|
createdAt: new Date().toISOString(),
|
|
350
401
|
};
|
|
351
402
|
await storage.saveQuestion(q);
|
|
352
403
|
result.filesImported++;
|
|
404
|
+
if (parentTitle) {
|
|
405
|
+
parentLinks.push({ questionId: q.id, topicId, parentTitle });
|
|
406
|
+
}
|
|
353
407
|
} catch (err: any) {
|
|
354
408
|
result.errors.push(`${filename}: ${err?.message ?? "failed"}`);
|
|
355
409
|
}
|
|
356
410
|
}),
|
|
357
411
|
);
|
|
358
412
|
|
|
413
|
+
// ── Phase 4b: re-link parent–child relationships by title ───────────────────
|
|
414
|
+
// IDs are regenerated on import, so we match parent by title within the same topic.
|
|
415
|
+
if (parentLinks.length > 0) {
|
|
416
|
+
// Group by topicId to avoid redundant getQuestionsByTopic calls
|
|
417
|
+
const byTopic = new Map<string, typeof parentLinks>();
|
|
418
|
+
for (const link of parentLinks) {
|
|
419
|
+
const arr = byTopic.get(link.topicId) ?? [];
|
|
420
|
+
arr.push(link);
|
|
421
|
+
byTopic.set(link.topicId, arr);
|
|
422
|
+
}
|
|
423
|
+
for (const [topicId, links] of byTopic) {
|
|
424
|
+
const allInTopic = await storage.getQuestionsByTopic(topicId);
|
|
425
|
+
for (const { questionId, parentTitle } of links) {
|
|
426
|
+
const parent = allInTopic.find(
|
|
427
|
+
(q) => q.title === parentTitle && q.id !== questionId,
|
|
428
|
+
);
|
|
429
|
+
const child = allInTopic.find((q) => q.id === questionId);
|
|
430
|
+
if (parent && child) {
|
|
431
|
+
child.parentQuestionId = parent.id;
|
|
432
|
+
await storage.saveQuestion(child);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
359
438
|
// ── Phase 5: save context file blobs in parallel, then update topics.json once ─
|
|
360
439
|
if (contextFiles.length > 0) {
|
|
361
440
|
// Write binary files in parallel
|
|
@@ -486,7 +565,35 @@ async function getExportDriveClient(): Promise<drive_v3.Drive> {
|
|
|
486
565
|
/* ok */
|
|
487
566
|
}
|
|
488
567
|
});
|
|
489
|
-
|
|
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;
|
|
490
597
|
}
|
|
491
598
|
|
|
492
599
|
export interface ExportResult {
|
|
@@ -645,13 +752,48 @@ export async function exportWorkspace(
|
|
|
645
752
|
try {
|
|
646
753
|
const safeName =
|
|
647
754
|
q.title.replace(/[/\\:*?"<>|]/g, "-").trim() || q.id;
|
|
755
|
+
// Look up parent title so the relationship can be restored on import
|
|
756
|
+
const parentTitle = q.parentQuestionId
|
|
757
|
+
? questions.find((p) => p.id === q.parentQuestionId)?.title
|
|
758
|
+
: undefined;
|
|
759
|
+
|
|
760
|
+
// Read code snippet blobs so they survive drive sync
|
|
761
|
+
const ctxDir =
|
|
762
|
+
storage.getContextFilesDirForWorkspace(workspaceId);
|
|
763
|
+
const contextFilesWithContent: Array<
|
|
764
|
+
storage.ContextFile & { content?: string }
|
|
765
|
+
> = [];
|
|
766
|
+
for (const cf of q.contextFiles || []) {
|
|
767
|
+
if (
|
|
768
|
+
cf.origin === "user" ||
|
|
769
|
+
cf.origin === "ai" ||
|
|
770
|
+
cf.origin === "sandbox" ||
|
|
771
|
+
cf.origin === "react" ||
|
|
772
|
+
cf.origin === "nextjs" ||
|
|
773
|
+
cf.origin === "infra"
|
|
774
|
+
) {
|
|
775
|
+
try {
|
|
776
|
+
const content = await fs.readFile(
|
|
777
|
+
path.join(ctxDir, cf.id),
|
|
778
|
+
"utf-8",
|
|
779
|
+
);
|
|
780
|
+
contextFilesWithContent.push({ ...cf, content });
|
|
781
|
+
} catch {
|
|
782
|
+
contextFilesWithContent.push(cf);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
648
787
|
// Export full question as JSON so nothing is lost on sync-back
|
|
649
788
|
const payload = JSON.stringify(
|
|
650
789
|
{
|
|
651
790
|
title: q.title,
|
|
791
|
+
parentTitle: parentTitle ?? null,
|
|
652
792
|
systemContext: q.systemContext || "",
|
|
653
793
|
messages: q.messages,
|
|
654
794
|
codeContextFiles: q.codeContextFiles,
|
|
795
|
+
codeAnnotations: q.codeAnnotations ?? {},
|
|
796
|
+
codeSnippets: contextFilesWithContent,
|
|
655
797
|
},
|
|
656
798
|
null,
|
|
657
799
|
2,
|