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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/template/client/package-lock.json +753 -1
  3. package/template/client/package.json +4 -0
  4. package/template/client/src/App.tsx +20 -0
  5. package/template/client/src/api.ts +455 -3
  6. package/template/client/src/components/AiSettingsModal.tsx +855 -248
  7. package/template/client/src/components/AnnotationDialog.tsx +3 -9
  8. package/template/client/src/components/ChatMessage.tsx +132 -27
  9. package/template/client/src/components/ChatView.tsx +365 -123
  10. package/template/client/src/components/CodeContextPanel.tsx +714 -0
  11. package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
  12. package/template/client/src/components/CodeRunnerModal.tsx +3030 -0
  13. package/template/client/src/components/DocRefModal.tsx +551 -0
  14. package/template/client/src/components/FileAttachments.tsx +128 -12
  15. package/template/client/src/components/FilePickerModal.tsx +181 -0
  16. package/template/client/src/components/FileViewerModal.tsx +406 -28
  17. package/template/client/src/components/InfraLabModal.tsx +1706 -0
  18. package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
  19. package/template/client/src/components/MarkdownRenderer.tsx +219 -2
  20. package/template/client/src/components/NotesModal.tsx +977 -0
  21. package/template/client/src/components/PlotEmbed.tsx +173 -0
  22. package/template/client/src/components/Sidebar.tsx +397 -127
  23. package/template/client/src/components/TextAnnotator.tsx +8 -15
  24. package/template/client/src/components/VizCraftEmbed.tsx +412 -25
  25. package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
  26. package/template/client/src/infraLab.ts +124 -0
  27. package/template/client/src/reactLab.ts +477 -0
  28. package/template/client/src/store.ts +416 -2
  29. package/template/client/src/types.ts +41 -1
  30. package/template/client/tsconfig.tsbuildinfo +1 -1
  31. package/template/cockpit.json +1 -1
  32. package/template/package.json +1 -1
  33. package/template/server/src/google-drive.ts +144 -2
  34. package/template/server/src/index.ts +1890 -188
  35. package/template/server/src/infra-runner.ts +1104 -0
  36. 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
- return google.drive({ version: "v3", auth: client });
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,