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
|
@@ -43,10 +43,15 @@ import {
|
|
|
43
43
|
generatePreviewHTML,
|
|
44
44
|
defaultForType,
|
|
45
45
|
resolveNextjsEntry,
|
|
46
|
+
type FrontendLabType,
|
|
46
47
|
} from "../reactLab";
|
|
47
48
|
import {
|
|
49
|
+
fetchModuleFederationStatus,
|
|
50
|
+
startModuleFederationSandbox,
|
|
48
51
|
startNextjsSandbox,
|
|
52
|
+
stopModuleFederationSandbox,
|
|
49
53
|
updateNextjsFiles,
|
|
54
|
+
updateModuleFederationFiles,
|
|
50
55
|
stopNextjsSandbox,
|
|
51
56
|
} from "../api";
|
|
52
57
|
import ReactMarkdown from "react-markdown";
|
|
@@ -67,6 +72,7 @@ interface OutputLine {
|
|
|
67
72
|
|
|
68
73
|
const LANG_OPTIONS = ["typescript", "javascript"] as const;
|
|
69
74
|
type Lang = (typeof LANG_OPTIONS)[number];
|
|
75
|
+
type FrontendClientType = "script" | FrontendLabType;
|
|
70
76
|
|
|
71
77
|
// ── Sandbox default snippets ─────────────────────────────────────────
|
|
72
78
|
const DEFAULT_SERVER_CODE = `import express from 'express';
|
|
@@ -202,6 +208,49 @@ function SyntaxEditor({
|
|
|
202
208
|
);
|
|
203
209
|
}
|
|
204
210
|
|
|
211
|
+
interface FileTreeNode {
|
|
212
|
+
name: string;
|
|
213
|
+
path: string;
|
|
214
|
+
children: FileTreeNode[];
|
|
215
|
+
files: string[];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildFileTree(paths: string[]): FileTreeNode {
|
|
219
|
+
const root: FileTreeNode = {
|
|
220
|
+
name: "",
|
|
221
|
+
path: "",
|
|
222
|
+
children: [],
|
|
223
|
+
files: [],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
for (const filePath of paths) {
|
|
227
|
+
const parts = filePath.split("/");
|
|
228
|
+
let node = root;
|
|
229
|
+
|
|
230
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
231
|
+
const name = parts[index];
|
|
232
|
+
const path = parts.slice(0, index + 1).join("/");
|
|
233
|
+
let child = node.children.find((entry) => entry.path === path);
|
|
234
|
+
|
|
235
|
+
if (!child) {
|
|
236
|
+
child = {
|
|
237
|
+
name,
|
|
238
|
+
path,
|
|
239
|
+
children: [],
|
|
240
|
+
files: [],
|
|
241
|
+
};
|
|
242
|
+
node.children.push(child);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
node = child;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
node.files.push(filePath);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return root;
|
|
252
|
+
}
|
|
253
|
+
|
|
205
254
|
export default function CodeRunnerModal() {
|
|
206
255
|
const {
|
|
207
256
|
closeCodeRunner,
|
|
@@ -246,9 +295,7 @@ export default function CodeRunnerModal() {
|
|
|
246
295
|
const [clientRunning, setClientRunning] = useState(false);
|
|
247
296
|
|
|
248
297
|
// ── React/Next.js client state ──────────────────────────────
|
|
249
|
-
const [clientType, setClientType] = useState<"script"
|
|
250
|
-
"script",
|
|
251
|
-
);
|
|
298
|
+
const [clientType, setClientType] = useState<FrontendClientType>("script");
|
|
252
299
|
const [reactFiles, setReactFiles] = useState<Record<string, string>>({});
|
|
253
300
|
const [reactActiveFile, setReactActiveFile] = useState<string>("");
|
|
254
301
|
const [reactClientTab, setReactClientTab] = useState<"edit" | "preview">(
|
|
@@ -267,6 +314,12 @@ export default function CodeRunnerModal() {
|
|
|
267
314
|
const [nxStarting, setNxStarting] = useState(false);
|
|
268
315
|
const [nxError, setNxError] = useState<string | null>(null);
|
|
269
316
|
const nxIframeRef = useRef<HTMLIFrameElement>(null);
|
|
317
|
+
const [mfSandboxId, setMfSandboxId] = useState<string | null>(null);
|
|
318
|
+
const [mfHostUrl, setMfHostUrl] = useState<string | null>(null);
|
|
319
|
+
const [mfAppUrls, setMfAppUrls] = useState<Record<string, string>>({});
|
|
320
|
+
const [mfStarting, setMfStarting] = useState(false);
|
|
321
|
+
const [mfError, setMfError] = useState<string | null>(null);
|
|
322
|
+
const [mfPreviewApp, setMfPreviewApp] = useState("host");
|
|
270
323
|
// Simulated URL bar state for Next.js mode
|
|
271
324
|
const [reactPreviewPath, setReactPreviewPath] = useState("/");
|
|
272
325
|
const [reactNavInput, setReactNavInput] = useState("/");
|
|
@@ -367,7 +420,9 @@ export default function CodeRunnerModal() {
|
|
|
367
420
|
? "react"
|
|
368
421
|
: clientType === "nextjs"
|
|
369
422
|
? "nextjs"
|
|
370
|
-
: "
|
|
423
|
+
: clientType === "module-federation"
|
|
424
|
+
? "module-federation"
|
|
425
|
+
: "sandbox";
|
|
371
426
|
const payload = JSON.stringify(
|
|
372
427
|
clientType === "script"
|
|
373
428
|
? { serverCode, serverLang, clientCode, clientLang }
|
|
@@ -474,10 +529,9 @@ export default function CodeRunnerModal() {
|
|
|
474
529
|
setActiveSandboxId(runnerInitialSandbox.fileId ?? null);
|
|
475
530
|
// Restore client type and React/Next.js files
|
|
476
531
|
const ct =
|
|
477
|
-
(runnerInitialSandbox.clientType as
|
|
478
|
-
"script";
|
|
532
|
+
(runnerInitialSandbox.clientType as FrontendClientType) ?? "script";
|
|
479
533
|
setClientType(ct);
|
|
480
|
-
if (ct
|
|
534
|
+
if (ct !== "script") {
|
|
481
535
|
if (
|
|
482
536
|
runnerInitialSandbox.reactFiles &&
|
|
483
537
|
Object.keys(runnerInitialSandbox.reactFiles).length > 0
|
|
@@ -495,6 +549,11 @@ export default function CodeRunnerModal() {
|
|
|
495
549
|
}
|
|
496
550
|
setReactPreviewSrc(null);
|
|
497
551
|
setReactClientTab("edit");
|
|
552
|
+
if (ct === "module-federation") {
|
|
553
|
+
setServerCollapsed(true);
|
|
554
|
+
setClientCollapsed(false);
|
|
555
|
+
setMfPreviewApp("host");
|
|
556
|
+
}
|
|
498
557
|
}
|
|
499
558
|
}, [runnerInitialSandbox]);
|
|
500
559
|
|
|
@@ -979,6 +1038,66 @@ export default function CodeRunnerModal() {
|
|
|
979
1038
|
}
|
|
980
1039
|
}, [nxStarting, reactFiles]);
|
|
981
1040
|
|
|
1041
|
+
const startModuleFederationServer = useCallback(async () => {
|
|
1042
|
+
if (mfStarting) return;
|
|
1043
|
+
setMfStarting(true);
|
|
1044
|
+
setMfError(null);
|
|
1045
|
+
setSbxBottomTab("output");
|
|
1046
|
+
setSandboxOutput([
|
|
1047
|
+
{
|
|
1048
|
+
kind: "info",
|
|
1049
|
+
text: "Installing dependencies and starting webpack apps…",
|
|
1050
|
+
source: "server",
|
|
1051
|
+
},
|
|
1052
|
+
]);
|
|
1053
|
+
try {
|
|
1054
|
+
const info = await startModuleFederationSandbox(reactFiles);
|
|
1055
|
+
setMfSandboxId(info.id);
|
|
1056
|
+
setMfHostUrl(info.hostUrl);
|
|
1057
|
+
setMfAppUrls(info.appUrls);
|
|
1058
|
+
setMfPreviewApp(
|
|
1059
|
+
info.appUrls.host ? "host" : (Object.keys(info.appUrls)[0] ?? "host"),
|
|
1060
|
+
);
|
|
1061
|
+
setReactClientTab("preview");
|
|
1062
|
+
setServerCollapsed(true);
|
|
1063
|
+
setClientCollapsed(false);
|
|
1064
|
+
setSandboxOutput((prev) => [
|
|
1065
|
+
...prev,
|
|
1066
|
+
{
|
|
1067
|
+
kind: "info",
|
|
1068
|
+
text: `✓ Webpack host running at ${info.hostUrl}`,
|
|
1069
|
+
source: "server",
|
|
1070
|
+
},
|
|
1071
|
+
]);
|
|
1072
|
+
} catch (err: any) {
|
|
1073
|
+
const message = err?.message ?? String(err);
|
|
1074
|
+
setMfError(message);
|
|
1075
|
+
setSandboxOutput((prev) => [
|
|
1076
|
+
...prev,
|
|
1077
|
+
{ kind: "stderr", text: message, source: "server" },
|
|
1078
|
+
]);
|
|
1079
|
+
} finally {
|
|
1080
|
+
setMfStarting(false);
|
|
1081
|
+
}
|
|
1082
|
+
}, [mfStarting, reactFiles]);
|
|
1083
|
+
|
|
1084
|
+
const stopModuleFederationServer = useCallback(async () => {
|
|
1085
|
+
if (!mfSandboxId) return;
|
|
1086
|
+
await stopModuleFederationSandbox(mfSandboxId).catch(() => {});
|
|
1087
|
+
setMfSandboxId(null);
|
|
1088
|
+
setMfHostUrl(null);
|
|
1089
|
+
setMfAppUrls({});
|
|
1090
|
+
setMfError(null);
|
|
1091
|
+
setSandboxOutput((prev) => [
|
|
1092
|
+
...prev,
|
|
1093
|
+
{
|
|
1094
|
+
kind: "info",
|
|
1095
|
+
text: "Webpack module federation lab stopped.",
|
|
1096
|
+
source: "server",
|
|
1097
|
+
},
|
|
1098
|
+
]);
|
|
1099
|
+
}, [mfSandboxId]);
|
|
1100
|
+
|
|
982
1101
|
/** Push updated files to the running Next.js server (HMR picks them up). */
|
|
983
1102
|
const pushNextjsFiles = useCallback(
|
|
984
1103
|
async (files: Record<string, string>) => {
|
|
@@ -992,6 +1111,18 @@ export default function CodeRunnerModal() {
|
|
|
992
1111
|
[nxSandboxId],
|
|
993
1112
|
);
|
|
994
1113
|
|
|
1114
|
+
const pushModuleFederationFiles = useCallback(
|
|
1115
|
+
async (files: Record<string, string>) => {
|
|
1116
|
+
if (!mfSandboxId) return;
|
|
1117
|
+
try {
|
|
1118
|
+
await updateModuleFederationFiles(mfSandboxId, files);
|
|
1119
|
+
} catch (err: any) {
|
|
1120
|
+
setMfError(err?.message ?? String(err));
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
[mfSandboxId],
|
|
1124
|
+
);
|
|
1125
|
+
|
|
995
1126
|
// Auto-push file changes to the running Next.js server
|
|
996
1127
|
const nxFilesRef = useRef(reactFiles);
|
|
997
1128
|
useEffect(() => {
|
|
@@ -1000,6 +1131,55 @@ export default function CodeRunnerModal() {
|
|
|
1000
1131
|
void pushNextjsFiles(reactFiles);
|
|
1001
1132
|
}, [reactFiles, nxSandboxId, pushNextjsFiles]);
|
|
1002
1133
|
|
|
1134
|
+
const mfFilesRef = useRef(reactFiles);
|
|
1135
|
+
useEffect(() => {
|
|
1136
|
+
if (!mfSandboxId || reactFiles === mfFilesRef.current) return;
|
|
1137
|
+
mfFilesRef.current = reactFiles;
|
|
1138
|
+
void pushModuleFederationFiles(reactFiles);
|
|
1139
|
+
}, [reactFiles, mfSandboxId, pushModuleFederationFiles]);
|
|
1140
|
+
|
|
1141
|
+
useEffect(() => {
|
|
1142
|
+
if (!mfSandboxId) return;
|
|
1143
|
+
const interval = setInterval(async () => {
|
|
1144
|
+
try {
|
|
1145
|
+
const status = await fetchModuleFederationStatus(mfSandboxId);
|
|
1146
|
+
if (!status.running) {
|
|
1147
|
+
setMfSandboxId(null);
|
|
1148
|
+
setMfHostUrl(null);
|
|
1149
|
+
setMfAppUrls({});
|
|
1150
|
+
setMfError(null);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
if (status.hostUrl) setMfHostUrl(status.hostUrl);
|
|
1154
|
+
if (status.appUrls) {
|
|
1155
|
+
setMfAppUrls(status.appUrls);
|
|
1156
|
+
setMfPreviewApp((prev) =>
|
|
1157
|
+
status.appUrls?.[prev]
|
|
1158
|
+
? prev
|
|
1159
|
+
: (Object.keys(status.appUrls ?? {})[0] ?? "host"),
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
if (status.logs) {
|
|
1163
|
+
setSandboxOutput(
|
|
1164
|
+
status.logs.flatMap((chunk) =>
|
|
1165
|
+
chunk
|
|
1166
|
+
.split("\n")
|
|
1167
|
+
.filter(Boolean)
|
|
1168
|
+
.map((text) => ({
|
|
1169
|
+
kind: "stdout" as const,
|
|
1170
|
+
text,
|
|
1171
|
+
source: "server" as const,
|
|
1172
|
+
})),
|
|
1173
|
+
),
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
} catch {
|
|
1177
|
+
/* ignore transient network errors */
|
|
1178
|
+
}
|
|
1179
|
+
}, 1000);
|
|
1180
|
+
return () => clearInterval(interval);
|
|
1181
|
+
}, [mfSandboxId]);
|
|
1182
|
+
|
|
1003
1183
|
// Clean up Next.js server when the modal is closed or mode changes away from nextjs
|
|
1004
1184
|
const prevClientTypeRef = useRef(clientType);
|
|
1005
1185
|
useEffect(() => {
|
|
@@ -1010,21 +1190,32 @@ export default function CodeRunnerModal() {
|
|
|
1010
1190
|
setNxSandboxId(null);
|
|
1011
1191
|
setNxSandboxUrl(null);
|
|
1012
1192
|
}
|
|
1013
|
-
|
|
1193
|
+
if (
|
|
1194
|
+
prev === "module-federation" &&
|
|
1195
|
+
clientType !== "module-federation" &&
|
|
1196
|
+
mfSandboxId
|
|
1197
|
+
) {
|
|
1198
|
+
void stopModuleFederationSandbox(mfSandboxId);
|
|
1199
|
+
setMfSandboxId(null);
|
|
1200
|
+
setMfHostUrl(null);
|
|
1201
|
+
setMfAppUrls({});
|
|
1202
|
+
}
|
|
1203
|
+
}, [clientType, nxSandboxId, mfSandboxId]);
|
|
1014
1204
|
|
|
1015
1205
|
// Clean up on unmount
|
|
1016
1206
|
useEffect(() => {
|
|
1017
1207
|
return () => {
|
|
1018
1208
|
if (nxSandboxId) void stopNextjsSandbox(nxSandboxId);
|
|
1209
|
+
if (mfSandboxId) void stopModuleFederationSandbox(mfSandboxId);
|
|
1019
1210
|
};
|
|
1020
1211
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1021
|
-
}, [nxSandboxId]);
|
|
1212
|
+
}, [nxSandboxId, mfSandboxId]);
|
|
1022
1213
|
|
|
1023
1214
|
const handleClientTypeChange = useCallback(
|
|
1024
|
-
(ct:
|
|
1215
|
+
(ct: FrontendClientType) => {
|
|
1025
1216
|
if (ct === clientType) return;
|
|
1026
1217
|
setClientType(ct);
|
|
1027
|
-
if (ct
|
|
1218
|
+
if (ct !== "script") {
|
|
1028
1219
|
const defs = defaultForType(ct);
|
|
1029
1220
|
setReactFiles(defs.files);
|
|
1030
1221
|
setReactActiveFile(defs.activeFile);
|
|
@@ -1036,6 +1227,12 @@ export default function CodeRunnerModal() {
|
|
|
1036
1227
|
setReactNavHistory(["/"]);
|
|
1037
1228
|
setReactNavIndex(0);
|
|
1038
1229
|
}
|
|
1230
|
+
if (ct === "module-federation") {
|
|
1231
|
+
setServerCollapsed(true);
|
|
1232
|
+
setClientCollapsed(false);
|
|
1233
|
+
setMfPreviewApp("host");
|
|
1234
|
+
setMfError(null);
|
|
1235
|
+
}
|
|
1039
1236
|
}
|
|
1040
1237
|
},
|
|
1041
1238
|
[clientType],
|
|
@@ -1061,12 +1258,19 @@ export default function CodeRunnerModal() {
|
|
|
1061
1258
|
...prev,
|
|
1062
1259
|
{ id: aId, role: "assistant", content: "" },
|
|
1063
1260
|
]);
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1261
|
+
const isFrontendMode =
|
|
1262
|
+
clientType === "react" ||
|
|
1263
|
+
clientType === "nextjs" ||
|
|
1264
|
+
clientType === "module-federation";
|
|
1265
|
+
const workspaceFiles = isFrontendMode
|
|
1066
1266
|
? reactFiles
|
|
1067
1267
|
: { "client.js": clientCode, "server.ts": serverCode };
|
|
1068
|
-
const labType:
|
|
1069
|
-
clientType === "nextjs"
|
|
1268
|
+
const labType: FrontendLabType =
|
|
1269
|
+
clientType === "nextjs"
|
|
1270
|
+
? "nextjs"
|
|
1271
|
+
: clientType === "module-federation"
|
|
1272
|
+
? "module-federation"
|
|
1273
|
+
: "react";
|
|
1070
1274
|
try {
|
|
1071
1275
|
const history = [...sbxChatMessages, userMsg].map((m) => ({
|
|
1072
1276
|
role: m.role,
|
|
@@ -1944,7 +2148,14 @@ export default function CodeRunnerModal() {
|
|
|
1944
2148
|
</span>
|
|
1945
2149
|
{/* Client type selector: JS / React / Next */}
|
|
1946
2150
|
<div className="flex items-center rounded overflow-hidden border border-slate-700 text-[9px] ml-1 shrink-0">
|
|
1947
|
-
{(
|
|
2151
|
+
{(
|
|
2152
|
+
[
|
|
2153
|
+
"script",
|
|
2154
|
+
"react",
|
|
2155
|
+
"nextjs",
|
|
2156
|
+
"module-federation",
|
|
2157
|
+
] as const
|
|
2158
|
+
).map((ct) => (
|
|
1948
2159
|
<button
|
|
1949
2160
|
key={ct}
|
|
1950
2161
|
type="button"
|
|
@@ -1959,7 +2170,9 @@ export default function CodeRunnerModal() {
|
|
|
1959
2170
|
? "JS"
|
|
1960
2171
|
: ct === "react"
|
|
1961
2172
|
? "React"
|
|
1962
|
-
: "
|
|
2173
|
+
: ct === "nextjs"
|
|
2174
|
+
? "Next"
|
|
2175
|
+
: "Webpack"}
|
|
1963
2176
|
</button>
|
|
1964
2177
|
))}
|
|
1965
2178
|
</div>
|
|
@@ -2023,9 +2236,11 @@ export default function CodeRunnerModal() {
|
|
|
2023
2236
|
</>
|
|
2024
2237
|
)}
|
|
2025
2238
|
{/* React/Next mode: optional URL + Preview button + edit/preview toggle for Next */}
|
|
2026
|
-
{(clientType === "react" ||
|
|
2239
|
+
{(clientType === "react" ||
|
|
2240
|
+
clientType === "nextjs" ||
|
|
2241
|
+
clientType === "module-federation") && (
|
|
2027
2242
|
<>
|
|
2028
|
-
{sandboxUrl && (
|
|
2243
|
+
{clientType !== "module-federation" && sandboxUrl && (
|
|
2029
2244
|
<span
|
|
2030
2245
|
className="text-[9px] font-mono text-slate-600 truncate max-w-[80px]"
|
|
2031
2246
|
title={sandboxUrl}
|
|
@@ -2037,7 +2252,7 @@ export default function CodeRunnerModal() {
|
|
|
2037
2252
|
{clientType === "react" && (
|
|
2038
2253
|
<button
|
|
2039
2254
|
type="button"
|
|
2040
|
-
onClick={refreshPreview}
|
|
2255
|
+
onClick={() => refreshPreview()}
|
|
2041
2256
|
className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-medium bg-cyan-600/20 hover:bg-cyan-600/40 text-cyan-400 transition-colors shrink-0"
|
|
2042
2257
|
title="Render preview"
|
|
2043
2258
|
>
|
|
@@ -2093,6 +2308,73 @@ export default function CodeRunnerModal() {
|
|
|
2093
2308
|
)}
|
|
2094
2309
|
</>
|
|
2095
2310
|
)}
|
|
2311
|
+
{clientType === "module-federation" && (
|
|
2312
|
+
<>
|
|
2313
|
+
{mfSandboxId && mfHostUrl && (
|
|
2314
|
+
<span
|
|
2315
|
+
className="text-[9px] font-mono text-slate-600 truncate max-w-[110px]"
|
|
2316
|
+
title={mfHostUrl}
|
|
2317
|
+
>
|
|
2318
|
+
{mfHostUrl.replace(/^https?:\/\//, "")}
|
|
2319
|
+
</span>
|
|
2320
|
+
)}
|
|
2321
|
+
{!mfSandboxId ? (
|
|
2322
|
+
<button
|
|
2323
|
+
type="button"
|
|
2324
|
+
onClick={() => void startModuleFederationServer()}
|
|
2325
|
+
disabled={mfStarting}
|
|
2326
|
+
className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-medium bg-cyan-600/20 hover:bg-cyan-600/40 text-cyan-400 disabled:opacity-50 transition-colors shrink-0"
|
|
2327
|
+
title="Start real webpack module federation dev servers"
|
|
2328
|
+
>
|
|
2329
|
+
{mfStarting ? (
|
|
2330
|
+
<Loader2 className="w-3 h-3 animate-spin" />
|
|
2331
|
+
) : (
|
|
2332
|
+
<Play className="w-3 h-3" />
|
|
2333
|
+
)}
|
|
2334
|
+
{mfStarting ? "Starting…" : "Run Webpack"}
|
|
2335
|
+
</button>
|
|
2336
|
+
) : (
|
|
2337
|
+
<>
|
|
2338
|
+
<div className="flex items-center rounded overflow-hidden border border-slate-700/50 text-[9px] shrink-0">
|
|
2339
|
+
<button
|
|
2340
|
+
type="button"
|
|
2341
|
+
onClick={() => setReactClientTab("edit")}
|
|
2342
|
+
className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
|
|
2343
|
+
reactClientTab === "edit"
|
|
2344
|
+
? "bg-slate-700 text-slate-200"
|
|
2345
|
+
: "text-slate-500 hover:text-slate-400"
|
|
2346
|
+
}`}
|
|
2347
|
+
title="Edit code"
|
|
2348
|
+
>
|
|
2349
|
+
<Code2 className="w-2.5 h-2.5" />
|
|
2350
|
+
</button>
|
|
2351
|
+
<button
|
|
2352
|
+
type="button"
|
|
2353
|
+
onClick={() => setReactClientTab("preview")}
|
|
2354
|
+
className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
|
|
2355
|
+
reactClientTab === "preview"
|
|
2356
|
+
? "bg-slate-700 text-slate-200"
|
|
2357
|
+
: "text-slate-500 hover:text-slate-400"
|
|
2358
|
+
}`}
|
|
2359
|
+
title="Live preview"
|
|
2360
|
+
>
|
|
2361
|
+
<Eye className="w-2.5 h-2.5" />
|
|
2362
|
+
</button>
|
|
2363
|
+
</div>
|
|
2364
|
+
<button
|
|
2365
|
+
type="button"
|
|
2366
|
+
onClick={() =>
|
|
2367
|
+
void stopModuleFederationServer()
|
|
2368
|
+
}
|
|
2369
|
+
className="p-0.5 rounded text-slate-600 hover:text-red-400 transition-colors shrink-0"
|
|
2370
|
+
title="Stop webpack lab"
|
|
2371
|
+
>
|
|
2372
|
+
<StopCircle className="w-3 h-3" />
|
|
2373
|
+
</button>
|
|
2374
|
+
</>
|
|
2375
|
+
)}
|
|
2376
|
+
</>
|
|
2377
|
+
)}
|
|
2096
2378
|
</>
|
|
2097
2379
|
)}
|
|
2098
2380
|
</div>
|
|
@@ -2215,10 +2497,11 @@ export default function CodeRunnerModal() {
|
|
|
2215
2497
|
|
|
2216
2498
|
{/* Client body */}
|
|
2217
2499
|
<div
|
|
2218
|
-
className={`flex-1 min-h-0 ${clientType === "nextjs" ? "flex flex-row" : "relative"}`}
|
|
2500
|
+
className={`flex-1 min-h-0 ${clientType === "nextjs" || clientType === "module-federation" ? "flex flex-row" : "relative"}`}
|
|
2219
2501
|
>
|
|
2220
2502
|
{/* ── Next.js VS Code-style file tree sidebar ── */}
|
|
2221
|
-
{clientType === "nextjs"
|
|
2503
|
+
{(clientType === "nextjs" ||
|
|
2504
|
+
clientType === "module-federation") && (
|
|
2222
2505
|
<div className="w-36 shrink-0 flex flex-col border-r border-slate-700 bg-slate-900/60 overflow-y-auto">
|
|
2223
2506
|
{/* Sidebar header */}
|
|
2224
2507
|
<div className="flex items-center justify-between px-2 py-1.5 border-b border-slate-700/60">
|
|
@@ -2256,7 +2539,11 @@ export default function CodeRunnerModal() {
|
|
|
2256
2539
|
setReactNewFileName("");
|
|
2257
2540
|
}
|
|
2258
2541
|
}}
|
|
2259
|
-
placeholder=
|
|
2542
|
+
placeholder={
|
|
2543
|
+
clientType === "module-federation"
|
|
2544
|
+
? "apps/orders/src/App.jsx"
|
|
2545
|
+
: "app/new.tsx"
|
|
2546
|
+
}
|
|
2260
2547
|
className="w-full bg-slate-800 border border-cyan-600/50 rounded px-1 py-0.5 text-[9px] font-mono text-slate-200 placeholder-slate-600 outline-none focus:border-cyan-500"
|
|
2261
2548
|
/>
|
|
2262
2549
|
) : (
|
|
@@ -2264,7 +2551,11 @@ export default function CodeRunnerModal() {
|
|
|
2264
2551
|
type="button"
|
|
2265
2552
|
onClick={() => setReactAddingFile(true)}
|
|
2266
2553
|
className="p-0.5 rounded text-slate-600 hover:text-cyan-400 transition-colors"
|
|
2267
|
-
title=
|
|
2554
|
+
title={
|
|
2555
|
+
clientType === "module-federation"
|
|
2556
|
+
? "New file (use paths like apps/orders/src/App.jsx)"
|
|
2557
|
+
: "New file (use paths like app/dashboard/page.tsx)"
|
|
2558
|
+
}
|
|
2268
2559
|
>
|
|
2269
2560
|
<FilePlus className="w-3 h-3" />
|
|
2270
2561
|
</button>
|
|
@@ -2273,25 +2564,7 @@ export default function CodeRunnerModal() {
|
|
|
2273
2564
|
{/* Tree nodes */}
|
|
2274
2565
|
<div className="flex-1 py-1">
|
|
2275
2566
|
{(() => {
|
|
2276
|
-
|
|
2277
|
-
const allFiles = Object.keys(reactFiles).sort(
|
|
2278
|
-
(a, b) => {
|
|
2279
|
-
const ad = a.split("/").length;
|
|
2280
|
-
const bd = b.split("/").length;
|
|
2281
|
-
return ad !== bd ? ad - bd : a.localeCompare(b);
|
|
2282
|
-
},
|
|
2283
|
-
);
|
|
2284
|
-
// Collect unique top-level folders (first path segment for nested files)
|
|
2285
|
-
const folders = Array.from(
|
|
2286
|
-
new Set(
|
|
2287
|
-
allFiles
|
|
2288
|
-
.filter((f) => f.includes("/"))
|
|
2289
|
-
.map((f) => f.split("/")[0]),
|
|
2290
|
-
),
|
|
2291
|
-
).sort();
|
|
2292
|
-
const rootFiles = allFiles.filter(
|
|
2293
|
-
(f) => !f.includes("/"),
|
|
2294
|
-
);
|
|
2567
|
+
const tree = buildFileTree(Object.keys(reactFiles));
|
|
2295
2568
|
|
|
2296
2569
|
const fileIcon = (name: string) => {
|
|
2297
2570
|
if (name.endsWith(".tsx") || name.endsWith(".jsx"))
|
|
@@ -2359,40 +2632,46 @@ export default function CodeRunnerModal() {
|
|
|
2359
2632
|
</div>
|
|
2360
2633
|
);
|
|
2361
2634
|
|
|
2362
|
-
const
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2635
|
+
const renderNode = (
|
|
2636
|
+
node: FileTreeNode,
|
|
2637
|
+
indent = 0,
|
|
2638
|
+
): React.ReactNode => {
|
|
2639
|
+
if (!node.path) {
|
|
2640
|
+
return (
|
|
2641
|
+
<>
|
|
2642
|
+
{node.children
|
|
2643
|
+
.sort((a, b) =>
|
|
2644
|
+
a.name.localeCompare(b.name),
|
|
2645
|
+
)
|
|
2646
|
+
.map((child) => renderNode(child, 0))}
|
|
2647
|
+
{node.files
|
|
2648
|
+
.sort((a, b) => a.localeCompare(b))
|
|
2649
|
+
.map((path) => renderFile(path, 0))}
|
|
2650
|
+
</>
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
const isOpen = !collapsedFolders.has(node.path);
|
|
2381
2655
|
|
|
2382
2656
|
return (
|
|
2383
|
-
<div key={
|
|
2384
|
-
{/* Folder row */}
|
|
2657
|
+
<div key={node.path}>
|
|
2385
2658
|
<button
|
|
2386
2659
|
type="button"
|
|
2387
2660
|
onClick={() =>
|
|
2388
2661
|
setCollapsedFolders((prev) => {
|
|
2389
2662
|
const next = new Set(prev);
|
|
2390
|
-
if (next.has(
|
|
2391
|
-
|
|
2663
|
+
if (next.has(node.path)) {
|
|
2664
|
+
next.delete(node.path);
|
|
2665
|
+
} else {
|
|
2666
|
+
next.add(node.path);
|
|
2667
|
+
}
|
|
2392
2668
|
return next;
|
|
2393
2669
|
})
|
|
2394
2670
|
}
|
|
2395
|
-
|
|
2671
|
+
style={{
|
|
2672
|
+
paddingLeft: `${8 + indent * 10}px`,
|
|
2673
|
+
}}
|
|
2674
|
+
className="w-full flex items-center gap-0.5 pr-2 py-0.5 text-left text-[10px] font-mono text-slate-300 hover:bg-slate-800 transition-colors select-none"
|
|
2396
2675
|
>
|
|
2397
2676
|
{isOpen ? (
|
|
2398
2677
|
<ChevronDown className="w-2.5 h-2.5 shrink-0 text-slate-500" />
|
|
@@ -2402,68 +2681,29 @@ export default function CodeRunnerModal() {
|
|
|
2402
2681
|
<span className="text-yellow-300/80 mr-0.5">
|
|
2403
2682
|
📁
|
|
2404
2683
|
</span>
|
|
2405
|
-
<span className="truncate">{
|
|
2684
|
+
<span className="truncate">{node.name}/</span>
|
|
2406
2685
|
</button>
|
|
2407
|
-
{/* Children */}
|
|
2408
2686
|
{isOpen && (
|
|
2409
2687
|
<div>
|
|
2410
|
-
{
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
)
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
<button
|
|
2423
|
-
type="button"
|
|
2424
|
-
onClick={() =>
|
|
2425
|
-
setCollapsedFolders((prev) => {
|
|
2426
|
-
const next = new Set(prev);
|
|
2427
|
-
if (next.has(sf))
|
|
2428
|
-
next.delete(sf);
|
|
2429
|
-
else next.add(sf);
|
|
2430
|
-
return next;
|
|
2431
|
-
})
|
|
2432
|
-
}
|
|
2433
|
-
className="w-full flex items-center gap-0.5 pl-[18px] pr-2 py-0.5 text-left text-[10px] font-mono text-slate-300 hover:bg-slate-800 transition-colors select-none"
|
|
2434
|
-
>
|
|
2435
|
-
{sfIsOpen ? (
|
|
2436
|
-
<ChevronDown className="w-2.5 h-2.5 shrink-0 text-slate-500" />
|
|
2437
|
-
) : (
|
|
2438
|
-
<ChevronRight className="w-2.5 h-2.5 shrink-0 text-slate-500" />
|
|
2439
|
-
)}
|
|
2440
|
-
<span className="text-yellow-300/80 mr-0.5">
|
|
2441
|
-
📁
|
|
2442
|
-
</span>
|
|
2443
|
-
<span className="truncate">
|
|
2444
|
-
{sfKey}/
|
|
2445
|
-
</span>
|
|
2446
|
-
</button>
|
|
2447
|
-
{sfIsOpen &&
|
|
2448
|
-
sfChildren.map((f) =>
|
|
2449
|
-
renderFile(f, 3),
|
|
2450
|
-
)}
|
|
2451
|
-
</div>
|
|
2452
|
-
);
|
|
2453
|
-
})}
|
|
2454
|
-
{directFiles.map((f) => renderFile(f, 1))}
|
|
2688
|
+
{node.children
|
|
2689
|
+
.sort((a, b) =>
|
|
2690
|
+
a.name.localeCompare(b.name),
|
|
2691
|
+
)
|
|
2692
|
+
.map((child) =>
|
|
2693
|
+
renderNode(child, indent + 1),
|
|
2694
|
+
)}
|
|
2695
|
+
{node.files
|
|
2696
|
+
.sort((a, b) => a.localeCompare(b))
|
|
2697
|
+
.map((path) =>
|
|
2698
|
+
renderFile(path, indent + 1),
|
|
2699
|
+
)}
|
|
2455
2700
|
</div>
|
|
2456
2701
|
)}
|
|
2457
2702
|
</div>
|
|
2458
2703
|
);
|
|
2459
2704
|
};
|
|
2460
2705
|
|
|
2461
|
-
return (
|
|
2462
|
-
<>
|
|
2463
|
-
{folders.map(renderFolder)}
|
|
2464
|
-
{rootFiles.map((f) => renderFile(f, 0))}
|
|
2465
|
-
</>
|
|
2466
|
-
);
|
|
2706
|
+
return renderNode(tree);
|
|
2467
2707
|
})()}
|
|
2468
2708
|
</div>
|
|
2469
2709
|
</div>
|
|
@@ -2471,7 +2711,7 @@ export default function CodeRunnerModal() {
|
|
|
2471
2711
|
|
|
2472
2712
|
{/* ── Editor / Preview area ── */}
|
|
2473
2713
|
<div
|
|
2474
|
-
className={`${clientType === "nextjs" ? "flex-1 min-w-0 relative" : "absolute inset-0"}`}
|
|
2714
|
+
className={`${clientType === "nextjs" || clientType === "module-federation" ? "flex-1 min-w-0 relative" : "absolute inset-0"}`}
|
|
2475
2715
|
>
|
|
2476
2716
|
{clientType === "script" ? (
|
|
2477
2717
|
<SyntaxEditor
|
|
@@ -2508,11 +2748,9 @@ export default function CodeRunnerModal() {
|
|
|
2508
2748
|
placeholder={`// ${reactActiveFile}\n`}
|
|
2509
2749
|
/>
|
|
2510
2750
|
) : (
|
|
2511
|
-
// Preview area — URL bar for Next.js, plain iframe for React
|
|
2512
2751
|
<div className="w-full h-full flex flex-col">
|
|
2513
2752
|
{clientType === "nextjs" && (
|
|
2514
2753
|
<div className="flex items-center gap-1 px-2 py-1 bg-slate-800 border-b border-slate-700 shrink-0">
|
|
2515
|
-
{/* Back */}
|
|
2516
2754
|
<button
|
|
2517
2755
|
type="button"
|
|
2518
2756
|
disabled={reactNavIndex <= 0}
|
|
@@ -2535,7 +2773,6 @@ export default function CodeRunnerModal() {
|
|
|
2535
2773
|
>
|
|
2536
2774
|
<ChevronLeft className="w-3.5 h-3.5" />
|
|
2537
2775
|
</button>
|
|
2538
|
-
{/* Forward */}
|
|
2539
2776
|
<button
|
|
2540
2777
|
type="button"
|
|
2541
2778
|
disabled={
|
|
@@ -2560,7 +2797,6 @@ export default function CodeRunnerModal() {
|
|
|
2560
2797
|
>
|
|
2561
2798
|
<ChevronRight className="w-3.5 h-3.5" />
|
|
2562
2799
|
</button>
|
|
2563
|
-
{/* Refresh */}
|
|
2564
2800
|
<button
|
|
2565
2801
|
type="button"
|
|
2566
2802
|
onClick={() => {
|
|
@@ -2582,7 +2818,6 @@ export default function CodeRunnerModal() {
|
|
|
2582
2818
|
<path d="M13.65 2.35A8 8 0 1 0 15 8h-2a6 6 0 1 1-1.1-3.48L10 6h5V1l-1.35 1.35z" />
|
|
2583
2819
|
</svg>
|
|
2584
2820
|
</button>
|
|
2585
|
-
{/* URL bar */}
|
|
2586
2821
|
<form
|
|
2587
2822
|
className="flex-1 flex items-center gap-1 bg-slate-900 border border-slate-600 rounded px-2 py-0.5 focus-within:border-blue-500/60 transition-colors"
|
|
2588
2823
|
onSubmit={(e) => {
|
|
@@ -2621,7 +2856,6 @@ export default function CodeRunnerModal() {
|
|
|
2621
2856
|
spellCheck={false}
|
|
2622
2857
|
/>
|
|
2623
2858
|
</form>
|
|
2624
|
-
{/* Status indicator */}
|
|
2625
2859
|
{nxSandboxUrl ? (
|
|
2626
2860
|
<span className="text-[9px] font-mono text-green-400 shrink-0">
|
|
2627
2861
|
● live
|
|
@@ -2648,49 +2882,103 @@ export default function CodeRunnerModal() {
|
|
|
2648
2882
|
)}
|
|
2649
2883
|
</div>
|
|
2650
2884
|
)}
|
|
2651
|
-
{
|
|
2652
|
-
|
|
2885
|
+
{clientType === "module-federation" && (
|
|
2886
|
+
<div className="flex items-center gap-1 px-2 py-1 bg-slate-800 border-b border-slate-700 shrink-0 overflow-x-auto">
|
|
2887
|
+
{Object.entries(mfAppUrls).map(([name, url]) => (
|
|
2888
|
+
<button
|
|
2889
|
+
key={name}
|
|
2890
|
+
type="button"
|
|
2891
|
+
onClick={() => setMfPreviewApp(name)}
|
|
2892
|
+
className={`px-2 py-0.5 rounded text-[10px] font-mono transition-colors shrink-0 ${
|
|
2893
|
+
mfPreviewApp === name
|
|
2894
|
+
? "bg-slate-700 text-slate-100"
|
|
2895
|
+
: "text-slate-500 hover:text-slate-300 hover:bg-slate-700/40"
|
|
2896
|
+
}`}
|
|
2897
|
+
title={url}
|
|
2898
|
+
>
|
|
2899
|
+
{name}
|
|
2900
|
+
</button>
|
|
2901
|
+
))}
|
|
2902
|
+
<div className="ml-auto text-[9px] font-mono text-slate-600 shrink-0">
|
|
2903
|
+
{mfAppUrls[mfPreviewApp] ??
|
|
2904
|
+
mfHostUrl ??
|
|
2905
|
+
"Start webpack to preview"}
|
|
2906
|
+
</div>
|
|
2907
|
+
</div>
|
|
2908
|
+
)}
|
|
2909
|
+
{((clientType === "module-federation" && mfError) ||
|
|
2910
|
+
(clientType !== "module-federation" && nxError)) && (
|
|
2653
2911
|
<div className="text-[10px] text-red-400 bg-red-950/40 border-b border-red-800 px-3 py-1.5 shrink-0 font-mono">
|
|
2654
|
-
{
|
|
2912
|
+
{clientType === "module-federation"
|
|
2913
|
+
? mfError
|
|
2914
|
+
: nxError}
|
|
2655
2915
|
</div>
|
|
2656
2916
|
)}
|
|
2657
|
-
{
|
|
2658
|
-
{nxStarting && (
|
|
2917
|
+
{(nxStarting || mfStarting) && (
|
|
2659
2918
|
<div className="flex-1 flex flex-col items-center justify-center gap-3 text-slate-400 text-sm bg-slate-950">
|
|
2660
2919
|
<Loader2 className="w-8 h-8 animate-spin text-cyan-400" />
|
|
2661
2920
|
<p className="text-[12px]">
|
|
2662
|
-
|
|
2921
|
+
{clientType === "module-federation"
|
|
2922
|
+
? "Installing dependencies and starting webpack apps…"
|
|
2923
|
+
: "Starting Next.js dev server…"}
|
|
2663
2924
|
</p>
|
|
2664
|
-
<p className="text-[10px] text-slate-600">
|
|
2665
|
-
|
|
2925
|
+
<p className="text-[10px] text-slate-600 max-w-md text-center px-4">
|
|
2926
|
+
{clientType === "module-federation"
|
|
2927
|
+
? "The first run can take a little longer because npm install runs inside the lab sandbox."
|
|
2928
|
+
: "This takes ~10 seconds on the first run"}
|
|
2666
2929
|
</p>
|
|
2667
2930
|
</div>
|
|
2668
2931
|
)}
|
|
2669
|
-
{
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2932
|
+
{!nxStarting &&
|
|
2933
|
+
clientType === "nextjs" &&
|
|
2934
|
+
nxSandboxUrl && (
|
|
2935
|
+
<iframe
|
|
2936
|
+
ref={nxIframeRef}
|
|
2937
|
+
src={nxSandboxUrl + reactPreviewPath}
|
|
2938
|
+
className="flex-1 min-h-0 w-full border-0 bg-white"
|
|
2939
|
+
title="Next.js Preview"
|
|
2940
|
+
onLoad={() => {
|
|
2941
|
+
try {
|
|
2942
|
+
const p =
|
|
2943
|
+
nxIframeRef.current?.contentWindow?.location
|
|
2944
|
+
.pathname;
|
|
2945
|
+
if (p) {
|
|
2946
|
+
setReactPreviewPath(p);
|
|
2947
|
+
setReactNavInput(p);
|
|
2948
|
+
}
|
|
2949
|
+
} catch {
|
|
2950
|
+
// cross-origin — ignore
|
|
2685
2951
|
}
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2952
|
+
}}
|
|
2953
|
+
/>
|
|
2954
|
+
)}
|
|
2955
|
+
{!mfStarting &&
|
|
2956
|
+
clientType === "module-federation" &&
|
|
2957
|
+
mfSandboxId &&
|
|
2958
|
+
(mfAppUrls[mfPreviewApp] ?? mfHostUrl) && (
|
|
2959
|
+
<iframe
|
|
2960
|
+
src={mfAppUrls[mfPreviewApp] ?? mfHostUrl ?? ""}
|
|
2961
|
+
className="flex-1 min-h-0 w-full border-0 bg-white"
|
|
2962
|
+
title="Webpack Module Federation Preview"
|
|
2963
|
+
/>
|
|
2964
|
+
)}
|
|
2965
|
+
{!mfStarting &&
|
|
2966
|
+
clientType === "module-federation" &&
|
|
2967
|
+
!mfSandboxId && (
|
|
2968
|
+
<div className="flex-1 flex flex-col items-center justify-center gap-3 text-slate-400 text-sm bg-slate-950 px-6 text-center">
|
|
2969
|
+
<Server className="w-8 h-8 text-cyan-400/70" />
|
|
2970
|
+
<p className="text-[12px]">
|
|
2971
|
+
Start the webpack lab to launch the host and
|
|
2972
|
+
remote dev servers.
|
|
2973
|
+
</p>
|
|
2974
|
+
<p className="text-[10px] text-slate-600 max-w-md">
|
|
2975
|
+
The preview lets you switch between the host and
|
|
2976
|
+
each remote so you can inspect their output
|
|
2977
|
+
independently.
|
|
2978
|
+
</p>
|
|
2979
|
+
</div>
|
|
2980
|
+
)}
|
|
2981
|
+
{!nxStarting && clientType === "react" && (
|
|
2694
2982
|
<iframe
|
|
2695
2983
|
srcDoc={reactPreviewSrc ?? ""}
|
|
2696
2984
|
sandbox="allow-scripts"
|
|
@@ -2815,7 +3103,9 @@ export default function CodeRunnerModal() {
|
|
|
2815
3103
|
!serverStarting &&
|
|
2816
3104
|
!clientRunning && (
|
|
2817
3105
|
<span className="text-slate-600">
|
|
2818
|
-
|
|
3106
|
+
{clientType === "module-federation"
|
|
3107
|
+
? "Run webpack to start the host and remotes"
|
|
3108
|
+
: "Start the server, then run the client"}
|
|
2819
3109
|
</span>
|
|
2820
3110
|
)}
|
|
2821
3111
|
{sandboxOutput.map((line, i) => (
|
|
@@ -2864,9 +3154,13 @@ export default function CodeRunnerModal() {
|
|
|
2864
3154
|
{sbxChatMessages.length === 0 && (
|
|
2865
3155
|
<p className="text-xs text-slate-600 pt-1">
|
|
2866
3156
|
Ask anything about your code —{" "}
|
|
2867
|
-
{clientType === "react" ||
|
|
3157
|
+
{clientType === "react" ||
|
|
3158
|
+
clientType === "nextjs" ||
|
|
3159
|
+
clientType === "module-federation" ? (
|
|
2868
3160
|
<span className="text-slate-500">
|
|
2869
|
-
|
|
3161
|
+
{clientType === "module-federation"
|
|
3162
|
+
? '"Why is remoteEntry.js 404ing?"'
|
|
3163
|
+
: '"Why does my useEffect run twice?"'}
|
|
2870
3164
|
</span>
|
|
2871
3165
|
) : (
|
|
2872
3166
|
<span className="text-slate-500">
|
|
@@ -3002,7 +3296,7 @@ export default function CodeRunnerModal() {
|
|
|
3002
3296
|
void handleSbxChatSend();
|
|
3003
3297
|
}
|
|
3004
3298
|
}}
|
|
3005
|
-
placeholder={`Ask about your ${clientType === "react" ? "React" : clientType === "nextjs" ? "Next.js" : "sandbox"} code…`}
|
|
3299
|
+
placeholder={`Ask about your ${clientType === "react" ? "React" : clientType === "nextjs" ? "Next.js" : clientType === "module-federation" ? "webpack module federation" : "sandbox"} code…`}
|
|
3006
3300
|
disabled={sbxChatLoading}
|
|
3007
3301
|
className="flex-1 bg-transparent text-xs text-slate-200 placeholder-slate-600 outline-none resize-none disabled:opacity-50 max-h-20"
|
|
3008
3302
|
/>
|