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