create-interview-cockpit 0.11.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 +119 -0
- package/template/client/src/components/CodeContextPanel.tsx +0 -622
- package/template/client/src/components/CodeRunnerModal.tsx +426 -240
- 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/reactLab.ts +96 -31
- 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 +310 -1
- package/template/server/src/storage.ts +31 -0
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
Globe,
|
|
20
20
|
SlidersHorizontal,
|
|
21
21
|
ArrowRightLeft,
|
|
22
|
+
MoreHorizontal,
|
|
22
23
|
} from "lucide-react";
|
|
23
24
|
|
|
24
25
|
const ROOT_PARENT_VALUE = "__root__";
|
|
@@ -75,6 +76,9 @@ export default function Sidebar() {
|
|
|
75
76
|
const [collapsedQuestions, setCollapsedQuestions] = useState<Set<string>>(
|
|
76
77
|
new Set(),
|
|
77
78
|
);
|
|
79
|
+
const [openMenuQuestionId, setOpenMenuQuestionId] = useState<string | null>(
|
|
80
|
+
null,
|
|
81
|
+
);
|
|
78
82
|
const [openTopicPrompts, setOpenTopicPrompts] = useState<Set<string>>(
|
|
79
83
|
new Set(),
|
|
80
84
|
);
|
|
@@ -320,6 +324,7 @@ export default function Sidebar() {
|
|
|
320
324
|
) => {
|
|
321
325
|
// 12px base left padding + 16px per depth level
|
|
322
326
|
const paddingLeft = 12 + depth * 16;
|
|
327
|
+
const isMenuOpen = openMenuQuestionId === q.id;
|
|
323
328
|
return (
|
|
324
329
|
<div
|
|
325
330
|
key={q.id}
|
|
@@ -371,7 +376,8 @@ export default function Sidebar() {
|
|
|
371
376
|
/>
|
|
372
377
|
) : (
|
|
373
378
|
<span
|
|
374
|
-
className="text-xs text-slate-400 truncate flex-1"
|
|
379
|
+
className="text-xs text-slate-400 truncate flex-1 min-w-0"
|
|
380
|
+
title={q.title}
|
|
375
381
|
onDoubleClick={(e) => {
|
|
376
382
|
e.stopPropagation();
|
|
377
383
|
setEditingQuestionId(q.id);
|
|
@@ -381,63 +387,99 @@ export default function Sidebar() {
|
|
|
381
387
|
{q.title}
|
|
382
388
|
</span>
|
|
383
389
|
)}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
</span>
|
|
387
|
-
{editingQuestionId !== q.id && (
|
|
388
|
-
<button
|
|
389
|
-
onClick={(e) => {
|
|
390
|
-
e.stopPropagation();
|
|
391
|
-
setAddingChildTo(q.id);
|
|
392
|
-
setNewChildTitle("");
|
|
393
|
-
}}
|
|
394
|
-
className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-cyan-400 transition-all"
|
|
395
|
-
title="Add child question"
|
|
396
|
-
>
|
|
397
|
-
<CornerDownRight className="w-2.5 h-2.5" />
|
|
398
|
-
</button>
|
|
399
|
-
)}
|
|
400
|
-
{editingQuestionId !== q.id && (
|
|
401
|
-
<button
|
|
402
|
-
onClick={(e) => {
|
|
403
|
-
e.stopPropagation();
|
|
404
|
-
setEditingQuestionId(q.id);
|
|
405
|
-
setEditingQuestionTitle(q.title);
|
|
406
|
-
}}
|
|
407
|
-
className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-cyan-400 transition-all"
|
|
408
|
-
title="Rename"
|
|
409
|
-
>
|
|
410
|
-
<Pencil className="w-2.5 h-2.5" />
|
|
411
|
-
</button>
|
|
412
|
-
)}
|
|
390
|
+
|
|
391
|
+
{/* Right side: count fades on hover, replaced by "..." menu */}
|
|
413
392
|
{editingQuestionId !== q.id && (
|
|
414
|
-
<
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
setMovingQuestionId((prev) => (prev === q.id ? null : q.id));
|
|
418
|
-
setMoveTargetParentId(q.parentQuestionId ?? ROOT_PARENT_VALUE);
|
|
419
|
-
}}
|
|
420
|
-
className={`p-0.5 rounded opacity-0 group-hover:opacity-100 transition-all ${
|
|
421
|
-
movingQuestionId === q.id
|
|
422
|
-
? "opacity-100 text-cyan-400"
|
|
423
|
-
: "text-slate-600 hover:text-cyan-400"
|
|
424
|
-
}`}
|
|
425
|
-
title="Move to a different parent"
|
|
393
|
+
<div
|
|
394
|
+
className="relative shrink-0 flex items-center"
|
|
395
|
+
onClick={(e) => e.stopPropagation()}
|
|
426
396
|
>
|
|
427
|
-
|
|
428
|
-
|
|
397
|
+
{/* Count — hidden while hovering or when menu is open */}
|
|
398
|
+
<span
|
|
399
|
+
className={`text-[10px] text-slate-700 ${
|
|
400
|
+
isMenuOpen ? "hidden" : "group-hover:hidden"
|
|
401
|
+
}`}
|
|
402
|
+
>
|
|
403
|
+
{q.messages.length > 0 ? `${q.messages.length}` : ""}
|
|
404
|
+
</span>
|
|
405
|
+
|
|
406
|
+
{/* "..." button — shown on hover or while menu is open */}
|
|
407
|
+
<button
|
|
408
|
+
onClick={() => setOpenMenuQuestionId(isMenuOpen ? null : q.id)}
|
|
409
|
+
className={`p-0.5 rounded transition-all ${
|
|
410
|
+
isMenuOpen
|
|
411
|
+
? "text-cyan-400"
|
|
412
|
+
: "opacity-0 group-hover:opacity-100 text-slate-500 hover:text-slate-300"
|
|
413
|
+
}`}
|
|
414
|
+
title="More options"
|
|
415
|
+
>
|
|
416
|
+
<MoreHorizontal className="w-3.5 h-3.5" />
|
|
417
|
+
</button>
|
|
418
|
+
|
|
419
|
+
{/* Dropdown */}
|
|
420
|
+
{isMenuOpen && (
|
|
421
|
+
<>
|
|
422
|
+
{/* Backdrop — closes menu when clicking outside */}
|
|
423
|
+
<div
|
|
424
|
+
className="fixed inset-0 z-40"
|
|
425
|
+
onClick={() => setOpenMenuQuestionId(null)}
|
|
426
|
+
/>
|
|
427
|
+
<div className="absolute right-0 top-full mt-0.5 z-50 bg-slate-800 border border-slate-700 rounded-md shadow-xl min-w-[140px] py-0.5">
|
|
428
|
+
<button
|
|
429
|
+
onClick={() => {
|
|
430
|
+
setOpenMenuQuestionId(null);
|
|
431
|
+
setEditingQuestionId(q.id);
|
|
432
|
+
setEditingQuestionTitle(q.title);
|
|
433
|
+
}}
|
|
434
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-white transition-colors"
|
|
435
|
+
>
|
|
436
|
+
<Pencil className="w-3 h-3" /> Rename
|
|
437
|
+
</button>
|
|
438
|
+
<button
|
|
439
|
+
onClick={() => {
|
|
440
|
+
setOpenMenuQuestionId(null);
|
|
441
|
+
setAddingChildTo(q.id);
|
|
442
|
+
setNewChildTitle("");
|
|
443
|
+
}}
|
|
444
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-white transition-colors"
|
|
445
|
+
>
|
|
446
|
+
<CornerDownRight className="w-3 h-3" /> Add child
|
|
447
|
+
</button>
|
|
448
|
+
<button
|
|
449
|
+
onClick={() => {
|
|
450
|
+
setOpenMenuQuestionId(null);
|
|
451
|
+
setMovingQuestionId((prev) =>
|
|
452
|
+
prev === q.id ? null : q.id,
|
|
453
|
+
);
|
|
454
|
+
setMoveTargetParentId(
|
|
455
|
+
q.parentQuestionId ?? ROOT_PARENT_VALUE,
|
|
456
|
+
);
|
|
457
|
+
}}
|
|
458
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-white transition-colors"
|
|
459
|
+
>
|
|
460
|
+
<ArrowRightLeft className="w-3 h-3" /> Move
|
|
461
|
+
</button>
|
|
462
|
+
<div className="border-t border-slate-700 my-0.5" />
|
|
463
|
+
<button
|
|
464
|
+
onClick={() => {
|
|
465
|
+
setOpenMenuQuestionId(null);
|
|
466
|
+
if (
|
|
467
|
+
window.confirm(
|
|
468
|
+
`Delete "${q.title}"? This cannot be undone.`,
|
|
469
|
+
)
|
|
470
|
+
) {
|
|
471
|
+
removeQuestion(q.id, topicId);
|
|
472
|
+
}
|
|
473
|
+
}}
|
|
474
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-slate-700 hover:text-red-300 transition-colors"
|
|
475
|
+
>
|
|
476
|
+
<Trash2 className="w-3 h-3" /> Delete
|
|
477
|
+
</button>
|
|
478
|
+
</div>
|
|
479
|
+
</>
|
|
480
|
+
)}
|
|
481
|
+
</div>
|
|
429
482
|
)}
|
|
430
|
-
<button
|
|
431
|
-
onClick={(e) => {
|
|
432
|
-
e.stopPropagation();
|
|
433
|
-
if (window.confirm(`Delete "${q.title}"? This cannot be undone.`)) {
|
|
434
|
-
removeQuestion(q.id, topicId);
|
|
435
|
-
}
|
|
436
|
-
}}
|
|
437
|
-
className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 transition-all"
|
|
438
|
-
>
|
|
439
|
-
<Trash2 className="w-2.5 h-2.5" />
|
|
440
|
-
</button>
|
|
441
483
|
</div>
|
|
442
484
|
);
|
|
443
485
|
};
|
|
@@ -5,7 +5,65 @@ export type FrontendLabType = FrontendLabWorkspace["type"];
|
|
|
5
5
|
// ── Default file contents ────────────────────────────────────────────────────
|
|
6
6
|
|
|
7
7
|
const REACT_DEFAULT_FILES: Record<string, string> = {
|
|
8
|
-
"
|
|
8
|
+
"package.json": `{
|
|
9
|
+
"name": "react-lab",
|
|
10
|
+
"private": true,
|
|
11
|
+
"version": "0.0.0",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "vite",
|
|
15
|
+
"build": "tsc -b && vite build",
|
|
16
|
+
"preview": "vite preview"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"react": "^18.3.1",
|
|
20
|
+
"react-dom": "^18.3.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/react": "^18.3.1",
|
|
24
|
+
"@types/react-dom": "^18.3.1",
|
|
25
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
26
|
+
"typescript": "^5.6.2",
|
|
27
|
+
"vite": "^6.0.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
`,
|
|
31
|
+
"vite.config.ts": `import { defineConfig } from "vite";
|
|
32
|
+
import react from "@vitejs/plugin-react";
|
|
33
|
+
|
|
34
|
+
export default defineConfig({
|
|
35
|
+
plugins: [react()],
|
|
36
|
+
server: {
|
|
37
|
+
headers: {
|
|
38
|
+
"X-Frame-Options": "ALLOWALL",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
`,
|
|
43
|
+
"index.html": `<!DOCTYPE html>
|
|
44
|
+
<html lang="en">
|
|
45
|
+
<head>
|
|
46
|
+
<meta charset="UTF-8" />
|
|
47
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
48
|
+
<title>React Lab</title>
|
|
49
|
+
</head>
|
|
50
|
+
<body>
|
|
51
|
+
<div id="root"></div>
|
|
52
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
55
|
+
`,
|
|
56
|
+
"src/main.tsx": `import React from "react";
|
|
57
|
+
import ReactDOM from "react-dom/client";
|
|
58
|
+
import App from "./App";
|
|
59
|
+
|
|
60
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
61
|
+
<React.StrictMode>
|
|
62
|
+
<App />
|
|
63
|
+
</React.StrictMode>,
|
|
64
|
+
);
|
|
65
|
+
`,
|
|
66
|
+
"src/App.tsx": `import { useState } from "react";
|
|
9
67
|
import { Counter } from "./Counter";
|
|
10
68
|
import type { User } from "./types";
|
|
11
69
|
|
|
@@ -30,7 +88,7 @@ export default function App() {
|
|
|
30
88
|
);
|
|
31
89
|
}
|
|
32
90
|
`,
|
|
33
|
-
"Counter.tsx": `import { useState, useCallback } from "react";
|
|
91
|
+
"src/Counter.tsx": `import { useState, useCallback } from "react";
|
|
34
92
|
import type { CounterProps } from "./types";
|
|
35
93
|
|
|
36
94
|
// Stateful child component — receives props from App
|
|
@@ -88,7 +146,7 @@ export function Counter({ initialCount = 0, onCountChange }: CounterProps) {
|
|
|
88
146
|
);
|
|
89
147
|
}
|
|
90
148
|
`,
|
|
91
|
-
"types.ts": `// Type definitions — shared across components
|
|
149
|
+
"src/types.ts": `// Type definitions — shared across components
|
|
92
150
|
|
|
93
151
|
export interface User {
|
|
94
152
|
name: string;
|
|
@@ -1065,7 +1123,7 @@ export const DEFAULT_REACT_LAB: FrontendLabWorkspace = {
|
|
|
1065
1123
|
version: 1,
|
|
1066
1124
|
label: "React Lab",
|
|
1067
1125
|
type: "react",
|
|
1068
|
-
activeFile: "App.tsx",
|
|
1126
|
+
activeFile: "src/App.tsx",
|
|
1069
1127
|
files: REACT_DEFAULT_FILES,
|
|
1070
1128
|
};
|
|
1071
1129
|
|
|
@@ -1175,9 +1233,11 @@ export function getEntryFile(workspace: FrontendLabWorkspace): string {
|
|
|
1175
1233
|
? "apps/host/src/App.jsx"
|
|
1176
1234
|
: Object.keys(workspace.files)[0];
|
|
1177
1235
|
}
|
|
1178
|
-
return workspace.files["
|
|
1179
|
-
? "
|
|
1180
|
-
:
|
|
1236
|
+
return workspace.files["main.tsx"]
|
|
1237
|
+
? "main.tsx"
|
|
1238
|
+
: workspace.files["App.tsx"]
|
|
1239
|
+
? "App.tsx"
|
|
1240
|
+
: Object.keys(workspace.files)[0];
|
|
1181
1241
|
}
|
|
1182
1242
|
|
|
1183
1243
|
/** Preferred display order for the file tree. */
|
|
@@ -1228,7 +1288,8 @@ export function resolveNextjsEntry(
|
|
|
1228
1288
|
*
|
|
1229
1289
|
* Approach: loads React 18 UMD + Babel standalone from CDN, runs a
|
|
1230
1290
|
* custom module system built on top of Babel's CJS transform plugin,
|
|
1231
|
-
* then
|
|
1291
|
+
* then either runs a bootstrap entry such as main.tsx or renders the
|
|
1292
|
+
* default export from `entryFile`.
|
|
1232
1293
|
*
|
|
1233
1294
|
* CDN URLs are version-pinned so the preview is reproducible.
|
|
1234
1295
|
*/
|
|
@@ -1242,9 +1303,6 @@ export function generatePreviewHTML(
|
|
|
1242
1303
|
const entryJSON = JSON.stringify(entryFile);
|
|
1243
1304
|
const sandboxJSON = JSON.stringify(sandboxUrl ?? "");
|
|
1244
1305
|
const isNextjsJSON = isNextjs ? "true" : "false";
|
|
1245
|
-
// _i breaks up the 'import' keyword so Vite/Babel doesn't misparse
|
|
1246
|
-
// the template literal below as containing real module import declarations
|
|
1247
|
-
const _i = "import";
|
|
1248
1306
|
|
|
1249
1307
|
return `<!DOCTYPE html>
|
|
1250
1308
|
<html>
|
|
@@ -1252,6 +1310,8 @@ export function generatePreviewHTML(
|
|
|
1252
1310
|
<meta charset="utf-8">
|
|
1253
1311
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1254
1312
|
<script>window.__F__=${filesJSON};window.__E__=${entryJSON};window.SANDBOX_URL=${sandboxJSON};window.__NX__=${isNextjsJSON};</script>
|
|
1313
|
+
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
|
|
1314
|
+
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
|
|
1255
1315
|
<script src="https://unpkg.com/@babel/standalone@7.26.10/babel.min.js"></script>
|
|
1256
1316
|
<style>
|
|
1257
1317
|
*{box-sizing:border-box}
|
|
@@ -1262,11 +1322,7 @@ body{margin:0;background:#fff;font-family:system-ui,sans-serif}
|
|
|
1262
1322
|
<body>
|
|
1263
1323
|
<div id="root"></div>
|
|
1264
1324
|
<div id="__err"></div>
|
|
1265
|
-
<script
|
|
1266
|
-
${_i} React from 'https://esm.sh/react@19.1.0';
|
|
1267
|
-
${_i} * as ReactDOM from 'https://esm.sh/react-dom@19.1.0/client?deps=react@19.1.0';
|
|
1268
|
-
window.React = React;
|
|
1269
|
-
window.ReactDOM = ReactDOM;
|
|
1325
|
+
<script>
|
|
1270
1326
|
(function(){
|
|
1271
1327
|
var files=window.__F__,entry=window.__E__,reg={};
|
|
1272
1328
|
function norm(from,id){
|
|
@@ -1329,27 +1385,36 @@ window.ReactDOM = ReactDOM;
|
|
|
1329
1385
|
window.addEventListener('unhandledrejection',function(e){showErr(e.reason&&e.reason.message?e.reason.message:String(e.reason));});
|
|
1330
1386
|
try{
|
|
1331
1387
|
order.forEach(loadMod);
|
|
1332
|
-
var em=reg[entry];
|
|
1333
|
-
if(!em)throw new Error('Entry not found: '+entry);
|
|
1334
|
-
var Comp=em.exports.default;
|
|
1335
|
-
if(typeof Comp!=='function')throw new Error('No default export (function/component) in '+entry);
|
|
1336
1388
|
// Expose a navigate helper so in-preview code can trigger URL bar changes:
|
|
1337
1389
|
// window.__nxNavigate('/dashboard')
|
|
1338
1390
|
window.__nxNavigate=function(to){try{parent.postMessage({type:'rlab-nav',to:to},'*');}catch(e){}};
|
|
1339
|
-
var
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1391
|
+
var em=reg[entry];
|
|
1392
|
+
if(!em)throw new Error('Entry not found: '+entry);
|
|
1393
|
+
var isBootstrapEntry=/(^|\\/)(main|index)\\.(tsx|ts|jsx|js)$/.test(entry);
|
|
1394
|
+
if(isBootstrapEntry){
|
|
1395
|
+
if(typeof em.exports.mount==='function'){
|
|
1396
|
+
var mountRoot=document.getElementById('root');
|
|
1397
|
+
if(!mountRoot)throw new Error('Root element #root not found');
|
|
1398
|
+
em.exports.mount(mountRoot);
|
|
1345
1399
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1400
|
+
}else{
|
|
1401
|
+
var Comp=em.exports.default;
|
|
1402
|
+
if(typeof Comp!=='function')throw new Error('No default export (function/component) in '+entry);
|
|
1403
|
+
var pageEl=React.createElement(Comp,null);
|
|
1404
|
+
// In Next.js mode: wrap the page in app/layout.tsx if it exists
|
|
1405
|
+
if(window.__NX__){
|
|
1406
|
+
var lk=null;
|
|
1407
|
+
for(var _le of['app/layout.tsx','app/layout.ts','app/layout.jsx','app/layout.js']){
|
|
1408
|
+
if(reg[_le]){lk=_le;break;}
|
|
1409
|
+
}
|
|
1410
|
+
if(lk&&typeof reg[lk].exports.default==='function'){
|
|
1411
|
+
pageEl=React.createElement(reg[lk].exports.default,null,pageEl);
|
|
1412
|
+
}
|
|
1348
1413
|
}
|
|
1414
|
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
1415
|
+
React.createElement(React.StrictMode,null,pageEl)
|
|
1416
|
+
);
|
|
1349
1417
|
}
|
|
1350
|
-
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
1351
|
-
React.createElement(React.StrictMode,null,pageEl)
|
|
1352
|
-
);
|
|
1353
1418
|
try{parent.postMessage({type:'rlab-ready'},'*');}catch(e){}
|
|
1354
1419
|
}catch(err){showErr(err.message+(err.stack?'\\n\\n'+err.stack:''));}
|
|
1355
1420
|
})();
|
|
@@ -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