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.
- package/README.md +23 -0
- package/package.json +1 -1
- package/template/client/package-lock.json +42 -0
- package/template/client/package.json +5 -0
- package/template/client/src/App.tsx +45 -12
- package/template/client/src/api.ts +174 -0
- package/template/client/src/components/AiSettingsModal.tsx +1041 -0
- package/template/client/src/components/AnnotationDialog.tsx +3 -9
- package/template/client/src/components/ChatMessage.tsx +110 -27
- package/template/client/src/components/ChatView.tsx +239 -137
- package/template/client/src/components/CodeContextPanel.tsx +297 -0
- package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
- package/template/client/src/components/CodeRunnerModal.tsx +1549 -0
- package/template/client/src/components/DocRefModal.tsx +502 -0
- package/template/client/src/components/FileAttachments.tsx +109 -9
- package/template/client/src/components/FilePickerModal.tsx +181 -0
- package/template/client/src/components/FileViewerModal.tsx +406 -28
- package/template/client/src/components/MarkdownRenderer.tsx +210 -2
- package/template/client/src/components/Sidebar.tsx +213 -125
- package/template/client/src/components/TextAnnotator.tsx +8 -15
- package/template/client/src/components/VizCraftEmbed.tsx +645 -0
- package/template/client/src/store.ts +275 -0
- package/template/client/src/types.ts +9 -0
- package/template/cockpit.json +1 -1
- package/template/data/ai-settings.json +49 -0
- package/template/server/src/google-drive.ts +109 -1
- package/template/server/src/index.ts +1187 -76
- 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
|
+
}
|