create-interview-cockpit 0.6.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/src/api.ts +65 -2
- package/template/client/src/components/CodeContextPanel.tsx +111 -0
- package/template/client/src/components/CodeRunnerModal.tsx +457 -163
- package/template/client/src/reactLab.ts +488 -5
- package/template/client/src/store.ts +35 -4
- package/template/client/src/types.ts +3 -2
- package/template/cockpit.json +1 -1
- package/template/server/src/google-drive.ts +2 -0
- package/template/server/src/index.ts +266 -5
- package/template/server/src/storage.ts +11 -2
package/package.json
CHANGED
|
@@ -320,7 +320,14 @@ export async function saveCodeSnippet(
|
|
|
320
320
|
code: string,
|
|
321
321
|
language: string,
|
|
322
322
|
label: string,
|
|
323
|
-
origin:
|
|
323
|
+
origin:
|
|
324
|
+
| "user"
|
|
325
|
+
| "ai"
|
|
326
|
+
| "sandbox"
|
|
327
|
+
| "infra"
|
|
328
|
+
| "react"
|
|
329
|
+
| "nextjs"
|
|
330
|
+
| "module-federation",
|
|
324
331
|
): Promise<ContextFile> {
|
|
325
332
|
const res = await fetch(`${BASE}/questions/${questionId}/save-code-snippet`, {
|
|
326
333
|
method: "POST",
|
|
@@ -538,7 +545,7 @@ export async function streamFrontendLabAsk(
|
|
|
538
545
|
input: {
|
|
539
546
|
messages: Array<{ role: "user" | "assistant"; content: string }>;
|
|
540
547
|
workspace: Record<string, string>;
|
|
541
|
-
labType: "react" | "nextjs";
|
|
548
|
+
labType: "react" | "nextjs" | "module-federation";
|
|
542
549
|
questionId?: string;
|
|
543
550
|
},
|
|
544
551
|
onDelta: (chunk: string) => void,
|
|
@@ -779,6 +786,20 @@ export interface NextjsSandboxInfo {
|
|
|
779
786
|
url: string;
|
|
780
787
|
}
|
|
781
788
|
|
|
789
|
+
export interface ModuleFederationSandboxInfo {
|
|
790
|
+
id: string;
|
|
791
|
+
hostUrl: string;
|
|
792
|
+
appUrls: Record<string, string>;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export interface ModuleFederationSandboxStatus {
|
|
796
|
+
running: boolean;
|
|
797
|
+
ready?: boolean;
|
|
798
|
+
hostUrl?: string;
|
|
799
|
+
appUrls?: Record<string, string>;
|
|
800
|
+
logs?: string[];
|
|
801
|
+
}
|
|
802
|
+
|
|
782
803
|
export async function startNextjsSandbox(
|
|
783
804
|
files: Record<string, string>,
|
|
784
805
|
): Promise<NextjsSandboxInfo> {
|
|
@@ -810,3 +831,45 @@ export async function updateNextjsFiles(
|
|
|
810
831
|
export async function stopNextjsSandbox(id: string): Promise<void> {
|
|
811
832
|
await fetch(`${BASE}/nextjs/${id}`, { method: "DELETE" });
|
|
812
833
|
}
|
|
834
|
+
|
|
835
|
+
export async function startModuleFederationSandbox(
|
|
836
|
+
files: Record<string, string>,
|
|
837
|
+
): Promise<ModuleFederationSandboxInfo> {
|
|
838
|
+
const res = await fetch(`${BASE}/module-federation/start`, {
|
|
839
|
+
method: "POST",
|
|
840
|
+
headers: { "Content-Type": "application/json" },
|
|
841
|
+
body: JSON.stringify({ files }),
|
|
842
|
+
});
|
|
843
|
+
if (!res.ok) {
|
|
844
|
+
const body = await res.json().catch(() => ({}));
|
|
845
|
+
throw new Error(
|
|
846
|
+
(body as any).error ||
|
|
847
|
+
`Failed to start webpack module federation lab (${res.status})`,
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
return res.json();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export async function updateModuleFederationFiles(
|
|
854
|
+
id: string,
|
|
855
|
+
files: Record<string, string>,
|
|
856
|
+
): Promise<void> {
|
|
857
|
+
const res = await fetch(`${BASE}/module-federation/${id}/update-files`, {
|
|
858
|
+
method: "POST",
|
|
859
|
+
headers: { "Content-Type": "application/json" },
|
|
860
|
+
body: JSON.stringify({ files }),
|
|
861
|
+
});
|
|
862
|
+
if (!res.ok) throw new Error(await res.text());
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export async function fetchModuleFederationStatus(
|
|
866
|
+
id: string,
|
|
867
|
+
): Promise<ModuleFederationSandboxStatus> {
|
|
868
|
+
const res = await fetch(`${BASE}/module-federation/${id}/status`);
|
|
869
|
+
if (!res.ok) throw new Error(await res.text());
|
|
870
|
+
return res.json();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
export async function stopModuleFederationSandbox(id: string): Promise<void> {
|
|
874
|
+
await fetch(`${BASE}/module-federation/${id}`, { method: "DELETE" });
|
|
875
|
+
}
|
|
@@ -238,6 +238,7 @@ export default function CodeContextPanel() {
|
|
|
238
238
|
openInfraLab,
|
|
239
239
|
openReactLab,
|
|
240
240
|
openNextLab,
|
|
241
|
+
openModuleFederationLab,
|
|
241
242
|
removeQuestionFile,
|
|
242
243
|
renameContextFile,
|
|
243
244
|
} = useStore();
|
|
@@ -1144,6 +1145,116 @@ export default function CodeContextPanel() {
|
|
|
1144
1145
|
</div>
|
|
1145
1146
|
)}
|
|
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
|
+
|
|
1147
1258
|
{/* ── Notes section ────────────────────────────────────── */}
|
|
1148
1259
|
<div className="border-t border-slate-800 px-3 py-2">
|
|
1149
1260
|
<button
|