create-interview-cockpit 0.12.0 → 0.13.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/src/App.tsx +30 -1
- package/template/client/src/api.ts +22 -0
- package/template/client/src/components/CodeContextPanel.tsx +0 -622
- package/template/client/src/components/DeploymentLabModal.tsx +1941 -0
- package/template/client/src/components/LabsPanel.tsx +565 -0
- package/template/client/src/components/Sidebar.tsx +97 -55
- package/template/client/src/store.ts +52 -1
- package/template/client/src/types.ts +2 -0
- package/template/cockpit.json +1 -1
- package/template/server/src/index.ts +35 -1
- package/template/server/src/storage.ts +31 -0
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState, useCallback } from "react";
|
|
2
2
|
import { useStore } from "../store";
|
|
3
|
-
import { parseInfraLabWorkspace } from "../infraLab";
|
|
4
|
-
import { parseFrontendLabWorkspace } from "../reactLab";
|
|
5
3
|
import FileViewerModal from "./FileViewerModal";
|
|
6
4
|
import NotesModal, { notesKey } from "./NotesModal";
|
|
7
5
|
import {
|
|
@@ -23,12 +21,8 @@ import {
|
|
|
23
21
|
Plus,
|
|
24
22
|
Play,
|
|
25
23
|
Trash2,
|
|
26
|
-
Server,
|
|
27
24
|
Pencil,
|
|
28
25
|
NotebookPen,
|
|
29
|
-
Globe,
|
|
30
|
-
Atom,
|
|
31
|
-
Layout,
|
|
32
26
|
} from "lucide-react";
|
|
33
27
|
|
|
34
28
|
// ─── Tree data structure ─────────────────────────────────
|
|
@@ -234,11 +228,6 @@ export default function CodeContextPanel() {
|
|
|
234
228
|
removeSnippet,
|
|
235
229
|
clearSnippets,
|
|
236
230
|
openCodeRunner,
|
|
237
|
-
openSandbox,
|
|
238
|
-
openInfraLab,
|
|
239
|
-
openReactLab,
|
|
240
|
-
openNextLab,
|
|
241
|
-
openModuleFederationLab,
|
|
242
231
|
removeQuestionFile,
|
|
243
232
|
renameContextFile,
|
|
244
233
|
} = useStore();
|
|
@@ -646,617 +635,6 @@ export default function CodeContextPanel() {
|
|
|
646
635
|
</div>
|
|
647
636
|
)}
|
|
648
637
|
|
|
649
|
-
{/* ── Sandboxes section ───────────────────────── */}
|
|
650
|
-
{currentQuestion && (
|
|
651
|
-
<div className="border-t border-slate-800 px-3 py-2">
|
|
652
|
-
<div className="flex items-center gap-1 mb-1">
|
|
653
|
-
<Server className="w-3 h-3 text-slate-500/70" />
|
|
654
|
-
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
655
|
-
Sandboxes (
|
|
656
|
-
{
|
|
657
|
-
(currentQuestion.contextFiles || []).filter(
|
|
658
|
-
(f) => f.origin === "sandbox",
|
|
659
|
-
).length
|
|
660
|
-
}
|
|
661
|
-
)
|
|
662
|
-
</span>
|
|
663
|
-
</div>
|
|
664
|
-
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
665
|
-
{(currentQuestion.contextFiles || [])
|
|
666
|
-
.filter((f) => f.origin === "sandbox")
|
|
667
|
-
.map((cf) => (
|
|
668
|
-
<div
|
|
669
|
-
key={cf.id}
|
|
670
|
-
className="flex items-center gap-1 text-xs bg-slate-500/10 border border-slate-500/20 rounded px-1.5 py-1 group"
|
|
671
|
-
>
|
|
672
|
-
{renamingId !== cf.id && (
|
|
673
|
-
<span
|
|
674
|
-
className="text-slate-300 font-medium truncate flex-1"
|
|
675
|
-
title={cf.label || cf.originalName}
|
|
676
|
-
>
|
|
677
|
-
{cf.label || cf.originalName}
|
|
678
|
-
</span>
|
|
679
|
-
)}
|
|
680
|
-
{renamingId === cf.id ? (
|
|
681
|
-
<>
|
|
682
|
-
<input
|
|
683
|
-
autoFocus
|
|
684
|
-
value={renameValue}
|
|
685
|
-
onChange={(e) => setRenameValue(e.target.value)}
|
|
686
|
-
onKeyDown={async (e) => {
|
|
687
|
-
if (e.key === "Enter") {
|
|
688
|
-
e.preventDefault();
|
|
689
|
-
if (renameValue.trim()) {
|
|
690
|
-
await renameContextFile(
|
|
691
|
-
currentQuestion.id,
|
|
692
|
-
cf.id,
|
|
693
|
-
renameValue.trim(),
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
setRenamingId(null);
|
|
697
|
-
} else if (e.key === "Escape") {
|
|
698
|
-
setRenamingId(null);
|
|
699
|
-
}
|
|
700
|
-
}}
|
|
701
|
-
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"
|
|
702
|
-
/>
|
|
703
|
-
<button
|
|
704
|
-
onClick={async () => {
|
|
705
|
-
if (renameValue.trim()) {
|
|
706
|
-
await renameContextFile(
|
|
707
|
-
currentQuestion.id,
|
|
708
|
-
cf.id,
|
|
709
|
-
renameValue.trim(),
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
|
-
setRenamingId(null);
|
|
713
|
-
}}
|
|
714
|
-
className="shrink-0 p-0.5 rounded text-violet-400 hover:bg-violet-600/20 transition-colors"
|
|
715
|
-
title="Confirm"
|
|
716
|
-
>
|
|
717
|
-
<Check className="w-3 h-3" />
|
|
718
|
-
</button>
|
|
719
|
-
<button
|
|
720
|
-
onClick={() => setRenamingId(null)}
|
|
721
|
-
className="shrink-0 p-0.5 rounded text-slate-500 hover:text-slate-300 transition-colors"
|
|
722
|
-
title="Cancel"
|
|
723
|
-
>
|
|
724
|
-
<X className="w-3 h-3" />
|
|
725
|
-
</button>
|
|
726
|
-
</>
|
|
727
|
-
) : (
|
|
728
|
-
<>
|
|
729
|
-
<button
|
|
730
|
-
onClick={() => {
|
|
731
|
-
setRenamingId(cf.id);
|
|
732
|
-
setRenameValue(cf.label || cf.originalName);
|
|
733
|
-
}}
|
|
734
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-300 transition-all"
|
|
735
|
-
title="Rename"
|
|
736
|
-
>
|
|
737
|
-
<Pencil className="w-3 h-3" />
|
|
738
|
-
</button>
|
|
739
|
-
<button
|
|
740
|
-
onClick={async () => {
|
|
741
|
-
try {
|
|
742
|
-
const raw = await fetch(
|
|
743
|
-
`/api/context-files/${cf.id}/content`,
|
|
744
|
-
)
|
|
745
|
-
.then((r) => r.json())
|
|
746
|
-
.then((d) => d.content as string);
|
|
747
|
-
const parsed = JSON.parse(raw) as {
|
|
748
|
-
serverCode: string;
|
|
749
|
-
serverLang: string;
|
|
750
|
-
clientCode: string;
|
|
751
|
-
clientLang: string;
|
|
752
|
-
clientType?: "script" | "react" | "nextjs";
|
|
753
|
-
reactFiles?: Record<string, string>;
|
|
754
|
-
reactActiveFile?: string;
|
|
755
|
-
};
|
|
756
|
-
openSandbox(
|
|
757
|
-
parsed.serverCode,
|
|
758
|
-
parsed.serverLang,
|
|
759
|
-
parsed.clientCode,
|
|
760
|
-
parsed.clientLang,
|
|
761
|
-
cf.id,
|
|
762
|
-
parsed.clientType
|
|
763
|
-
? {
|
|
764
|
-
clientType: parsed.clientType,
|
|
765
|
-
reactFiles: parsed.reactFiles,
|
|
766
|
-
reactActiveFile: parsed.reactActiveFile,
|
|
767
|
-
}
|
|
768
|
-
: undefined,
|
|
769
|
-
);
|
|
770
|
-
} catch {
|
|
771
|
-
/* malformed — ignore */
|
|
772
|
-
}
|
|
773
|
-
}}
|
|
774
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
|
|
775
|
-
title="Open in Sandbox"
|
|
776
|
-
>
|
|
777
|
-
<Play className="w-3 h-3" />
|
|
778
|
-
</button>
|
|
779
|
-
<button
|
|
780
|
-
onClick={() =>
|
|
781
|
-
removeQuestionFile(currentQuestion.id, cf.id)
|
|
782
|
-
}
|
|
783
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
784
|
-
title="Remove"
|
|
785
|
-
>
|
|
786
|
-
<Trash2 className="w-3 h-3" />
|
|
787
|
-
</button>
|
|
788
|
-
</>
|
|
789
|
-
)}
|
|
790
|
-
</div>
|
|
791
|
-
))}
|
|
792
|
-
{(currentQuestion.contextFiles || []).filter(
|
|
793
|
-
(f) => f.origin === "sandbox",
|
|
794
|
-
).length === 0 && (
|
|
795
|
-
<p className="text-[10px] text-slate-700 italic">
|
|
796
|
-
Save a sandbox to see it here
|
|
797
|
-
</p>
|
|
798
|
-
)}
|
|
799
|
-
</div>
|
|
800
|
-
</div>
|
|
801
|
-
)}
|
|
802
|
-
|
|
803
|
-
{currentQuestion && (
|
|
804
|
-
<div className="border-t border-slate-800 px-3 py-2">
|
|
805
|
-
<div className="flex items-center justify-between mb-1">
|
|
806
|
-
<div className="flex items-center gap-1">
|
|
807
|
-
<Globe className="w-3 h-3 text-cyan-400/70" />
|
|
808
|
-
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
809
|
-
Infra Labs (
|
|
810
|
-
{
|
|
811
|
-
(currentQuestion.contextFiles || []).filter(
|
|
812
|
-
(f) => f.origin === "infra",
|
|
813
|
-
).length
|
|
814
|
-
}
|
|
815
|
-
)
|
|
816
|
-
</span>
|
|
817
|
-
</div>
|
|
818
|
-
<button
|
|
819
|
-
onClick={() => openInfraLab()}
|
|
820
|
-
className="p-0.5 rounded text-slate-600 hover:text-cyan-400 hover:bg-slate-700 transition-colors"
|
|
821
|
-
title="Open Infrastructure Lab"
|
|
822
|
-
>
|
|
823
|
-
<Plus className="w-3.5 h-3.5" />
|
|
824
|
-
</button>
|
|
825
|
-
</div>
|
|
826
|
-
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
827
|
-
{(currentQuestion.contextFiles || [])
|
|
828
|
-
.filter((f) => f.origin === "infra")
|
|
829
|
-
.map((cf) => (
|
|
830
|
-
<div
|
|
831
|
-
key={cf.id}
|
|
832
|
-
className="flex items-center gap-1 text-xs bg-cyan-500/10 border border-cyan-500/20 rounded px-1.5 py-1 group"
|
|
833
|
-
>
|
|
834
|
-
{renamingId !== cf.id && (
|
|
835
|
-
<span
|
|
836
|
-
className="text-cyan-200 font-medium truncate flex-1"
|
|
837
|
-
title={cf.label || cf.originalName}
|
|
838
|
-
>
|
|
839
|
-
{cf.label || cf.originalName}
|
|
840
|
-
</span>
|
|
841
|
-
)}
|
|
842
|
-
{renamingId === cf.id ? (
|
|
843
|
-
<>
|
|
844
|
-
<input
|
|
845
|
-
autoFocus
|
|
846
|
-
value={renameValue}
|
|
847
|
-
onChange={(e) => setRenameValue(e.target.value)}
|
|
848
|
-
onKeyDown={async (e) => {
|
|
849
|
-
if (e.key === "Enter") {
|
|
850
|
-
e.preventDefault();
|
|
851
|
-
if (renameValue.trim()) {
|
|
852
|
-
await renameContextFile(
|
|
853
|
-
currentQuestion.id,
|
|
854
|
-
cf.id,
|
|
855
|
-
renameValue.trim(),
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
setRenamingId(null);
|
|
859
|
-
} else if (e.key === "Escape") {
|
|
860
|
-
setRenamingId(null);
|
|
861
|
-
}
|
|
862
|
-
}}
|
|
863
|
-
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"
|
|
864
|
-
/>
|
|
865
|
-
<button
|
|
866
|
-
onClick={async () => {
|
|
867
|
-
if (renameValue.trim()) {
|
|
868
|
-
await renameContextFile(
|
|
869
|
-
currentQuestion.id,
|
|
870
|
-
cf.id,
|
|
871
|
-
renameValue.trim(),
|
|
872
|
-
);
|
|
873
|
-
}
|
|
874
|
-
setRenamingId(null);
|
|
875
|
-
}}
|
|
876
|
-
className="shrink-0 p-0.5 rounded text-cyan-400 hover:bg-cyan-600/20 transition-colors"
|
|
877
|
-
title="Confirm"
|
|
878
|
-
>
|
|
879
|
-
<Check className="w-3 h-3" />
|
|
880
|
-
</button>
|
|
881
|
-
<button
|
|
882
|
-
onClick={() => setRenamingId(null)}
|
|
883
|
-
className="shrink-0 p-0.5 rounded text-slate-500 hover:text-slate-300 transition-colors"
|
|
884
|
-
title="Cancel"
|
|
885
|
-
>
|
|
886
|
-
<X className="w-3 h-3" />
|
|
887
|
-
</button>
|
|
888
|
-
</>
|
|
889
|
-
) : (
|
|
890
|
-
<>
|
|
891
|
-
<button
|
|
892
|
-
onClick={() => {
|
|
893
|
-
setRenamingId(cf.id);
|
|
894
|
-
setRenameValue(cf.label || cf.originalName);
|
|
895
|
-
}}
|
|
896
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-300 transition-all"
|
|
897
|
-
title="Rename"
|
|
898
|
-
>
|
|
899
|
-
<Pencil className="w-3 h-3" />
|
|
900
|
-
</button>
|
|
901
|
-
<button
|
|
902
|
-
onClick={async () => {
|
|
903
|
-
const raw = await fetch(
|
|
904
|
-
`/api/context-files/${cf.id}/content`,
|
|
905
|
-
)
|
|
906
|
-
.then((r) => r.json())
|
|
907
|
-
.then((d) => d.content as string);
|
|
908
|
-
const parsed = parseInfraLabWorkspace(raw);
|
|
909
|
-
if (parsed) openInfraLab(parsed, cf.id);
|
|
910
|
-
}}
|
|
911
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
|
|
912
|
-
title="Open in Infrastructure Lab"
|
|
913
|
-
>
|
|
914
|
-
<Play className="w-3 h-3" />
|
|
915
|
-
</button>
|
|
916
|
-
<button
|
|
917
|
-
onClick={() =>
|
|
918
|
-
removeQuestionFile(currentQuestion.id, cf.id)
|
|
919
|
-
}
|
|
920
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
921
|
-
title="Remove"
|
|
922
|
-
>
|
|
923
|
-
<Trash2 className="w-3 h-3" />
|
|
924
|
-
</button>
|
|
925
|
-
</>
|
|
926
|
-
)}
|
|
927
|
-
</div>
|
|
928
|
-
))}
|
|
929
|
-
{(currentQuestion.contextFiles || []).filter(
|
|
930
|
-
(f) => f.origin === "infra",
|
|
931
|
-
).length === 0 && (
|
|
932
|
-
<p className="text-[10px] text-slate-700 italic">
|
|
933
|
-
Save an infra lab to reopen it here
|
|
934
|
-
</p>
|
|
935
|
-
)}
|
|
936
|
-
</div>
|
|
937
|
-
</div>
|
|
938
|
-
)}
|
|
939
|
-
|
|
940
|
-
{/* ── React Labs section ───────────────────── */}
|
|
941
|
-
{currentQuestion && (
|
|
942
|
-
<div className="border-t border-slate-800 px-3 py-2">
|
|
943
|
-
<div className="flex items-center justify-between mb-1">
|
|
944
|
-
<div className="flex items-center gap-1">
|
|
945
|
-
<Atom className="w-3 h-3 text-cyan-400/70" />
|
|
946
|
-
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
947
|
-
React Labs (
|
|
948
|
-
{
|
|
949
|
-
(currentQuestion.contextFiles || []).filter(
|
|
950
|
-
(f) => f.origin === "react",
|
|
951
|
-
).length
|
|
952
|
-
}
|
|
953
|
-
)
|
|
954
|
-
</span>
|
|
955
|
-
</div>
|
|
956
|
-
<button
|
|
957
|
-
onClick={() => openReactLab()}
|
|
958
|
-
className="p-0.5 rounded text-slate-600 hover:text-cyan-400 hover:bg-slate-700 transition-colors"
|
|
959
|
-
title="Open React Lab"
|
|
960
|
-
>
|
|
961
|
-
<Plus className="w-3.5 h-3.5" />
|
|
962
|
-
</button>
|
|
963
|
-
</div>
|
|
964
|
-
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
965
|
-
{(currentQuestion.contextFiles || [])
|
|
966
|
-
.filter((f) => f.origin === "react")
|
|
967
|
-
.map((cf) => (
|
|
968
|
-
<div
|
|
969
|
-
key={cf.id}
|
|
970
|
-
className="flex items-center gap-1 text-xs bg-cyan-500/10 border border-cyan-500/20 rounded px-1.5 py-1 group"
|
|
971
|
-
>
|
|
972
|
-
<span
|
|
973
|
-
className="text-cyan-200 font-medium truncate flex-1"
|
|
974
|
-
title={cf.label || cf.originalName}
|
|
975
|
-
>
|
|
976
|
-
{cf.label || cf.originalName}
|
|
977
|
-
</span>
|
|
978
|
-
<button
|
|
979
|
-
onClick={async () => {
|
|
980
|
-
try {
|
|
981
|
-
const raw = await fetch(
|
|
982
|
-
`/api/context-files/${cf.id}/content`,
|
|
983
|
-
)
|
|
984
|
-
.then((r) => r.json())
|
|
985
|
-
.then((d) => d.content as string);
|
|
986
|
-
// Try new extended sandbox format first
|
|
987
|
-
const ext = JSON.parse(raw) as {
|
|
988
|
-
clientType?: string;
|
|
989
|
-
reactFiles?: Record<string, string>;
|
|
990
|
-
reactActiveFile?: string;
|
|
991
|
-
serverCode?: string;
|
|
992
|
-
serverLang?: string;
|
|
993
|
-
};
|
|
994
|
-
if (ext?.clientType === "react" && ext.reactFiles) {
|
|
995
|
-
openReactLab(
|
|
996
|
-
{
|
|
997
|
-
version: 1,
|
|
998
|
-
type: "react",
|
|
999
|
-
label: cf.label || "React Lab",
|
|
1000
|
-
activeFile:
|
|
1001
|
-
ext.reactActiveFile ??
|
|
1002
|
-
Object.keys(ext.reactFiles)[0] ??
|
|
1003
|
-
"App.tsx",
|
|
1004
|
-
files: ext.reactFiles,
|
|
1005
|
-
},
|
|
1006
|
-
cf.id,
|
|
1007
|
-
ext.serverCode,
|
|
1008
|
-
ext.serverLang,
|
|
1009
|
-
);
|
|
1010
|
-
} else {
|
|
1011
|
-
// Fall back to old FrontendLabWorkspace format
|
|
1012
|
-
const ws = parseFrontendLabWorkspace(raw);
|
|
1013
|
-
if (ws) openReactLab(ws, cf.id);
|
|
1014
|
-
}
|
|
1015
|
-
} catch {
|
|
1016
|
-
/* ignore */
|
|
1017
|
-
}
|
|
1018
|
-
}}
|
|
1019
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
|
|
1020
|
-
title="Open in React Lab"
|
|
1021
|
-
>
|
|
1022
|
-
<Play className="w-3 h-3" />
|
|
1023
|
-
</button>
|
|
1024
|
-
<button
|
|
1025
|
-
onClick={() =>
|
|
1026
|
-
removeQuestionFile(currentQuestion.id, cf.id)
|
|
1027
|
-
}
|
|
1028
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
1029
|
-
title="Remove"
|
|
1030
|
-
>
|
|
1031
|
-
<Trash2 className="w-3 h-3" />
|
|
1032
|
-
</button>
|
|
1033
|
-
</div>
|
|
1034
|
-
))}
|
|
1035
|
-
{(currentQuestion.contextFiles || []).filter(
|
|
1036
|
-
(f) => f.origin === "react",
|
|
1037
|
-
).length === 0 && (
|
|
1038
|
-
<p className="text-[10px] text-slate-700 italic">
|
|
1039
|
-
Save a React lab to reopen it here
|
|
1040
|
-
</p>
|
|
1041
|
-
)}
|
|
1042
|
-
</div>
|
|
1043
|
-
</div>
|
|
1044
|
-
)}
|
|
1045
|
-
|
|
1046
|
-
{/* ── Next.js Labs section ──────────────────── */}
|
|
1047
|
-
{currentQuestion && (
|
|
1048
|
-
<div className="border-t border-slate-800 px-3 py-2">
|
|
1049
|
-
<div className="flex items-center justify-between mb-1">
|
|
1050
|
-
<div className="flex items-center gap-1">
|
|
1051
|
-
<Layout className="w-3 h-3 text-violet-400/70" />
|
|
1052
|
-
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
1053
|
-
Next.js Labs (
|
|
1054
|
-
{
|
|
1055
|
-
(currentQuestion.contextFiles || []).filter(
|
|
1056
|
-
(f) => f.origin === "nextjs",
|
|
1057
|
-
).length
|
|
1058
|
-
}
|
|
1059
|
-
)
|
|
1060
|
-
</span>
|
|
1061
|
-
</div>
|
|
1062
|
-
<button
|
|
1063
|
-
onClick={() => openNextLab()}
|
|
1064
|
-
className="p-0.5 rounded text-slate-600 hover:text-violet-400 hover:bg-slate-700 transition-colors"
|
|
1065
|
-
title="Open Next.js Lab"
|
|
1066
|
-
>
|
|
1067
|
-
<Plus className="w-3.5 h-3.5" />
|
|
1068
|
-
</button>
|
|
1069
|
-
</div>
|
|
1070
|
-
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
1071
|
-
{(currentQuestion.contextFiles || [])
|
|
1072
|
-
.filter((f) => f.origin === "nextjs")
|
|
1073
|
-
.map((cf) => (
|
|
1074
|
-
<div
|
|
1075
|
-
key={cf.id}
|
|
1076
|
-
className="flex items-center gap-1 text-xs bg-violet-500/10 border border-violet-500/20 rounded px-1.5 py-1 group"
|
|
1077
|
-
>
|
|
1078
|
-
<span
|
|
1079
|
-
className="text-violet-200 font-medium truncate flex-1"
|
|
1080
|
-
title={cf.label || cf.originalName}
|
|
1081
|
-
>
|
|
1082
|
-
{cf.label || cf.originalName}
|
|
1083
|
-
</span>
|
|
1084
|
-
<button
|
|
1085
|
-
onClick={async () => {
|
|
1086
|
-
try {
|
|
1087
|
-
const raw = await fetch(
|
|
1088
|
-
`/api/context-files/${cf.id}/content`,
|
|
1089
|
-
)
|
|
1090
|
-
.then((r) => r.json())
|
|
1091
|
-
.then((d) => d.content as string);
|
|
1092
|
-
const ext = JSON.parse(raw) as {
|
|
1093
|
-
clientType?: string;
|
|
1094
|
-
reactFiles?: Record<string, string>;
|
|
1095
|
-
reactActiveFile?: string;
|
|
1096
|
-
serverCode?: string;
|
|
1097
|
-
serverLang?: string;
|
|
1098
|
-
};
|
|
1099
|
-
if (ext?.clientType === "nextjs" && ext.reactFiles) {
|
|
1100
|
-
openNextLab(
|
|
1101
|
-
{
|
|
1102
|
-
version: 1,
|
|
1103
|
-
type: "nextjs",
|
|
1104
|
-
label: cf.label || "Next.js Lab",
|
|
1105
|
-
activeFile:
|
|
1106
|
-
ext.reactActiveFile ??
|
|
1107
|
-
Object.keys(ext.reactFiles)[0] ??
|
|
1108
|
-
"app/page.tsx",
|
|
1109
|
-
files: ext.reactFiles,
|
|
1110
|
-
},
|
|
1111
|
-
cf.id,
|
|
1112
|
-
ext.serverCode,
|
|
1113
|
-
ext.serverLang,
|
|
1114
|
-
);
|
|
1115
|
-
} else {
|
|
1116
|
-
const ws = parseFrontendLabWorkspace(raw);
|
|
1117
|
-
if (ws) openNextLab(ws, cf.id);
|
|
1118
|
-
}
|
|
1119
|
-
} catch {
|
|
1120
|
-
/* ignore */
|
|
1121
|
-
}
|
|
1122
|
-
}}
|
|
1123
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
|
|
1124
|
-
title="Open in Next.js Lab"
|
|
1125
|
-
>
|
|
1126
|
-
<Play className="w-3 h-3" />
|
|
1127
|
-
</button>
|
|
1128
|
-
<button
|
|
1129
|
-
onClick={() =>
|
|
1130
|
-
removeQuestionFile(currentQuestion.id, cf.id)
|
|
1131
|
-
}
|
|
1132
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
1133
|
-
title="Remove"
|
|
1134
|
-
>
|
|
1135
|
-
<Trash2 className="w-3 h-3" />
|
|
1136
|
-
</button>
|
|
1137
|
-
</div>
|
|
1138
|
-
))}
|
|
1139
|
-
{(currentQuestion.contextFiles || []).filter(
|
|
1140
|
-
(f) => f.origin === "nextjs",
|
|
1141
|
-
).length === 0 && (
|
|
1142
|
-
<p className="text-[10px] text-slate-700 italic">
|
|
1143
|
-
Save a Next.js lab to reopen it here
|
|
1144
|
-
</p>
|
|
1145
|
-
)}
|
|
1146
|
-
</div>
|
|
1147
|
-
</div>
|
|
1148
|
-
)}
|
|
1149
|
-
|
|
1150
|
-
{/* ── Webpack Module Federation Labs section ───────────── */}
|
|
1151
|
-
{currentQuestion && (
|
|
1152
|
-
<div className="border-t border-slate-800 px-3 py-2">
|
|
1153
|
-
<div className="flex items-center justify-between mb-1">
|
|
1154
|
-
<div className="flex items-center gap-1">
|
|
1155
|
-
<Server className="w-3 h-3 text-emerald-400/70" />
|
|
1156
|
-
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
1157
|
-
Webpack MF Labs (
|
|
1158
|
-
{
|
|
1159
|
-
(currentQuestion.contextFiles || []).filter(
|
|
1160
|
-
(f) => f.origin === "module-federation",
|
|
1161
|
-
).length
|
|
1162
|
-
}
|
|
1163
|
-
)
|
|
1164
|
-
</span>
|
|
1165
|
-
</div>
|
|
1166
|
-
<button
|
|
1167
|
-
onClick={() => openModuleFederationLab()}
|
|
1168
|
-
className="p-0.5 rounded text-slate-600 hover:text-emerald-400 hover:bg-slate-700 transition-colors"
|
|
1169
|
-
title="Open Webpack Module Federation Lab"
|
|
1170
|
-
>
|
|
1171
|
-
<Plus className="w-3.5 h-3.5" />
|
|
1172
|
-
</button>
|
|
1173
|
-
</div>
|
|
1174
|
-
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
1175
|
-
{(currentQuestion.contextFiles || [])
|
|
1176
|
-
.filter((f) => f.origin === "module-federation")
|
|
1177
|
-
.map((cf) => (
|
|
1178
|
-
<div
|
|
1179
|
-
key={cf.id}
|
|
1180
|
-
className="flex items-center gap-1 text-xs bg-emerald-500/10 border border-emerald-500/20 rounded px-1.5 py-1 group"
|
|
1181
|
-
>
|
|
1182
|
-
<span
|
|
1183
|
-
className="text-emerald-200 font-medium truncate flex-1"
|
|
1184
|
-
title={cf.label || cf.originalName}
|
|
1185
|
-
>
|
|
1186
|
-
{cf.label || cf.originalName}
|
|
1187
|
-
</span>
|
|
1188
|
-
<button
|
|
1189
|
-
onClick={async () => {
|
|
1190
|
-
try {
|
|
1191
|
-
const raw = await fetch(
|
|
1192
|
-
`/api/context-files/${cf.id}/content`,
|
|
1193
|
-
)
|
|
1194
|
-
.then((r) => r.json())
|
|
1195
|
-
.then((d) => d.content as string);
|
|
1196
|
-
const ext = JSON.parse(raw) as {
|
|
1197
|
-
clientType?: string;
|
|
1198
|
-
reactFiles?: Record<string, string>;
|
|
1199
|
-
reactActiveFile?: string;
|
|
1200
|
-
serverCode?: string;
|
|
1201
|
-
serverLang?: string;
|
|
1202
|
-
};
|
|
1203
|
-
if (
|
|
1204
|
-
ext?.clientType === "module-federation" &&
|
|
1205
|
-
ext.reactFiles
|
|
1206
|
-
) {
|
|
1207
|
-
openModuleFederationLab(
|
|
1208
|
-
{
|
|
1209
|
-
version: 1,
|
|
1210
|
-
type: "module-federation",
|
|
1211
|
-
label:
|
|
1212
|
-
cf.label || "Webpack Module Federation Lab",
|
|
1213
|
-
activeFile:
|
|
1214
|
-
ext.reactActiveFile ??
|
|
1215
|
-
Object.keys(ext.reactFiles)[0] ??
|
|
1216
|
-
"apps/host/webpack.config.js",
|
|
1217
|
-
files: ext.reactFiles,
|
|
1218
|
-
},
|
|
1219
|
-
cf.id,
|
|
1220
|
-
ext.serverCode,
|
|
1221
|
-
ext.serverLang,
|
|
1222
|
-
);
|
|
1223
|
-
} else {
|
|
1224
|
-
const ws = parseFrontendLabWorkspace(raw);
|
|
1225
|
-
if (ws?.type === "module-federation") {
|
|
1226
|
-
openModuleFederationLab(ws, cf.id);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
} catch {
|
|
1230
|
-
/* ignore */
|
|
1231
|
-
}
|
|
1232
|
-
}}
|
|
1233
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-emerald-400 transition-all"
|
|
1234
|
-
title="Open in Webpack Module Federation Lab"
|
|
1235
|
-
>
|
|
1236
|
-
<Play className="w-3 h-3" />
|
|
1237
|
-
</button>
|
|
1238
|
-
<button
|
|
1239
|
-
onClick={() =>
|
|
1240
|
-
removeQuestionFile(currentQuestion.id, cf.id)
|
|
1241
|
-
}
|
|
1242
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
1243
|
-
title="Remove"
|
|
1244
|
-
>
|
|
1245
|
-
<Trash2 className="w-3 h-3" />
|
|
1246
|
-
</button>
|
|
1247
|
-
</div>
|
|
1248
|
-
))}
|
|
1249
|
-
{(currentQuestion.contextFiles || []).filter(
|
|
1250
|
-
(f) => f.origin === "module-federation",
|
|
1251
|
-
).length === 0 && (
|
|
1252
|
-
<p className="text-[10px] text-slate-700 italic">
|
|
1253
|
-
Save a webpack module federation lab to reopen it here
|
|
1254
|
-
</p>
|
|
1255
|
-
)}
|
|
1256
|
-
</div>
|
|
1257
|
-
</div>
|
|
1258
|
-
)}
|
|
1259
|
-
|
|
1260
638
|
{/* ── Notes section ────────────────────────────────────── */}
|
|
1261
639
|
<div className="border-t border-slate-800 px-3 py-2">
|
|
1262
640
|
<button
|