create-interview-cockpit 0.4.0 → 0.5.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 (27) hide show
  1. package/package.json +1 -1
  2. package/template/client/package-lock.json +19 -0
  3. package/template/client/package.json +3 -0
  4. package/template/client/src/App.tsx +17 -0
  5. package/template/client/src/api.ts +135 -0
  6. package/template/client/src/components/AiSettingsModal.tsx +218 -4
  7. package/template/client/src/components/AnnotationDialog.tsx +3 -9
  8. package/template/client/src/components/ChatMessage.tsx +110 -27
  9. package/template/client/src/components/ChatView.tsx +69 -4
  10. package/template/client/src/components/CodeContextPanel.tsx +297 -0
  11. package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
  12. package/template/client/src/components/CodeRunnerModal.tsx +1549 -0
  13. package/template/client/src/components/DocRefModal.tsx +502 -0
  14. package/template/client/src/components/FileAttachments.tsx +109 -9
  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/MarkdownRenderer.tsx +205 -2
  18. package/template/client/src/components/Sidebar.tsx +213 -127
  19. package/template/client/src/components/TextAnnotator.tsx +8 -15
  20. package/template/client/src/components/VizCraftEmbed.tsx +162 -19
  21. package/template/client/src/store.ts +201 -0
  22. package/template/client/src/types.ts +8 -0
  23. package/template/cockpit.json +1 -1
  24. package/template/package.json +1 -1
  25. package/template/server/src/google-drive.ts +109 -1
  26. package/template/server/src/index.ts +1107 -46
  27. package/template/server/src/storage.ts +263 -2
@@ -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,42 @@ 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
+ ) {
362
+ try {
363
+ await fs.mkdir(ctxDir, { recursive: true });
364
+ await fs.writeFile(
365
+ path.join(ctxDir, cs.id),
366
+ Buffer.from(cs.content, "utf-8"),
367
+ );
368
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
369
+ const { content: _content, ...cfMeta } = cs;
370
+ restoredContextFiles.push(cfMeta);
371
+ } catch {
372
+ /* skip bad blobs */
373
+ }
374
+ }
375
+ },
376
+ ),
377
+ );
378
+ }
332
379
  } catch {
333
380
  // Malformed JSON — fall through to raw text
334
381
  systemContext = buffer.toString("utf-8");
@@ -344,18 +391,47 @@ export async function syncWorkspace(
344
391
  title,
345
392
  systemContext,
346
393
  codeContextFiles,
347
- contextFiles: [],
394
+ contextFiles: restoredContextFiles,
348
395
  messages,
396
+ codeAnnotations,
349
397
  createdAt: new Date().toISOString(),
350
398
  };
351
399
  await storage.saveQuestion(q);
352
400
  result.filesImported++;
401
+ if (parentTitle) {
402
+ parentLinks.push({ questionId: q.id, topicId, parentTitle });
403
+ }
353
404
  } catch (err: any) {
354
405
  result.errors.push(`${filename}: ${err?.message ?? "failed"}`);
355
406
  }
356
407
  }),
357
408
  );
358
409
 
410
+ // ── Phase 4b: re-link parent–child relationships by title ───────────────────
411
+ // IDs are regenerated on import, so we match parent by title within the same topic.
412
+ if (parentLinks.length > 0) {
413
+ // Group by topicId to avoid redundant getQuestionsByTopic calls
414
+ const byTopic = new Map<string, typeof parentLinks>();
415
+ for (const link of parentLinks) {
416
+ const arr = byTopic.get(link.topicId) ?? [];
417
+ arr.push(link);
418
+ byTopic.set(link.topicId, arr);
419
+ }
420
+ for (const [topicId, links] of byTopic) {
421
+ const allInTopic = await storage.getQuestionsByTopic(topicId);
422
+ for (const { questionId, parentTitle } of links) {
423
+ const parent = allInTopic.find(
424
+ (q) => q.title === parentTitle && q.id !== questionId,
425
+ );
426
+ const child = allInTopic.find((q) => q.id === questionId);
427
+ if (parent && child) {
428
+ child.parentQuestionId = parent.id;
429
+ await storage.saveQuestion(child);
430
+ }
431
+ }
432
+ }
433
+ }
434
+
359
435
  // ── Phase 5: save context file blobs in parallel, then update topics.json once ─
360
436
  if (contextFiles.length > 0) {
361
437
  // Write binary files in parallel
@@ -645,13 +721,45 @@ export async function exportWorkspace(
645
721
  try {
646
722
  const safeName =
647
723
  q.title.replace(/[/\\:*?"<>|]/g, "-").trim() || q.id;
724
+ // Look up parent title so the relationship can be restored on import
725
+ const parentTitle = q.parentQuestionId
726
+ ? questions.find((p) => p.id === q.parentQuestionId)?.title
727
+ : undefined;
728
+
729
+ // Read code snippet blobs so they survive drive sync
730
+ const ctxDir =
731
+ storage.getContextFilesDirForWorkspace(workspaceId);
732
+ const contextFilesWithContent: Array<
733
+ storage.ContextFile & { content?: string }
734
+ > = [];
735
+ for (const cf of q.contextFiles || []) {
736
+ if (
737
+ cf.origin === "user" ||
738
+ cf.origin === "ai" ||
739
+ cf.origin === "sandbox"
740
+ ) {
741
+ try {
742
+ const content = await fs.readFile(
743
+ path.join(ctxDir, cf.id),
744
+ "utf-8",
745
+ );
746
+ contextFilesWithContent.push({ ...cf, content });
747
+ } catch {
748
+ contextFilesWithContent.push(cf);
749
+ }
750
+ }
751
+ }
752
+
648
753
  // Export full question as JSON so nothing is lost on sync-back
649
754
  const payload = JSON.stringify(
650
755
  {
651
756
  title: q.title,
757
+ parentTitle: parentTitle ?? null,
652
758
  systemContext: q.systemContext || "",
653
759
  messages: q.messages,
654
760
  codeContextFiles: q.codeContextFiles,
761
+ codeAnnotations: q.codeAnnotations ?? {},
762
+ codeSnippets: contextFilesWithContent,
655
763
  },
656
764
  null,
657
765
  2,