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.
- 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 +626 -0
- package/template/client/src/components/Sidebar.tsx +97 -55
- package/template/client/src/reactLab.ts +408 -0
- 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 +77 -24
- package/template/server/src/storage.ts +31 -0
|
@@ -106,6 +106,7 @@ interface Store {
|
|
|
106
106
|
expandedTopics: string[];
|
|
107
107
|
availableFiles: string[];
|
|
108
108
|
showCodePanel: boolean;
|
|
109
|
+
showLabsPanel: boolean;
|
|
109
110
|
showSidebar: boolean;
|
|
110
111
|
viewingFile: string | null;
|
|
111
112
|
viewingDoc: { fileId: string; quote: string; fileName: string } | null;
|
|
@@ -183,6 +184,7 @@ interface Store {
|
|
|
183
184
|
selectQuestion: (topicId: string, questionId: string) => Promise<void>;
|
|
184
185
|
toggleTopic: (topicId: string) => void;
|
|
185
186
|
toggleCodePanel: () => void;
|
|
187
|
+
toggleLabsPanel: () => void;
|
|
186
188
|
toggleSidebar: () => void;
|
|
187
189
|
fetchAvailableFiles: () => Promise<void>;
|
|
188
190
|
updateCodeContext: (questionId: string, files: string[]) => Promise<void>;
|
|
@@ -202,6 +204,8 @@ interface Store {
|
|
|
202
204
|
files: FileList | File[],
|
|
203
205
|
) => Promise<void>;
|
|
204
206
|
removeQuestionFile: (questionId: string, fileId: string) => Promise<void>;
|
|
207
|
+
detachLabFile: (questionId: string, fileId: string) => Promise<void>;
|
|
208
|
+
attachLabFile: (questionId: string, fileId: string) => Promise<void>;
|
|
205
209
|
linkFileToQuestion: (
|
|
206
210
|
questionId: string,
|
|
207
211
|
fileId: string,
|
|
@@ -311,6 +315,11 @@ interface Store {
|
|
|
311
315
|
) => Promise<void>;
|
|
312
316
|
closeCodeRunner: () => void;
|
|
313
317
|
|
|
318
|
+
// ── Deployment Lab ──────────────────────────────────────────
|
|
319
|
+
showDeploymentLab: boolean;
|
|
320
|
+
openDeploymentLab: () => void;
|
|
321
|
+
closeDeploymentLab: () => void;
|
|
322
|
+
|
|
314
323
|
// ── Infra Lab ────────────────────────────────────────────────
|
|
315
324
|
showInfraLab: boolean;
|
|
316
325
|
runnerInitialInfra: InfraLabWorkspace | null;
|
|
@@ -349,6 +358,7 @@ export const useStore = create<Store>((set, get) => ({
|
|
|
349
358
|
expandedTopics: [],
|
|
350
359
|
availableFiles: [],
|
|
351
360
|
showCodePanel: false,
|
|
361
|
+
showLabsPanel: false,
|
|
352
362
|
showSidebar: true,
|
|
353
363
|
viewingFile: null,
|
|
354
364
|
viewingDoc: null,
|
|
@@ -362,6 +372,7 @@ export const useStore = create<Store>((set, get) => ({
|
|
|
362
372
|
runnerInitialLanguage: "typescript",
|
|
363
373
|
runnerInitialSandbox: null,
|
|
364
374
|
runnerInitialFileId: null,
|
|
375
|
+
showDeploymentLab: false,
|
|
365
376
|
showInfraLab: false,
|
|
366
377
|
runnerInitialInfra: null,
|
|
367
378
|
runnerInitialInfraFileId: null,
|
|
@@ -671,13 +682,20 @@ export const useStore = create<Store>((set, get) => ({
|
|
|
671
682
|
},
|
|
672
683
|
|
|
673
684
|
toggleCodePanel: () => {
|
|
674
|
-
set((s) => ({
|
|
685
|
+
set((s) => ({
|
|
686
|
+
showCodePanel: !s.showCodePanel,
|
|
687
|
+
showLabsPanel: false,
|
|
688
|
+
}));
|
|
675
689
|
const { availableFiles, fetchAvailableFiles } = get();
|
|
676
690
|
if (availableFiles.length === 0) {
|
|
677
691
|
fetchAvailableFiles();
|
|
678
692
|
}
|
|
679
693
|
},
|
|
680
694
|
|
|
695
|
+
toggleLabsPanel: () => {
|
|
696
|
+
set((s) => ({ showLabsPanel: !s.showLabsPanel, showCodePanel: false }));
|
|
697
|
+
},
|
|
698
|
+
|
|
681
699
|
fetchAvailableFiles: async () => {
|
|
682
700
|
const files = await api.fetchCodeContextTree();
|
|
683
701
|
set({ availableFiles: files });
|
|
@@ -769,6 +787,36 @@ export const useStore = create<Store>((set, get) => ({
|
|
|
769
787
|
}));
|
|
770
788
|
},
|
|
771
789
|
|
|
790
|
+
detachLabFile: async (questionId, fileId) => {
|
|
791
|
+
const cf = await api.detachQuestionLabFile(questionId, fileId);
|
|
792
|
+
set((s) => ({
|
|
793
|
+
currentQuestion:
|
|
794
|
+
s.currentQuestion?.id === questionId
|
|
795
|
+
? {
|
|
796
|
+
...s.currentQuestion,
|
|
797
|
+
contextFiles: (s.currentQuestion.contextFiles || []).map((f) =>
|
|
798
|
+
f.id === fileId ? { ...f, inContext: cf.inContext } : f,
|
|
799
|
+
),
|
|
800
|
+
}
|
|
801
|
+
: s.currentQuestion,
|
|
802
|
+
}));
|
|
803
|
+
},
|
|
804
|
+
|
|
805
|
+
attachLabFile: async (questionId, fileId) => {
|
|
806
|
+
const cf = await api.attachQuestionLabFile(questionId, fileId);
|
|
807
|
+
set((s) => ({
|
|
808
|
+
currentQuestion:
|
|
809
|
+
s.currentQuestion?.id === questionId
|
|
810
|
+
? {
|
|
811
|
+
...s.currentQuestion,
|
|
812
|
+
contextFiles: (s.currentQuestion.contextFiles || []).map((f) =>
|
|
813
|
+
f.id === fileId ? { ...f, inContext: cf.inContext } : f,
|
|
814
|
+
),
|
|
815
|
+
}
|
|
816
|
+
: s.currentQuestion,
|
|
817
|
+
}));
|
|
818
|
+
},
|
|
819
|
+
|
|
772
820
|
linkFileToQuestion: async (questionId, fileId, originalName) => {
|
|
773
821
|
const cf = await api.linkFileToQuestion(questionId, fileId, originalName);
|
|
774
822
|
set((s) => ({
|
|
@@ -1010,6 +1058,9 @@ export const useStore = create<Store>((set, get) => ({
|
|
|
1010
1058
|
}));
|
|
1011
1059
|
},
|
|
1012
1060
|
closeCodeRunner: () => set({ showCodeRunner: false }),
|
|
1061
|
+
showDeploymentLab: false,
|
|
1062
|
+
openDeploymentLab: () => set({ showDeploymentLab: true }),
|
|
1063
|
+
closeDeploymentLab: () => set({ showDeploymentLab: false }),
|
|
1013
1064
|
closeInfraLab: () => set({ showInfraLab: false }),
|
|
1014
1065
|
|
|
1015
1066
|
fetchAiSettings: async () => {
|
|
@@ -23,6 +23,8 @@ export interface ContextFile {
|
|
|
23
23
|
language?: string;
|
|
24
24
|
/** Short display label for code snippets. */
|
|
25
25
|
label?: string;
|
|
26
|
+
/** When false, file is saved but excluded from AI prompt context (detached lab). */
|
|
27
|
+
inContext?: boolean;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface FrontendLabWorkspace {
|
package/template/cockpit.json
CHANGED
|
@@ -684,6 +684,38 @@ app.delete(
|
|
|
684
684
|
},
|
|
685
685
|
);
|
|
686
686
|
|
|
687
|
+
// Detach a lab file from AI context without deleting it
|
|
688
|
+
app.post(
|
|
689
|
+
"/api/questions/:questionId/context-files/:fileId/detach",
|
|
690
|
+
async (req, res) => {
|
|
691
|
+
try {
|
|
692
|
+
const cf = await storage.detachQuestionContextFile(
|
|
693
|
+
req.params.questionId,
|
|
694
|
+
req.params.fileId,
|
|
695
|
+
);
|
|
696
|
+
res.json(cf);
|
|
697
|
+
} catch (err: any) {
|
|
698
|
+
res.status(500).json({ error: err?.message || "Failed to detach" });
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
// Re-attach a previously detached lab file to AI context
|
|
704
|
+
app.post(
|
|
705
|
+
"/api/questions/:questionId/context-files/:fileId/attach",
|
|
706
|
+
async (req, res) => {
|
|
707
|
+
try {
|
|
708
|
+
const cf = await storage.attachQuestionContextFile(
|
|
709
|
+
req.params.questionId,
|
|
710
|
+
req.params.fileId,
|
|
711
|
+
);
|
|
712
|
+
res.json(cf);
|
|
713
|
+
} catch (err: any) {
|
|
714
|
+
res.status(500).json({ error: err?.message || "Failed to attach" });
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
);
|
|
718
|
+
|
|
687
719
|
// Save a code snippet (from Code Runner or AI response) as a question context file
|
|
688
720
|
app.post("/api/questions/:questionId/save-code-snippet", async (req, res) => {
|
|
689
721
|
const { code, language, label, origin } = req.body as {
|
|
@@ -1376,7 +1408,9 @@ app.post("/api/chat", async (req, res) => {
|
|
|
1376
1408
|
if (questionId) {
|
|
1377
1409
|
const question = await storage.getQuestion(questionId);
|
|
1378
1410
|
if (question?.contextFiles?.length) {
|
|
1379
|
-
for (const cf of question.contextFiles
|
|
1411
|
+
for (const cf of question.contextFiles.filter(
|
|
1412
|
+
(c) => c.inContext !== false,
|
|
1413
|
+
)) {
|
|
1380
1414
|
fileRegistry.set(cf.id, {
|
|
1381
1415
|
label: `[question] ${cf.originalName}`,
|
|
1382
1416
|
reader: () => storage.readContextFileContent(cf.id),
|
|
@@ -2555,17 +2589,19 @@ function getModuleFederationCommandEnv(
|
|
|
2555
2589
|
sandbox: ModuleFederationSandboxEntry,
|
|
2556
2590
|
): NodeJS.ProcessEnv {
|
|
2557
2591
|
const hostPort = new URL(sandbox.appUrls.host).port;
|
|
2558
|
-
const
|
|
2559
|
-
const checkoutPort = new URL(sandbox.appUrls.checkout).port;
|
|
2560
|
-
|
|
2561
|
-
return {
|
|
2592
|
+
const env: NodeJS.ProcessEnv = {
|
|
2562
2593
|
...process.env,
|
|
2563
2594
|
HOST_PORT: hostPort,
|
|
2564
|
-
PROFILE_PORT: profilePort,
|
|
2565
|
-
CHECKOUT_PORT: checkoutPort,
|
|
2566
2595
|
MF_SANDBOX_ID: sandbox.id,
|
|
2567
2596
|
npm_config_update_notifier: "false",
|
|
2568
2597
|
};
|
|
2598
|
+
if (sandbox.appUrls.profile)
|
|
2599
|
+
env.PROFILE_PORT = new URL(sandbox.appUrls.profile).port;
|
|
2600
|
+
if (sandbox.appUrls.checkout)
|
|
2601
|
+
env.CHECKOUT_PORT = new URL(sandbox.appUrls.checkout).port;
|
|
2602
|
+
if (sandbox.appUrls.mfeAuth)
|
|
2603
|
+
env.MFE_AUTH_PORT = new URL(sandbox.appUrls.mfeAuth).port;
|
|
2604
|
+
return env;
|
|
2569
2605
|
}
|
|
2570
2606
|
|
|
2571
2607
|
async function runStreamedCommand(
|
|
@@ -2900,24 +2936,42 @@ app.post("/api/module-federation/start", async (req, res) => {
|
|
|
2900
2936
|
logs,
|
|
2901
2937
|
);
|
|
2902
2938
|
|
|
2903
|
-
|
|
2904
|
-
const
|
|
2939
|
+
// Detect isolated 2-app pattern (host + mfe-auth, no profile/checkout).
|
|
2940
|
+
const isIsolated =
|
|
2941
|
+
typeof files["apps/mfe-auth/package.json"] === "string" &&
|
|
2942
|
+
typeof files["apps/checkout/package.json"] !== "string";
|
|
2943
|
+
|
|
2944
|
+
const ports = await getDistinctPorts(isIsolated ? 2 : 3);
|
|
2945
|
+
const [hostPort] = ports;
|
|
2946
|
+
|
|
2947
|
+
const appUrls: Record<string, string> = {
|
|
2905
2948
|
host: `http://localhost:${hostPort}`,
|
|
2906
|
-
profile: `http://localhost:${profilePort}`,
|
|
2907
|
-
checkout: `http://localhost:${checkoutPort}`,
|
|
2908
2949
|
};
|
|
2950
|
+
const spawnEnv: NodeJS.ProcessEnv = {
|
|
2951
|
+
...process.env,
|
|
2952
|
+
HOST_PORT: String(hostPort),
|
|
2953
|
+
MF_SANDBOX_ID: id,
|
|
2954
|
+
npm_config_update_notifier: "false",
|
|
2955
|
+
};
|
|
2956
|
+
|
|
2957
|
+
if (isIsolated) {
|
|
2958
|
+
const [, mfeAuthPort] = ports;
|
|
2959
|
+
appUrls.mfeAuth = `http://localhost:${mfeAuthPort}`;
|
|
2960
|
+
spawnEnv.MFE_AUTH_PORT = String(mfeAuthPort);
|
|
2961
|
+
} else {
|
|
2962
|
+
const [, profilePort, checkoutPort] = ports;
|
|
2963
|
+
appUrls.profile = `http://localhost:${profilePort}`;
|
|
2964
|
+
appUrls.checkout = `http://localhost:${checkoutPort}`;
|
|
2965
|
+
spawnEnv.PROFILE_PORT = String(profilePort);
|
|
2966
|
+
spawnEnv.CHECKOUT_PORT = String(checkoutPort);
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2909
2969
|
const readyPorts = new Set<string>();
|
|
2970
|
+
const requiredPorts = isIsolated ? 2 : 3;
|
|
2910
2971
|
|
|
2911
2972
|
const child = spawn(npmCommand(), ["run", "dev"], {
|
|
2912
2973
|
cwd: dir,
|
|
2913
|
-
env:
|
|
2914
|
-
...process.env,
|
|
2915
|
-
HOST_PORT: String(hostPort),
|
|
2916
|
-
PROFILE_PORT: String(profilePort),
|
|
2917
|
-
CHECKOUT_PORT: String(checkoutPort),
|
|
2918
|
-
MF_SANDBOX_ID: id,
|
|
2919
|
-
npm_config_update_notifier: "false",
|
|
2920
|
-
},
|
|
2974
|
+
env: spawnEnv,
|
|
2921
2975
|
});
|
|
2922
2976
|
|
|
2923
2977
|
const entry: ModuleFederationSandboxEntry = {
|
|
@@ -2932,11 +2986,10 @@ app.post("/api/module-federation/start", async (req, res) => {
|
|
|
2932
2986
|
};
|
|
2933
2987
|
|
|
2934
2988
|
const markReady = (text: string) => {
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
if (readyPorts.size === 3) {
|
|
2989
|
+
for (const [key, url] of Object.entries(appUrls)) {
|
|
2990
|
+
if (text.includes(new URL(url).host)) readyPorts.add(key);
|
|
2991
|
+
}
|
|
2992
|
+
if (readyPorts.size >= requiredPorts) {
|
|
2940
2993
|
entry.ready = true;
|
|
2941
2994
|
}
|
|
2942
2995
|
};
|
|
@@ -76,6 +76,9 @@ export interface ContextFile {
|
|
|
76
76
|
language?: string;
|
|
77
77
|
/** Short display label for code snippets. */
|
|
78
78
|
label?: string;
|
|
79
|
+
/** When false, the file is saved but excluded from the AI prompt context.
|
|
80
|
+
* Used to detach lab files without permanently deleting them. */
|
|
81
|
+
inContext?: boolean;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
export interface Message {
|
|
@@ -802,6 +805,34 @@ export async function deleteQuestionContextFile(
|
|
|
802
805
|
}
|
|
803
806
|
}
|
|
804
807
|
|
|
808
|
+
/** Removes a lab file from the AI context without deleting it from disk. */
|
|
809
|
+
export async function detachQuestionContextFile(
|
|
810
|
+
questionId: string,
|
|
811
|
+
fileId: string,
|
|
812
|
+
): Promise<ContextFile> {
|
|
813
|
+
const q = await getQuestion(questionId);
|
|
814
|
+
if (!q) throw new Error("Question not found");
|
|
815
|
+
const cf = (q.contextFiles || []).find((f) => f.id === fileId);
|
|
816
|
+
if (!cf) throw new Error("Context file not found");
|
|
817
|
+
cf.inContext = false;
|
|
818
|
+
await saveQuestion(q);
|
|
819
|
+
return cf;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** Re-attaches a detached lab file to the AI context. */
|
|
823
|
+
export async function attachQuestionContextFile(
|
|
824
|
+
questionId: string,
|
|
825
|
+
fileId: string,
|
|
826
|
+
): Promise<ContextFile> {
|
|
827
|
+
const q = await getQuestion(questionId);
|
|
828
|
+
if (!q) throw new Error("Question not found");
|
|
829
|
+
const cf = (q.contextFiles || []).find((f) => f.id === fileId);
|
|
830
|
+
if (!cf) throw new Error("Context file not found");
|
|
831
|
+
cf.inContext = true;
|
|
832
|
+
await saveQuestion(q);
|
|
833
|
+
return cf;
|
|
834
|
+
}
|
|
835
|
+
|
|
805
836
|
export async function updateQuestionMessages(
|
|
806
837
|
questionId: string,
|
|
807
838
|
messages: Message[],
|