create-interview-cockpit 0.3.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 (28) hide show
  1. package/README.md +23 -0
  2. package/package.json +1 -1
  3. package/template/client/package-lock.json +42 -0
  4. package/template/client/package.json +5 -0
  5. package/template/client/src/App.tsx +45 -12
  6. package/template/client/src/api.ts +174 -0
  7. package/template/client/src/components/AiSettingsModal.tsx +1041 -0
  8. package/template/client/src/components/AnnotationDialog.tsx +3 -9
  9. package/template/client/src/components/ChatMessage.tsx +110 -27
  10. package/template/client/src/components/ChatView.tsx +239 -137
  11. package/template/client/src/components/CodeContextPanel.tsx +297 -0
  12. package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
  13. package/template/client/src/components/CodeRunnerModal.tsx +1549 -0
  14. package/template/client/src/components/DocRefModal.tsx +502 -0
  15. package/template/client/src/components/FileAttachments.tsx +109 -9
  16. package/template/client/src/components/FilePickerModal.tsx +181 -0
  17. package/template/client/src/components/FileViewerModal.tsx +406 -28
  18. package/template/client/src/components/MarkdownRenderer.tsx +210 -2
  19. package/template/client/src/components/Sidebar.tsx +213 -125
  20. package/template/client/src/components/TextAnnotator.tsx +8 -15
  21. package/template/client/src/components/VizCraftEmbed.tsx +645 -0
  22. package/template/client/src/store.ts +275 -0
  23. package/template/client/src/types.ts +9 -0
  24. package/template/cockpit.json +1 -1
  25. package/template/data/ai-settings.json +49 -0
  26. package/template/server/src/google-drive.ts +109 -1
  27. package/template/server/src/index.ts +1187 -76
  28. package/template/server/src/storage.ts +359 -2
@@ -15,6 +15,13 @@ import {
15
15
  MinusSquare,
16
16
  Eye,
17
17
  Scissors,
18
+ Terminal,
19
+ Sparkles,
20
+ Plus,
21
+ Play,
22
+ Trash2,
23
+ Server,
24
+ Pencil,
18
25
  } from "lucide-react";
19
26
 
20
27
  // ─── Tree data structure ─────────────────────────────────
@@ -219,8 +226,15 @@ export default function CodeContextPanel() {
219
226
  codeSnippets,
220
227
  removeSnippet,
221
228
  clearSnippets,
229
+ openCodeRunner,
230
+ openSandbox,
231
+ removeQuestionFile,
232
+ renameContextFile,
222
233
  } = useStore();
223
234
 
235
+ const [renamingId, setRenamingId] = useState<string | null>(null);
236
+ const [renameValue, setRenameValue] = useState("");
237
+
224
238
  const [search, setSearch] = useState("");
225
239
  const [selectedFiles, setSelectedFiles] = useState<string[]>(
226
240
  currentQuestion?.codeContextFiles || [],
@@ -459,6 +473,289 @@ export default function CodeContextPanel() {
459
473
  })}
460
474
  </div>
461
475
 
476
+ {/* ── My Code section ─────────────────────────────────── */}
477
+ {currentQuestion && (
478
+ <div className="border-t border-slate-800 px-3 py-2">
479
+ <div className="flex items-center justify-between mb-1">
480
+ <div className="flex items-center gap-1">
481
+ <Terminal className="w-3 h-3 text-emerald-400/70" />
482
+ <span className="text-[10px] uppercase tracking-wider text-slate-600">
483
+ My Code (
484
+ {
485
+ (currentQuestion.contextFiles || []).filter(
486
+ (f) => f.origin === "user",
487
+ ).length
488
+ }
489
+ )
490
+ </span>
491
+ </div>
492
+ <button
493
+ onClick={() => openCodeRunner()}
494
+ className="p-0.5 rounded text-slate-600 hover:text-emerald-400 hover:bg-slate-700 transition-colors"
495
+ title="Open Code Runner"
496
+ >
497
+ <Plus className="w-3.5 h-3.5" />
498
+ </button>
499
+ </div>
500
+ <div className="space-y-0.5 max-h-32 overflow-y-auto">
501
+ {(currentQuestion.contextFiles || [])
502
+ .filter((f) => f.origin === "user")
503
+ .map((cf) => (
504
+ <div
505
+ key={cf.id}
506
+ className="flex items-center gap-1 text-xs bg-emerald-500/10 border border-emerald-500/20 rounded px-1.5 py-1 group"
507
+ >
508
+ <span
509
+ className="text-emerald-400 font-medium truncate flex-1"
510
+ title={cf.label || cf.originalName}
511
+ >
512
+ {cf.label || cf.originalName}
513
+ </span>
514
+ <button
515
+ onClick={async () => {
516
+ const content = await fetch(
517
+ `/api/context-files/${cf.id}/content`,
518
+ )
519
+ .then((r) => r.json())
520
+ .then((d) => d.content);
521
+ openCodeRunner(content, cf.language ?? "typescript");
522
+ }}
523
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-emerald-400 transition-all"
524
+ title="Open in Code Runner"
525
+ >
526
+ <Play className="w-3 h-3" />
527
+ </button>
528
+ <button
529
+ onClick={() =>
530
+ removeQuestionFile(currentQuestion.id, cf.id)
531
+ }
532
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
533
+ title="Remove"
534
+ >
535
+ <Trash2 className="w-3 h-3" />
536
+ </button>
537
+ </div>
538
+ ))}
539
+ {(currentQuestion.contextFiles || []).filter(
540
+ (f) => f.origin === "user",
541
+ ).length === 0 && (
542
+ <p className="text-[10px] text-slate-700 italic">
543
+ Save code from the runner to see it here
544
+ </p>
545
+ )}
546
+ </div>
547
+ </div>
548
+ )}
549
+
550
+ {/* ── AI Generated section ──────────────────────────── */}
551
+ {currentQuestion && (
552
+ <div className="border-t border-slate-800 px-3 py-2">
553
+ <div className="flex items-center gap-1 mb-1">
554
+ <Sparkles className="w-3 h-3 text-violet-400/70" />
555
+ <span className="text-[10px] uppercase tracking-wider text-slate-600">
556
+ AI Generated (
557
+ {
558
+ (currentQuestion.contextFiles || []).filter(
559
+ (f) => f.origin === "ai",
560
+ ).length
561
+ }
562
+ )
563
+ </span>
564
+ </div>
565
+ <div className="space-y-0.5 max-h-32 overflow-y-auto">
566
+ {(currentQuestion.contextFiles || [])
567
+ .filter((f) => f.origin === "ai")
568
+ .map((cf) => (
569
+ <div
570
+ key={cf.id}
571
+ className="flex items-center gap-1 text-xs bg-violet-500/10 border border-violet-500/20 rounded px-1.5 py-1 group"
572
+ >
573
+ <span
574
+ className="text-violet-300 font-medium truncate flex-1"
575
+ title={cf.label || cf.originalName}
576
+ >
577
+ {cf.label || cf.originalName}
578
+ </span>
579
+ <button
580
+ onClick={async () => {
581
+ const content = await fetch(
582
+ `/api/context-files/${cf.id}/content`,
583
+ )
584
+ .then((r) => r.json())
585
+ .then((d) => d.content);
586
+ openCodeRunner(content, cf.language ?? "typescript");
587
+ }}
588
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
589
+ title="Open in Code Runner"
590
+ >
591
+ <Play className="w-3 h-3" />
592
+ </button>
593
+ <button
594
+ onClick={() =>
595
+ removeQuestionFile(currentQuestion.id, cf.id)
596
+ }
597
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
598
+ title="Remove"
599
+ >
600
+ <Trash2 className="w-3 h-3" />
601
+ </button>
602
+ </div>
603
+ ))}
604
+ {(currentQuestion.contextFiles || []).filter(
605
+ (f) => f.origin === "ai",
606
+ ).length === 0 && (
607
+ <p className="text-[10px] text-slate-700 italic">
608
+ Save AI code blocks to see them here
609
+ </p>
610
+ )}
611
+ </div>
612
+ </div>
613
+ )}
614
+
615
+ {/* ── Sandboxes section ───────────────────────── */}
616
+ {currentQuestion && (
617
+ <div className="border-t border-slate-800 px-3 py-2">
618
+ <div className="flex items-center gap-1 mb-1">
619
+ <Server className="w-3 h-3 text-slate-500/70" />
620
+ <span className="text-[10px] uppercase tracking-wider text-slate-600">
621
+ Sandboxes (
622
+ {
623
+ (currentQuestion.contextFiles || []).filter(
624
+ (f) => f.origin === "sandbox",
625
+ ).length
626
+ }
627
+ )
628
+ </span>
629
+ </div>
630
+ <div className="space-y-0.5 max-h-32 overflow-y-auto">
631
+ {(currentQuestion.contextFiles || [])
632
+ .filter((f) => f.origin === "sandbox")
633
+ .map((cf) => (
634
+ <div
635
+ key={cf.id}
636
+ className="flex items-center gap-1 text-xs bg-slate-500/10 border border-slate-500/20 rounded px-1.5 py-1 group"
637
+ >
638
+ {renamingId !== cf.id && (
639
+ <span
640
+ className="text-slate-300 font-medium truncate flex-1"
641
+ title={cf.label || cf.originalName}
642
+ >
643
+ {cf.label || cf.originalName}
644
+ </span>
645
+ )}
646
+ {renamingId === cf.id ? (
647
+ <>
648
+ <input
649
+ autoFocus
650
+ value={renameValue}
651
+ onChange={(e) => setRenameValue(e.target.value)}
652
+ onKeyDown={async (e) => {
653
+ if (e.key === "Enter") {
654
+ e.preventDefault();
655
+ if (renameValue.trim()) {
656
+ await renameContextFile(
657
+ currentQuestion.id,
658
+ cf.id,
659
+ renameValue.trim(),
660
+ );
661
+ }
662
+ setRenamingId(null);
663
+ } else if (e.key === "Escape") {
664
+ setRenamingId(null);
665
+ }
666
+ }}
667
+ className="w-28 bg-slate-900 border border-violet-600/50 rounded px-1.5 py-0.5 text-[11px] text-slate-200 outline-none focus:border-violet-500 shrink-0"
668
+ />
669
+ <button
670
+ onClick={async () => {
671
+ if (renameValue.trim()) {
672
+ await renameContextFile(
673
+ currentQuestion.id,
674
+ cf.id,
675
+ renameValue.trim(),
676
+ );
677
+ }
678
+ setRenamingId(null);
679
+ }}
680
+ className="shrink-0 p-0.5 rounded text-violet-400 hover:bg-violet-600/20 transition-colors"
681
+ title="Confirm"
682
+ >
683
+ <Check className="w-3 h-3" />
684
+ </button>
685
+ <button
686
+ onClick={() => setRenamingId(null)}
687
+ className="shrink-0 p-0.5 rounded text-slate-500 hover:text-slate-300 transition-colors"
688
+ title="Cancel"
689
+ >
690
+ <X className="w-3 h-3" />
691
+ </button>
692
+ </>
693
+ ) : (
694
+ <>
695
+ <button
696
+ onClick={() => {
697
+ setRenamingId(cf.id);
698
+ setRenameValue(cf.label || cf.originalName);
699
+ }}
700
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-300 transition-all"
701
+ title="Rename"
702
+ >
703
+ <Pencil className="w-3 h-3" />
704
+ </button>
705
+ <button
706
+ onClick={async () => {
707
+ try {
708
+ const raw = await fetch(
709
+ `/api/context-files/${cf.id}/content`,
710
+ )
711
+ .then((r) => r.json())
712
+ .then((d) => d.content as string);
713
+ const parsed = JSON.parse(raw) as {
714
+ serverCode: string;
715
+ serverLang: string;
716
+ clientCode: string;
717
+ clientLang: string;
718
+ };
719
+ openSandbox(
720
+ parsed.serverCode,
721
+ parsed.serverLang,
722
+ parsed.clientCode,
723
+ parsed.clientLang,
724
+ cf.id,
725
+ );
726
+ } catch {
727
+ /* malformed — ignore */
728
+ }
729
+ }}
730
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
731
+ title="Open in Sandbox"
732
+ >
733
+ <Play className="w-3 h-3" />
734
+ </button>
735
+ <button
736
+ onClick={() =>
737
+ removeQuestionFile(currentQuestion.id, cf.id)
738
+ }
739
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
740
+ title="Remove"
741
+ >
742
+ <Trash2 className="w-3 h-3" />
743
+ </button>
744
+ </>
745
+ )}
746
+ </div>
747
+ ))}
748
+ {(currentQuestion.contextFiles || []).filter(
749
+ (f) => f.origin === "sandbox",
750
+ ).length === 0 && (
751
+ <p className="text-[10px] text-slate-700 italic">
752
+ Save a sandbox to see it here
753
+ </p>
754
+ )}
755
+ </div>
756
+ </div>
757
+ )}
758
+
462
759
  {viewingFile && (
463
760
  <FileViewerModal
464
761
  filePath={viewingFile}
@@ -0,0 +1,179 @@
1
+ import { useState, useRef, useCallback, useEffect } from "react";
2
+ import { X, Code2 } from "lucide-react";
3
+ import MarkdownRenderer from "./MarkdownRenderer";
4
+
5
+ export interface CodeAnnotation {
6
+ id: string;
7
+ lineNumber: number;
8
+ lineContent: string;
9
+ prompt: string;
10
+ response: string;
11
+ filePath: string;
12
+ }
13
+
14
+ interface Props {
15
+ annotation: CodeAnnotation;
16
+ onClose: () => void;
17
+ initialPos?: { x: number; y: number };
18
+ }
19
+
20
+ const DEFAULT_W = 400;
21
+ const DEFAULT_H = 300;
22
+ const MIN_W = 280;
23
+ const MIN_H = 160;
24
+
25
+ export default function CodeLineAnnotationPopup({
26
+ annotation,
27
+ onClose,
28
+ initialPos,
29
+ }: Props) {
30
+ const [pos, setPos] = useState(() => ({
31
+ x:
32
+ initialPos?.x ??
33
+ Math.min(window.innerWidth - DEFAULT_W - 16, window.innerWidth * 0.6),
34
+ y: initialPos?.y ?? Math.max(8, window.innerHeight * 0.3),
35
+ }));
36
+ const [size, setSize] = useState({ w: DEFAULT_W, h: DEFAULT_H });
37
+
38
+ const dragStart = useRef<{
39
+ mx: number;
40
+ my: number;
41
+ ox: number;
42
+ oy: number;
43
+ } | null>(null);
44
+ const resizeStart = useRef<{
45
+ mx: number;
46
+ my: number;
47
+ ox: number;
48
+ oy: number;
49
+ ow: number;
50
+ oh: number;
51
+ } | null>(null);
52
+ const resizeDir = useRef<"e" | "s" | "se" | null>(null);
53
+
54
+ useEffect(() => {
55
+ const onMove = (e: MouseEvent) => {
56
+ if (dragStart.current) {
57
+ setPos({
58
+ x: Math.max(
59
+ 0,
60
+ dragStart.current.ox + (e.clientX - dragStart.current.mx),
61
+ ),
62
+ y: Math.max(
63
+ 0,
64
+ dragStart.current.oy + (e.clientY - dragStart.current.my),
65
+ ),
66
+ });
67
+ }
68
+ if (resizeStart.current && resizeDir.current) {
69
+ const dx = e.clientX - resizeStart.current.mx;
70
+ const dy = e.clientY - resizeStart.current.my;
71
+ setSize((prev) => ({
72
+ w: resizeDir.current?.includes("e")
73
+ ? Math.max(MIN_W, resizeStart.current!.ow + dx)
74
+ : prev.w,
75
+ h: resizeDir.current?.includes("s")
76
+ ? Math.max(MIN_H, resizeStart.current!.oh + dy)
77
+ : prev.h,
78
+ }));
79
+ }
80
+ };
81
+ const onUp = () => {
82
+ dragStart.current = null;
83
+ resizeStart.current = null;
84
+ resizeDir.current = null;
85
+ };
86
+ document.addEventListener("mousemove", onMove);
87
+ document.addEventListener("mouseup", onUp);
88
+ return () => {
89
+ document.removeEventListener("mousemove", onMove);
90
+ document.removeEventListener("mouseup", onUp);
91
+ };
92
+ }, []);
93
+
94
+ const onTitleMouseDown = useCallback(
95
+ (e: React.MouseEvent) => {
96
+ e.preventDefault();
97
+ dragStart.current = {
98
+ mx: e.clientX,
99
+ my: e.clientY,
100
+ ox: pos.x,
101
+ oy: pos.y,
102
+ };
103
+ },
104
+ [pos],
105
+ );
106
+
107
+ const startResize = (dir: "e" | "s" | "se") => (e: React.MouseEvent) => {
108
+ e.preventDefault();
109
+ e.stopPropagation();
110
+ resizeDir.current = dir;
111
+ resizeStart.current = {
112
+ mx: e.clientX,
113
+ my: e.clientY,
114
+ ox: pos.x,
115
+ oy: pos.y,
116
+ ow: size.w,
117
+ oh: size.h,
118
+ };
119
+ };
120
+
121
+ const fileName = annotation.filePath.split("/").pop() ?? annotation.filePath;
122
+ const linePreview =
123
+ annotation.lineContent.trim().length > 55
124
+ ? annotation.lineContent.trim().slice(0, 55) + "…"
125
+ : annotation.lineContent.trim();
126
+
127
+ return (
128
+ <div
129
+ className="fixed z-[65] flex flex-col bg-slate-900 border border-violet-500/40 rounded-xl shadow-2xl overflow-hidden"
130
+ style={{ left: pos.x, top: pos.y, width: size.w, height: size.h }}
131
+ >
132
+ {/* Title bar */}
133
+ <div
134
+ className="flex items-start gap-2 px-3 py-2 bg-slate-800/90 border-b border-violet-700/30 cursor-move select-none shrink-0"
135
+ onMouseDown={onTitleMouseDown}
136
+ >
137
+ <Code2 className="w-3.5 h-3.5 text-violet-400 shrink-0 mt-0.5" />
138
+ <div className="flex-1 min-w-0">
139
+ <p className="text-xs font-medium text-slate-200 truncate">
140
+ {annotation.prompt}
141
+ </p>
142
+ <p className="text-[10px] text-slate-500 font-mono truncate mt-0.5">
143
+ {fileName}
144
+ <span className="text-violet-500/70">:{annotation.lineNumber}</span>
145
+ {linePreview && (
146
+ <span className="text-slate-600"> — {linePreview}</span>
147
+ )}
148
+ </p>
149
+ </div>
150
+ <button
151
+ onMouseDown={(e) => e.stopPropagation()}
152
+ onClick={onClose}
153
+ className="p-1 rounded hover:bg-slate-700 text-slate-500 hover:text-slate-300 transition-colors shrink-0"
154
+ >
155
+ <X className="w-3.5 h-3.5" />
156
+ </button>
157
+ </div>
158
+
159
+ {/* Body */}
160
+ <div className="flex-1 overflow-y-auto px-4 py-3 text-sm min-h-0">
161
+ <MarkdownRenderer content={annotation.response} />
162
+ </div>
163
+
164
+ {/* Resize handles */}
165
+ <div
166
+ className="absolute inset-y-0 right-0 w-1.5 cursor-ew-resize"
167
+ onMouseDown={startResize("e")}
168
+ />
169
+ <div
170
+ className="absolute inset-x-0 bottom-0 h-1.5 cursor-ns-resize"
171
+ onMouseDown={startResize("s")}
172
+ />
173
+ <div
174
+ className="absolute bottom-0 right-0 w-3 h-3 cursor-se-resize z-10"
175
+ onMouseDown={startResize("se")}
176
+ />
177
+ </div>
178
+ );
179
+ }