create-interview-cockpit 0.5.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 +734 -1
- package/template/client/package.json +1 -0
- package/template/client/src/App.tsx +3 -0
- package/template/client/src/api.ts +321 -4
- package/template/client/src/components/AiSettingsModal.tsx +818 -425
- package/template/client/src/components/ChatMessage.tsx +34 -12
- package/template/client/src/components/ChatView.tsx +298 -121
- package/template/client/src/components/CodeContextPanel.tsx +419 -2
- package/template/client/src/components/CodeRunnerModal.tsx +1601 -120
- package/template/client/src/components/DocRefModal.tsx +55 -6
- package/template/client/src/components/FileAttachments.tsx +20 -4
- 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 +22 -8
- 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 +184 -0
- package/template/client/src/components/VizCraftEmbed.tsx +257 -13
- 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 +219 -6
- package/template/client/src/types.ts +35 -3
- package/template/client/tsconfig.tsbuildinfo +1 -1
- package/template/server/src/google-drive.ts +37 -3
- package/template/server/src/index.ts +693 -52
- package/template/server/src/infra-runner.ts +1104 -0
- package/template/server/src/storage.ts +13 -3
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { useEffect, useState, useCallback } from "react";
|
|
2
2
|
import { useStore } from "../store";
|
|
3
|
+
import { parseInfraLabWorkspace } from "../infraLab";
|
|
4
|
+
import { parseFrontendLabWorkspace } from "../reactLab";
|
|
3
5
|
import FileViewerModal from "./FileViewerModal";
|
|
6
|
+
import NotesModal, { notesKey } from "./NotesModal";
|
|
4
7
|
import {
|
|
5
8
|
File,
|
|
6
9
|
Search,
|
|
@@ -22,6 +25,10 @@ import {
|
|
|
22
25
|
Trash2,
|
|
23
26
|
Server,
|
|
24
27
|
Pencil,
|
|
28
|
+
NotebookPen,
|
|
29
|
+
Globe,
|
|
30
|
+
Atom,
|
|
31
|
+
Layout,
|
|
25
32
|
} from "lucide-react";
|
|
26
33
|
|
|
27
34
|
// ─── Tree data structure ─────────────────────────────────
|
|
@@ -228,6 +235,9 @@ export default function CodeContextPanel() {
|
|
|
228
235
|
clearSnippets,
|
|
229
236
|
openCodeRunner,
|
|
230
237
|
openSandbox,
|
|
238
|
+
openInfraLab,
|
|
239
|
+
openReactLab,
|
|
240
|
+
openNextLab,
|
|
231
241
|
removeQuestionFile,
|
|
232
242
|
renameContextFile,
|
|
233
243
|
} = useStore();
|
|
@@ -243,6 +253,19 @@ export default function CodeContextPanel() {
|
|
|
243
253
|
new Set(),
|
|
244
254
|
);
|
|
245
255
|
const [viewingFile, setViewingFile] = useState<string | null>(null);
|
|
256
|
+
const [notesOpen, setNotesOpen] = useState(false);
|
|
257
|
+
|
|
258
|
+
// Show a dot in the Notes button when there are saved notes for this context
|
|
259
|
+
const hasNotes = (() => {
|
|
260
|
+
try {
|
|
261
|
+
const raw = localStorage.getItem(notesKey(currentQuestion?.id));
|
|
262
|
+
if (!raw) return false;
|
|
263
|
+
const parsed = JSON.parse(raw);
|
|
264
|
+
return Array.isArray(parsed) && parsed.length > 0;
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
})();
|
|
246
269
|
|
|
247
270
|
useEffect(() => {
|
|
248
271
|
fetchAvailableFiles();
|
|
@@ -518,7 +541,11 @@ export default function CodeContextPanel() {
|
|
|
518
541
|
)
|
|
519
542
|
.then((r) => r.json())
|
|
520
543
|
.then((d) => d.content);
|
|
521
|
-
openCodeRunner(
|
|
544
|
+
openCodeRunner(
|
|
545
|
+
content,
|
|
546
|
+
cf.language ?? "typescript",
|
|
547
|
+
cf.id,
|
|
548
|
+
);
|
|
522
549
|
}}
|
|
523
550
|
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-emerald-400 transition-all"
|
|
524
551
|
title="Open in Code Runner"
|
|
@@ -583,7 +610,11 @@ export default function CodeContextPanel() {
|
|
|
583
610
|
)
|
|
584
611
|
.then((r) => r.json())
|
|
585
612
|
.then((d) => d.content);
|
|
586
|
-
openCodeRunner(
|
|
613
|
+
openCodeRunner(
|
|
614
|
+
content,
|
|
615
|
+
cf.language ?? "typescript",
|
|
616
|
+
cf.id,
|
|
617
|
+
);
|
|
587
618
|
}}
|
|
588
619
|
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
|
|
589
620
|
title="Open in Code Runner"
|
|
@@ -715,6 +746,9 @@ export default function CodeContextPanel() {
|
|
|
715
746
|
serverLang: string;
|
|
716
747
|
clientCode: string;
|
|
717
748
|
clientLang: string;
|
|
749
|
+
clientType?: "script" | "react" | "nextjs";
|
|
750
|
+
reactFiles?: Record<string, string>;
|
|
751
|
+
reactActiveFile?: string;
|
|
718
752
|
};
|
|
719
753
|
openSandbox(
|
|
720
754
|
parsed.serverCode,
|
|
@@ -722,6 +756,13 @@ export default function CodeContextPanel() {
|
|
|
722
756
|
parsed.clientCode,
|
|
723
757
|
parsed.clientLang,
|
|
724
758
|
cf.id,
|
|
759
|
+
parsed.clientType
|
|
760
|
+
? {
|
|
761
|
+
clientType: parsed.clientType,
|
|
762
|
+
reactFiles: parsed.reactFiles,
|
|
763
|
+
reactActiveFile: parsed.reactActiveFile,
|
|
764
|
+
}
|
|
765
|
+
: undefined,
|
|
725
766
|
);
|
|
726
767
|
} catch {
|
|
727
768
|
/* malformed — ignore */
|
|
@@ -756,12 +797,388 @@ export default function CodeContextPanel() {
|
|
|
756
797
|
</div>
|
|
757
798
|
)}
|
|
758
799
|
|
|
800
|
+
{currentQuestion && (
|
|
801
|
+
<div className="border-t border-slate-800 px-3 py-2">
|
|
802
|
+
<div className="flex items-center justify-between mb-1">
|
|
803
|
+
<div className="flex items-center gap-1">
|
|
804
|
+
<Globe className="w-3 h-3 text-cyan-400/70" />
|
|
805
|
+
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
806
|
+
Infra Labs (
|
|
807
|
+
{
|
|
808
|
+
(currentQuestion.contextFiles || []).filter(
|
|
809
|
+
(f) => f.origin === "infra",
|
|
810
|
+
).length
|
|
811
|
+
}
|
|
812
|
+
)
|
|
813
|
+
</span>
|
|
814
|
+
</div>
|
|
815
|
+
<button
|
|
816
|
+
onClick={() => openInfraLab()}
|
|
817
|
+
className="p-0.5 rounded text-slate-600 hover:text-cyan-400 hover:bg-slate-700 transition-colors"
|
|
818
|
+
title="Open Infrastructure Lab"
|
|
819
|
+
>
|
|
820
|
+
<Plus className="w-3.5 h-3.5" />
|
|
821
|
+
</button>
|
|
822
|
+
</div>
|
|
823
|
+
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
824
|
+
{(currentQuestion.contextFiles || [])
|
|
825
|
+
.filter((f) => f.origin === "infra")
|
|
826
|
+
.map((cf) => (
|
|
827
|
+
<div
|
|
828
|
+
key={cf.id}
|
|
829
|
+
className="flex items-center gap-1 text-xs bg-cyan-500/10 border border-cyan-500/20 rounded px-1.5 py-1 group"
|
|
830
|
+
>
|
|
831
|
+
{renamingId !== cf.id && (
|
|
832
|
+
<span
|
|
833
|
+
className="text-cyan-200 font-medium truncate flex-1"
|
|
834
|
+
title={cf.label || cf.originalName}
|
|
835
|
+
>
|
|
836
|
+
{cf.label || cf.originalName}
|
|
837
|
+
</span>
|
|
838
|
+
)}
|
|
839
|
+
{renamingId === cf.id ? (
|
|
840
|
+
<>
|
|
841
|
+
<input
|
|
842
|
+
autoFocus
|
|
843
|
+
value={renameValue}
|
|
844
|
+
onChange={(e) => setRenameValue(e.target.value)}
|
|
845
|
+
onKeyDown={async (e) => {
|
|
846
|
+
if (e.key === "Enter") {
|
|
847
|
+
e.preventDefault();
|
|
848
|
+
if (renameValue.trim()) {
|
|
849
|
+
await renameContextFile(
|
|
850
|
+
currentQuestion.id,
|
|
851
|
+
cf.id,
|
|
852
|
+
renameValue.trim(),
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
setRenamingId(null);
|
|
856
|
+
} else if (e.key === "Escape") {
|
|
857
|
+
setRenamingId(null);
|
|
858
|
+
}
|
|
859
|
+
}}
|
|
860
|
+
className="w-28 bg-slate-900 border border-cyan-600/50 rounded px-1.5 py-0.5 text-[11px] text-slate-200 outline-none focus:border-cyan-500 shrink-0"
|
|
861
|
+
/>
|
|
862
|
+
<button
|
|
863
|
+
onClick={async () => {
|
|
864
|
+
if (renameValue.trim()) {
|
|
865
|
+
await renameContextFile(
|
|
866
|
+
currentQuestion.id,
|
|
867
|
+
cf.id,
|
|
868
|
+
renameValue.trim(),
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
setRenamingId(null);
|
|
872
|
+
}}
|
|
873
|
+
className="shrink-0 p-0.5 rounded text-cyan-400 hover:bg-cyan-600/20 transition-colors"
|
|
874
|
+
title="Confirm"
|
|
875
|
+
>
|
|
876
|
+
<Check className="w-3 h-3" />
|
|
877
|
+
</button>
|
|
878
|
+
<button
|
|
879
|
+
onClick={() => setRenamingId(null)}
|
|
880
|
+
className="shrink-0 p-0.5 rounded text-slate-500 hover:text-slate-300 transition-colors"
|
|
881
|
+
title="Cancel"
|
|
882
|
+
>
|
|
883
|
+
<X className="w-3 h-3" />
|
|
884
|
+
</button>
|
|
885
|
+
</>
|
|
886
|
+
) : (
|
|
887
|
+
<>
|
|
888
|
+
<button
|
|
889
|
+
onClick={() => {
|
|
890
|
+
setRenamingId(cf.id);
|
|
891
|
+
setRenameValue(cf.label || cf.originalName);
|
|
892
|
+
}}
|
|
893
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-300 transition-all"
|
|
894
|
+
title="Rename"
|
|
895
|
+
>
|
|
896
|
+
<Pencil className="w-3 h-3" />
|
|
897
|
+
</button>
|
|
898
|
+
<button
|
|
899
|
+
onClick={async () => {
|
|
900
|
+
const raw = await fetch(
|
|
901
|
+
`/api/context-files/${cf.id}/content`,
|
|
902
|
+
)
|
|
903
|
+
.then((r) => r.json())
|
|
904
|
+
.then((d) => d.content as string);
|
|
905
|
+
const parsed = parseInfraLabWorkspace(raw);
|
|
906
|
+
if (parsed) openInfraLab(parsed, cf.id);
|
|
907
|
+
}}
|
|
908
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
|
|
909
|
+
title="Open in Infrastructure Lab"
|
|
910
|
+
>
|
|
911
|
+
<Play className="w-3 h-3" />
|
|
912
|
+
</button>
|
|
913
|
+
<button
|
|
914
|
+
onClick={() =>
|
|
915
|
+
removeQuestionFile(currentQuestion.id, cf.id)
|
|
916
|
+
}
|
|
917
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
918
|
+
title="Remove"
|
|
919
|
+
>
|
|
920
|
+
<Trash2 className="w-3 h-3" />
|
|
921
|
+
</button>
|
|
922
|
+
</>
|
|
923
|
+
)}
|
|
924
|
+
</div>
|
|
925
|
+
))}
|
|
926
|
+
{(currentQuestion.contextFiles || []).filter(
|
|
927
|
+
(f) => f.origin === "infra",
|
|
928
|
+
).length === 0 && (
|
|
929
|
+
<p className="text-[10px] text-slate-700 italic">
|
|
930
|
+
Save an infra lab to reopen it here
|
|
931
|
+
</p>
|
|
932
|
+
)}
|
|
933
|
+
</div>
|
|
934
|
+
</div>
|
|
935
|
+
)}
|
|
936
|
+
|
|
937
|
+
{/* ── React Labs section ───────────────────── */}
|
|
938
|
+
{currentQuestion && (
|
|
939
|
+
<div className="border-t border-slate-800 px-3 py-2">
|
|
940
|
+
<div className="flex items-center justify-between mb-1">
|
|
941
|
+
<div className="flex items-center gap-1">
|
|
942
|
+
<Atom className="w-3 h-3 text-cyan-400/70" />
|
|
943
|
+
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
944
|
+
React Labs (
|
|
945
|
+
{
|
|
946
|
+
(currentQuestion.contextFiles || []).filter(
|
|
947
|
+
(f) => f.origin === "react",
|
|
948
|
+
).length
|
|
949
|
+
}
|
|
950
|
+
)
|
|
951
|
+
</span>
|
|
952
|
+
</div>
|
|
953
|
+
<button
|
|
954
|
+
onClick={() => openReactLab()}
|
|
955
|
+
className="p-0.5 rounded text-slate-600 hover:text-cyan-400 hover:bg-slate-700 transition-colors"
|
|
956
|
+
title="Open React Lab"
|
|
957
|
+
>
|
|
958
|
+
<Plus className="w-3.5 h-3.5" />
|
|
959
|
+
</button>
|
|
960
|
+
</div>
|
|
961
|
+
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
962
|
+
{(currentQuestion.contextFiles || [])
|
|
963
|
+
.filter((f) => f.origin === "react")
|
|
964
|
+
.map((cf) => (
|
|
965
|
+
<div
|
|
966
|
+
key={cf.id}
|
|
967
|
+
className="flex items-center gap-1 text-xs bg-cyan-500/10 border border-cyan-500/20 rounded px-1.5 py-1 group"
|
|
968
|
+
>
|
|
969
|
+
<span
|
|
970
|
+
className="text-cyan-200 font-medium truncate flex-1"
|
|
971
|
+
title={cf.label || cf.originalName}
|
|
972
|
+
>
|
|
973
|
+
{cf.label || cf.originalName}
|
|
974
|
+
</span>
|
|
975
|
+
<button
|
|
976
|
+
onClick={async () => {
|
|
977
|
+
try {
|
|
978
|
+
const raw = await fetch(
|
|
979
|
+
`/api/context-files/${cf.id}/content`,
|
|
980
|
+
)
|
|
981
|
+
.then((r) => r.json())
|
|
982
|
+
.then((d) => d.content as string);
|
|
983
|
+
// Try new extended sandbox format first
|
|
984
|
+
const ext = JSON.parse(raw) as {
|
|
985
|
+
clientType?: string;
|
|
986
|
+
reactFiles?: Record<string, string>;
|
|
987
|
+
reactActiveFile?: string;
|
|
988
|
+
serverCode?: string;
|
|
989
|
+
serverLang?: string;
|
|
990
|
+
};
|
|
991
|
+
if (ext?.clientType === "react" && ext.reactFiles) {
|
|
992
|
+
openReactLab(
|
|
993
|
+
{
|
|
994
|
+
version: 1,
|
|
995
|
+
type: "react",
|
|
996
|
+
label: cf.label || "React Lab",
|
|
997
|
+
activeFile:
|
|
998
|
+
ext.reactActiveFile ??
|
|
999
|
+
Object.keys(ext.reactFiles)[0] ??
|
|
1000
|
+
"App.tsx",
|
|
1001
|
+
files: ext.reactFiles,
|
|
1002
|
+
},
|
|
1003
|
+
cf.id,
|
|
1004
|
+
ext.serverCode,
|
|
1005
|
+
ext.serverLang,
|
|
1006
|
+
);
|
|
1007
|
+
} else {
|
|
1008
|
+
// Fall back to old FrontendLabWorkspace format
|
|
1009
|
+
const ws = parseFrontendLabWorkspace(raw);
|
|
1010
|
+
if (ws) openReactLab(ws, cf.id);
|
|
1011
|
+
}
|
|
1012
|
+
} catch {
|
|
1013
|
+
/* ignore */
|
|
1014
|
+
}
|
|
1015
|
+
}}
|
|
1016
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
|
|
1017
|
+
title="Open in React Lab"
|
|
1018
|
+
>
|
|
1019
|
+
<Play className="w-3 h-3" />
|
|
1020
|
+
</button>
|
|
1021
|
+
<button
|
|
1022
|
+
onClick={() =>
|
|
1023
|
+
removeQuestionFile(currentQuestion.id, cf.id)
|
|
1024
|
+
}
|
|
1025
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
1026
|
+
title="Remove"
|
|
1027
|
+
>
|
|
1028
|
+
<Trash2 className="w-3 h-3" />
|
|
1029
|
+
</button>
|
|
1030
|
+
</div>
|
|
1031
|
+
))}
|
|
1032
|
+
{(currentQuestion.contextFiles || []).filter(
|
|
1033
|
+
(f) => f.origin === "react",
|
|
1034
|
+
).length === 0 && (
|
|
1035
|
+
<p className="text-[10px] text-slate-700 italic">
|
|
1036
|
+
Save a React lab to reopen it here
|
|
1037
|
+
</p>
|
|
1038
|
+
)}
|
|
1039
|
+
</div>
|
|
1040
|
+
</div>
|
|
1041
|
+
)}
|
|
1042
|
+
|
|
1043
|
+
{/* ── Next.js Labs section ──────────────────── */}
|
|
1044
|
+
{currentQuestion && (
|
|
1045
|
+
<div className="border-t border-slate-800 px-3 py-2">
|
|
1046
|
+
<div className="flex items-center justify-between mb-1">
|
|
1047
|
+
<div className="flex items-center gap-1">
|
|
1048
|
+
<Layout className="w-3 h-3 text-violet-400/70" />
|
|
1049
|
+
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
1050
|
+
Next.js Labs (
|
|
1051
|
+
{
|
|
1052
|
+
(currentQuestion.contextFiles || []).filter(
|
|
1053
|
+
(f) => f.origin === "nextjs",
|
|
1054
|
+
).length
|
|
1055
|
+
}
|
|
1056
|
+
)
|
|
1057
|
+
</span>
|
|
1058
|
+
</div>
|
|
1059
|
+
<button
|
|
1060
|
+
onClick={() => openNextLab()}
|
|
1061
|
+
className="p-0.5 rounded text-slate-600 hover:text-violet-400 hover:bg-slate-700 transition-colors"
|
|
1062
|
+
title="Open Next.js Lab"
|
|
1063
|
+
>
|
|
1064
|
+
<Plus className="w-3.5 h-3.5" />
|
|
1065
|
+
</button>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
1068
|
+
{(currentQuestion.contextFiles || [])
|
|
1069
|
+
.filter((f) => f.origin === "nextjs")
|
|
1070
|
+
.map((cf) => (
|
|
1071
|
+
<div
|
|
1072
|
+
key={cf.id}
|
|
1073
|
+
className="flex items-center gap-1 text-xs bg-violet-500/10 border border-violet-500/20 rounded px-1.5 py-1 group"
|
|
1074
|
+
>
|
|
1075
|
+
<span
|
|
1076
|
+
className="text-violet-200 font-medium truncate flex-1"
|
|
1077
|
+
title={cf.label || cf.originalName}
|
|
1078
|
+
>
|
|
1079
|
+
{cf.label || cf.originalName}
|
|
1080
|
+
</span>
|
|
1081
|
+
<button
|
|
1082
|
+
onClick={async () => {
|
|
1083
|
+
try {
|
|
1084
|
+
const raw = await fetch(
|
|
1085
|
+
`/api/context-files/${cf.id}/content`,
|
|
1086
|
+
)
|
|
1087
|
+
.then((r) => r.json())
|
|
1088
|
+
.then((d) => d.content as string);
|
|
1089
|
+
const ext = JSON.parse(raw) as {
|
|
1090
|
+
clientType?: string;
|
|
1091
|
+
reactFiles?: Record<string, string>;
|
|
1092
|
+
reactActiveFile?: string;
|
|
1093
|
+
serverCode?: string;
|
|
1094
|
+
serverLang?: string;
|
|
1095
|
+
};
|
|
1096
|
+
if (ext?.clientType === "nextjs" && ext.reactFiles) {
|
|
1097
|
+
openNextLab(
|
|
1098
|
+
{
|
|
1099
|
+
version: 1,
|
|
1100
|
+
type: "nextjs",
|
|
1101
|
+
label: cf.label || "Next.js Lab",
|
|
1102
|
+
activeFile:
|
|
1103
|
+
ext.reactActiveFile ??
|
|
1104
|
+
Object.keys(ext.reactFiles)[0] ??
|
|
1105
|
+
"app/page.tsx",
|
|
1106
|
+
files: ext.reactFiles,
|
|
1107
|
+
},
|
|
1108
|
+
cf.id,
|
|
1109
|
+
ext.serverCode,
|
|
1110
|
+
ext.serverLang,
|
|
1111
|
+
);
|
|
1112
|
+
} else {
|
|
1113
|
+
const ws = parseFrontendLabWorkspace(raw);
|
|
1114
|
+
if (ws) openNextLab(ws, cf.id);
|
|
1115
|
+
}
|
|
1116
|
+
} catch {
|
|
1117
|
+
/* ignore */
|
|
1118
|
+
}
|
|
1119
|
+
}}
|
|
1120
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
|
|
1121
|
+
title="Open in Next.js Lab"
|
|
1122
|
+
>
|
|
1123
|
+
<Play className="w-3 h-3" />
|
|
1124
|
+
</button>
|
|
1125
|
+
<button
|
|
1126
|
+
onClick={() =>
|
|
1127
|
+
removeQuestionFile(currentQuestion.id, cf.id)
|
|
1128
|
+
}
|
|
1129
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
1130
|
+
title="Remove"
|
|
1131
|
+
>
|
|
1132
|
+
<Trash2 className="w-3 h-3" />
|
|
1133
|
+
</button>
|
|
1134
|
+
</div>
|
|
1135
|
+
))}
|
|
1136
|
+
{(currentQuestion.contextFiles || []).filter(
|
|
1137
|
+
(f) => f.origin === "nextjs",
|
|
1138
|
+
).length === 0 && (
|
|
1139
|
+
<p className="text-[10px] text-slate-700 italic">
|
|
1140
|
+
Save a Next.js lab to reopen it here
|
|
1141
|
+
</p>
|
|
1142
|
+
)}
|
|
1143
|
+
</div>
|
|
1144
|
+
</div>
|
|
1145
|
+
)}
|
|
1146
|
+
|
|
1147
|
+
{/* ── Notes section ────────────────────────────────────── */}
|
|
1148
|
+
<div className="border-t border-slate-800 px-3 py-2">
|
|
1149
|
+
<button
|
|
1150
|
+
onClick={() => setNotesOpen((v) => !v)}
|
|
1151
|
+
className="w-full flex items-center gap-2 group"
|
|
1152
|
+
>
|
|
1153
|
+
<NotebookPen className="w-3 h-3 text-amber-400/70 shrink-0" />
|
|
1154
|
+
<span className="text-[10px] uppercase tracking-wider text-slate-600 flex-1 text-left">
|
|
1155
|
+
Notes
|
|
1156
|
+
</span>
|
|
1157
|
+
{hasNotes && (
|
|
1158
|
+
<span
|
|
1159
|
+
className="w-1.5 h-1.5 rounded-full bg-amber-400/70 shrink-0"
|
|
1160
|
+
title="Has notes"
|
|
1161
|
+
/>
|
|
1162
|
+
)}
|
|
1163
|
+
<span className="text-[10px] text-slate-700 group-hover:text-slate-500 transition-colors">
|
|
1164
|
+
{notesOpen ? "close" : "open"}
|
|
1165
|
+
</span>
|
|
1166
|
+
</button>
|
|
1167
|
+
</div>
|
|
1168
|
+
|
|
759
1169
|
{viewingFile && (
|
|
760
1170
|
<FileViewerModal
|
|
761
1171
|
filePath={viewingFile}
|
|
762
1172
|
onClose={() => setViewingFile(null)}
|
|
763
1173
|
/>
|
|
764
1174
|
)}
|
|
1175
|
+
|
|
1176
|
+
{notesOpen && (
|
|
1177
|
+
<NotesModal
|
|
1178
|
+
questionId={currentQuestion?.id}
|
|
1179
|
+
onClose={() => setNotesOpen(false)}
|
|
1180
|
+
/>
|
|
1181
|
+
)}
|
|
765
1182
|
</div>
|
|
766
1183
|
);
|
|
767
1184
|
}
|