create-interview-cockpit 0.12.0 → 0.14.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.
@@ -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