create-interview-cockpit 0.4.0 → 0.6.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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/template/client/package-lock.json +753 -1
  3. package/template/client/package.json +4 -0
  4. package/template/client/src/App.tsx +20 -0
  5. package/template/client/src/api.ts +455 -3
  6. package/template/client/src/components/AiSettingsModal.tsx +855 -248
  7. package/template/client/src/components/AnnotationDialog.tsx +3 -9
  8. package/template/client/src/components/ChatMessage.tsx +132 -27
  9. package/template/client/src/components/ChatView.tsx +365 -123
  10. package/template/client/src/components/CodeContextPanel.tsx +714 -0
  11. package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
  12. package/template/client/src/components/CodeRunnerModal.tsx +3030 -0
  13. package/template/client/src/components/DocRefModal.tsx +551 -0
  14. package/template/client/src/components/FileAttachments.tsx +128 -12
  15. package/template/client/src/components/FilePickerModal.tsx +181 -0
  16. package/template/client/src/components/FileViewerModal.tsx +406 -28
  17. package/template/client/src/components/InfraLabModal.tsx +1706 -0
  18. package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
  19. package/template/client/src/components/MarkdownRenderer.tsx +219 -2
  20. package/template/client/src/components/NotesModal.tsx +977 -0
  21. package/template/client/src/components/PlotEmbed.tsx +173 -0
  22. package/template/client/src/components/Sidebar.tsx +397 -127
  23. package/template/client/src/components/TextAnnotator.tsx +8 -15
  24. package/template/client/src/components/VizCraftEmbed.tsx +412 -25
  25. package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
  26. package/template/client/src/infraLab.ts +124 -0
  27. package/template/client/src/reactLab.ts +477 -0
  28. package/template/client/src/store.ts +416 -2
  29. package/template/client/src/types.ts +41 -1
  30. package/template/client/tsconfig.tsbuildinfo +1 -1
  31. package/template/cockpit.json +1 -1
  32. package/template/package.json +1 -1
  33. package/template/server/src/google-drive.ts +144 -2
  34. package/template/server/src/index.ts +1890 -188
  35. package/template/server/src/infra-runner.ts +1104 -0
  36. package/template/server/src/storage.ts +274 -3
@@ -0,0 +1,124 @@
1
+ import type { InfraLabWorkspace } from "./types";
2
+
3
+ const DEFAULT_INFRA_FILES: Record<string, string> = {
4
+ "provider.tf": `terraform {
5
+ required_providers {
6
+ aws = {
7
+ source = "hashicorp/aws"
8
+ version = "~> 5.0"
9
+ }
10
+ }
11
+ }
12
+
13
+ provider "aws" {
14
+ region = "us-east-1"
15
+ access_key = "test"
16
+ secret_key = "test"
17
+ skip_credentials_validation = true
18
+ skip_metadata_api_check = true
19
+ skip_requesting_account_id = true
20
+ s3_use_path_style = true
21
+
22
+ endpoints {
23
+ s3 = "http://localhost:4566"
24
+ }
25
+ }
26
+ `,
27
+ "main.tf": `resource "aws_s3_bucket" "example" {
28
+ bucket = "practice-bucket"
29
+ }
30
+ `,
31
+ };
32
+
33
+ export const DEFAULT_INFRA_LAB: InfraLabWorkspace = {
34
+ version: 1,
35
+ label: "AWS LocalStack Lab",
36
+ provider: "aws",
37
+ executionMode: "localstack",
38
+ activeFile: "main.tf",
39
+ files: DEFAULT_INFRA_FILES,
40
+ };
41
+
42
+ export function cloneInfraLabWorkspace(
43
+ workspace?: InfraLabWorkspace | null,
44
+ ): InfraLabWorkspace {
45
+ const source = workspace ?? DEFAULT_INFRA_LAB;
46
+ // Only seed defaults when the source has no files of its own
47
+ const files =
48
+ source.files && Object.keys(source.files).length > 0
49
+ ? { ...source.files }
50
+ : { ...DEFAULT_INFRA_FILES };
51
+ const activeFile = files[source.activeFile]
52
+ ? source.activeFile
53
+ : (Object.keys(files)[0] ?? "main.tf");
54
+
55
+ return {
56
+ version: 1,
57
+ label: source.label || DEFAULT_INFRA_LAB.label,
58
+ provider: "aws",
59
+ executionMode:
60
+ source.executionMode === "plan-only" ? "plan-only" : "localstack",
61
+ activeFile,
62
+ files,
63
+ };
64
+ }
65
+
66
+ export function getInfraLabFileOrder(workspace: InfraLabWorkspace): string[] {
67
+ const preferred = [
68
+ "main.tf",
69
+ "provider.tf",
70
+ "variables.tf",
71
+ "terraform.tfvars",
72
+ "outputs.tf",
73
+ "locals.tf",
74
+ "README.md",
75
+ ];
76
+ const extras = Object.keys(workspace.files)
77
+ .filter((name) => !preferred.includes(name))
78
+ .sort();
79
+
80
+ return preferred.filter((name) => workspace.files[name]).concat(extras);
81
+ }
82
+
83
+ export function serializeInfraLabWorkspace(
84
+ workspace: InfraLabWorkspace,
85
+ ): string {
86
+ return JSON.stringify(cloneInfraLabWorkspace(workspace), null, 2);
87
+ }
88
+
89
+ export function parseInfraLabWorkspace(raw: string): InfraLabWorkspace | null {
90
+ try {
91
+ const parsed = JSON.parse(raw) as Partial<InfraLabWorkspace> & {
92
+ files?: Record<string, unknown>;
93
+ };
94
+
95
+ if (!parsed || typeof parsed !== "object") return null;
96
+ if (!parsed.files || typeof parsed.files !== "object") return null;
97
+
98
+ const files = Object.fromEntries(
99
+ Object.entries(parsed.files).filter(
100
+ (entry): entry is [string, string] => typeof entry[1] === "string",
101
+ ),
102
+ );
103
+
104
+ if (Object.keys(files).length === 0) return null;
105
+
106
+ return cloneInfraLabWorkspace({
107
+ version: 1,
108
+ label:
109
+ typeof parsed.label === "string" && parsed.label.trim()
110
+ ? parsed.label.trim()
111
+ : DEFAULT_INFRA_LAB.label,
112
+ provider: "aws",
113
+ executionMode:
114
+ parsed.executionMode === "plan-only" ? "plan-only" : "localstack",
115
+ activeFile:
116
+ typeof parsed.activeFile === "string"
117
+ ? parsed.activeFile
118
+ : DEFAULT_INFRA_LAB.activeFile,
119
+ files,
120
+ });
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
@@ -0,0 +1,477 @@
1
+ import type { FrontendLabWorkspace } from "./types";
2
+
3
+ // ── Default file contents ────────────────────────────────────────────────────
4
+
5
+ const REACT_DEFAULT_FILES: Record<string, string> = {
6
+ "App.tsx": `import { useState } from "react";
7
+ import { Counter } from "./Counter";
8
+ import type { User } from "./types";
9
+
10
+ const user: User = { name: "Alice", age: 28 };
11
+
12
+ export default function App() {
13
+ const [count, setCount] = useState(0);
14
+
15
+ return (
16
+ <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
17
+ <h1 style={{ fontSize: "1.5rem", fontWeight: "bold", marginBottom: "0.5rem" }}>
18
+ React + TypeScript Lab
19
+ </h1>
20
+ <p style={{ color: "#64748b", marginBottom: "1.5rem" }}>
21
+ Welcome, {user.name}! Practice React fundamentals here.
22
+ </p>
23
+ <Counter initialCount={count} onCountChange={setCount} />
24
+ <p style={{ marginTop: "1rem", color: "#94a3b8", fontSize: "0.875rem" }}>
25
+ Parent count: {count}
26
+ </p>
27
+ </div>
28
+ );
29
+ }
30
+ `,
31
+ "Counter.tsx": `import { useState, useCallback } from "react";
32
+ import type { CounterProps } from "./types";
33
+
34
+ // Stateful child component — receives props from App
35
+ export function Counter({ initialCount = 0, onCountChange }: CounterProps) {
36
+ const [count, setCount] = useState(initialCount);
37
+
38
+ const increment = useCallback(() => {
39
+ setCount((c) => {
40
+ const next = c + 1;
41
+ onCountChange?.(next);
42
+ return next;
43
+ });
44
+ }, [onCountChange]);
45
+
46
+ const decrement = useCallback(() => {
47
+ setCount((c) => {
48
+ const next = c - 1;
49
+ onCountChange?.(next);
50
+ return next;
51
+ });
52
+ }, [onCountChange]);
53
+
54
+ return (
55
+ <div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
56
+ <button
57
+ onClick={decrement}
58
+ style={{
59
+ padding: "0.5rem 1.25rem",
60
+ fontSize: "1.25rem",
61
+ cursor: "pointer",
62
+ borderRadius: "0.375rem",
63
+ border: "1px solid #cbd5e1",
64
+ background: "#f8fafc",
65
+ }}
66
+ >
67
+
68
+ </button>
69
+ <span style={{ fontSize: "2rem", fontWeight: "bold", minWidth: "3rem", textAlign: "center" }}>
70
+ {count}
71
+ </span>
72
+ <button
73
+ onClick={increment}
74
+ style={{
75
+ padding: "0.5rem 1.25rem",
76
+ fontSize: "1.25rem",
77
+ cursor: "pointer",
78
+ borderRadius: "0.375rem",
79
+ border: "1px solid #cbd5e1",
80
+ background: "#f8fafc",
81
+ }}
82
+ >
83
+ +
84
+ </button>
85
+ </div>
86
+ );
87
+ }
88
+ `,
89
+ "types.ts": `// Type definitions — shared across components
90
+
91
+ export interface User {
92
+ name: string;
93
+ age: number;
94
+ }
95
+
96
+ export interface CounterProps {
97
+ initialCount?: number;
98
+ /** Callback that fires whenever the count changes */
99
+ onCountChange?: (count: number) => void;
100
+ }
101
+ `,
102
+ };
103
+
104
+ const NEXTJS_DEFAULT_FILES: Record<string, string> = {
105
+ "app/page.tsx": `// Server Component (default in App Router — no "use client" needed)
106
+ // In real Next.js this could be async and fetch data directly
107
+ import { Counter } from "../components/Counter";
108
+
109
+ export default function HomePage() {
110
+ // In real Next.js: const data = await fetch('/api/...').then(r => r.json())
111
+ const message = "Server Components render on the server — no useState here!";
112
+
113
+ return (
114
+ <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
115
+ <h1 style={{ fontSize: "1.5rem", fontWeight: "bold", marginBottom: "0.5rem" }}>
116
+ Next.js App Router Lab
117
+ </h1>
118
+ <p style={{ color: "#64748b", marginBottom: "1.5rem" }}>{message}</p>
119
+ {/* Counter is a Client Component — it can use useState */}
120
+ <Counter />
121
+ </div>
122
+ );
123
+ }
124
+ `,
125
+ "app/layout.tsx": `// Root Layout — always a Server Component
126
+ // Wraps ALL pages; persists across navigations without re-mounting
127
+
128
+ export default function RootLayout({
129
+ children,
130
+ }: {
131
+ children: React.ReactNode;
132
+ }) {
133
+ return (
134
+ <html lang="en">
135
+ <body style={{ margin: 0, background: "#f8fafc", fontFamily: "system-ui, sans-serif" }}>
136
+ <nav
137
+ style={{
138
+ padding: "0.75rem 2rem",
139
+ background: "#0070f3",
140
+ color: "#fff",
141
+ marginBottom: "0",
142
+ }}
143
+ >
144
+ <strong>My Next.js App</strong>
145
+ </nav>
146
+ <main>{children}</main>
147
+ </body>
148
+ </html>
149
+ );
150
+ }
151
+ `,
152
+ "app/loading.tsx": `// loading.tsx — shown while the page is fetching data (Suspense boundary)
153
+ // Next.js displays this automatically while the page component awaits
154
+
155
+ export default function Loading() {
156
+ return (
157
+ <div style={{ padding: "2rem", color: "#64748b" }}>
158
+ Loading…
159
+ </div>
160
+ );
161
+ }
162
+ `,
163
+ "components/Counter.tsx": `"use client";
164
+ // "use client" — marks this as a Client Component
165
+ // Only Client Components can use useState, useEffect, and browser APIs
166
+
167
+ import { useState } from "react";
168
+
169
+ export function Counter() {
170
+ const [count, setCount] = useState(0);
171
+
172
+ return (
173
+ <div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
174
+ <button
175
+ onClick={() => setCount((c) => c - 1)}
176
+ style={{ padding: "0.5rem 1.25rem", fontSize: "1.25rem", cursor: "pointer",
177
+ borderRadius: "0.375rem", border: "1px solid #cbd5e1", background: "#f8fafc" }}
178
+ >
179
+
180
+ </button>
181
+ <span style={{ fontSize: "2rem", fontWeight: "bold", minWidth: "3rem", textAlign: "center" }}>
182
+ {count}
183
+ </span>
184
+ <button
185
+ onClick={() => setCount((c) => c + 1)}
186
+ style={{ padding: "0.5rem 1.25rem", fontSize: "1.25rem", cursor: "pointer",
187
+ borderRadius: "0.375rem", border: "1px solid #cbd5e1", background: "#f8fafc" }}
188
+ >
189
+ +
190
+ </button>
191
+ </div>
192
+ );
193
+ }
194
+ `,
195
+ "types.ts": `// Shared TypeScript types
196
+
197
+ export interface PageProps {
198
+ params: { slug: string };
199
+ searchParams: Record<string, string | string[] | undefined>;
200
+ }
201
+
202
+ export interface User {
203
+ id: string;
204
+ name: string;
205
+ email: string;
206
+ }
207
+ `,
208
+ };
209
+
210
+ // ── Lab workspace constructors ────────────────────────────────────────────────
211
+
212
+ export const DEFAULT_REACT_LAB: FrontendLabWorkspace = {
213
+ version: 1,
214
+ label: "React Lab",
215
+ type: "react",
216
+ activeFile: "App.tsx",
217
+ files: REACT_DEFAULT_FILES,
218
+ };
219
+
220
+ export const DEFAULT_NEXTJS_LAB: FrontendLabWorkspace = {
221
+ version: 1,
222
+ label: "Next.js Lab",
223
+ type: "nextjs",
224
+ activeFile: "app/page.tsx",
225
+ files: NEXTJS_DEFAULT_FILES,
226
+ };
227
+
228
+ export function defaultForType(type: "react" | "nextjs"): FrontendLabWorkspace {
229
+ return type === "nextjs" ? DEFAULT_NEXTJS_LAB : DEFAULT_REACT_LAB;
230
+ }
231
+
232
+ export function cloneFrontendLabWorkspace(
233
+ workspace?: FrontendLabWorkspace | null,
234
+ type?: "react" | "nextjs",
235
+ ): FrontendLabWorkspace {
236
+ const resolvedType = workspace?.type ?? type ?? "react";
237
+ const defaults = defaultForType(resolvedType);
238
+ const source = workspace ?? defaults;
239
+ const files =
240
+ source.files && Object.keys(source.files).length > 0
241
+ ? { ...source.files }
242
+ : { ...defaults.files };
243
+ const activeFile = files[source.activeFile]
244
+ ? source.activeFile
245
+ : (Object.keys(files)[0] ?? defaults.activeFile);
246
+
247
+ return {
248
+ version: 1,
249
+ label: source.label?.trim() || defaults.label,
250
+ type: resolvedType,
251
+ activeFile,
252
+ files,
253
+ };
254
+ }
255
+
256
+ export function serializeFrontendLabWorkspace(
257
+ workspace: FrontendLabWorkspace,
258
+ ): string {
259
+ return JSON.stringify(cloneFrontendLabWorkspace(workspace), null, 2);
260
+ }
261
+
262
+ export function parseFrontendLabWorkspace(
263
+ raw: string,
264
+ ): FrontendLabWorkspace | null {
265
+ try {
266
+ const parsed = JSON.parse(raw) as Partial<FrontendLabWorkspace> & {
267
+ files?: Record<string, unknown>;
268
+ };
269
+ if (!parsed || typeof parsed !== "object") return null;
270
+ if (!parsed.files || typeof parsed.files !== "object") return null;
271
+
272
+ const files = Object.fromEntries(
273
+ Object.entries(parsed.files).filter(
274
+ (e): e is [string, string] => typeof e[1] === "string",
275
+ ),
276
+ );
277
+ if (Object.keys(files).length === 0) return null;
278
+
279
+ const type: "react" | "nextjs" =
280
+ parsed.type === "nextjs" ? "nextjs" : "react";
281
+
282
+ return cloneFrontendLabWorkspace({
283
+ version: 1,
284
+ type,
285
+ label:
286
+ typeof parsed.label === "string" && parsed.label.trim()
287
+ ? parsed.label.trim()
288
+ : defaultForType(type).label,
289
+ activeFile:
290
+ typeof parsed.activeFile === "string"
291
+ ? parsed.activeFile
292
+ : defaultForType(type).activeFile,
293
+ files,
294
+ });
295
+ } catch {
296
+ return null;
297
+ }
298
+ }
299
+
300
+ /** Returns the canonical entry file for "Run" → preview. */
301
+ export function getEntryFile(workspace: FrontendLabWorkspace): string {
302
+ if (workspace.type === "nextjs") {
303
+ return workspace.files["app/page.tsx"]
304
+ ? "app/page.tsx"
305
+ : Object.keys(workspace.files)[0];
306
+ }
307
+ return workspace.files["App.tsx"]
308
+ ? "App.tsx"
309
+ : Object.keys(workspace.files)[0];
310
+ }
311
+
312
+ /** Preferred display order for the file tree. */
313
+ export function getFrontendLabFileOrder(
314
+ workspace: FrontendLabWorkspace,
315
+ ): string[] {
316
+ const allFiles = Object.keys(workspace.files).sort((a, b) => {
317
+ // Sort by folder depth first, then alphabetically
318
+ const ad = a.split("/").length;
319
+ const bd = b.split("/").length;
320
+ return ad !== bd ? ad - bd : a.localeCompare(b);
321
+ });
322
+ return allFiles;
323
+ }
324
+
325
+ // ── Preview HTML generator ────────────────────────────────────────────────────
326
+
327
+ /**
328
+ * Resolves which page.tsx file corresponds to a Next.js route path.
329
+ * Returns null if no matching file exists in `files`.
330
+ */
331
+ export function resolveNextjsEntry(
332
+ files: Record<string, string>,
333
+ routePath: string,
334
+ ): string | null {
335
+ const segments = routePath.replace(/^\//, "").split("/").filter(Boolean);
336
+ const base =
337
+ segments.length === 0 ? "app/page" : `app/${segments.join("/")}/page`;
338
+ for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
339
+ if (files[base + ext] !== undefined) return base + ext;
340
+ }
341
+ return null;
342
+ }
343
+
344
+ /**
345
+ * Generates a self-contained HTML page for the preview iframe.
346
+ *
347
+ * Approach: loads React 18 UMD + Babel standalone from CDN, runs a
348
+ * custom module system built on top of Babel's CJS transform plugin,
349
+ * then renders the default export from `entryFile`.
350
+ *
351
+ * CDN URLs are version-pinned so the preview is reproducible.
352
+ */
353
+ export function generatePreviewHTML(
354
+ files: Record<string, string>,
355
+ entryFile: string,
356
+ sandboxUrl?: string,
357
+ isNextjs?: boolean,
358
+ ): string {
359
+ const filesJSON = JSON.stringify(files);
360
+ const entryJSON = JSON.stringify(entryFile);
361
+ const sandboxJSON = JSON.stringify(sandboxUrl ?? "");
362
+ const isNextjsJSON = isNextjs ? "true" : "false";
363
+ // _i breaks up the 'import' keyword so Vite/Babel doesn't misparse
364
+ // the template literal below as containing real module import declarations
365
+ const _i = "import";
366
+
367
+ return `<!DOCTYPE html>
368
+ <html>
369
+ <head>
370
+ <meta charset="utf-8">
371
+ <meta name="viewport" content="width=device-width, initial-scale=1">
372
+ <script>window.__F__=${filesJSON};window.__E__=${entryJSON};window.SANDBOX_URL=${sandboxJSON};window.__NX__=${isNextjsJSON};</script>
373
+ <script src="https://unpkg.com/@babel/standalone@7.26.10/babel.min.js"></script>
374
+ <style>
375
+ *{box-sizing:border-box}
376
+ body{margin:0;background:#fff;font-family:system-ui,sans-serif}
377
+ #__err{display:none;position:fixed;bottom:0;left:0;right:0;padding:0.75rem 1rem;background:#fef2f2;color:#991b1b;font:12px/1.5 monospace;white-space:pre-wrap;border-top:2px solid #fca5a5;max-height:50%;overflow:auto;z-index:9999}
378
+ </style>
379
+ </head>
380
+ <body>
381
+ <div id="root"></div>
382
+ <div id="__err"></div>
383
+ <script type="module">
384
+ ${_i} React from 'https://esm.sh/react@19.1.0';
385
+ ${_i} * as ReactDOM from 'https://esm.sh/react-dom@19.1.0/client?deps=react@19.1.0';
386
+ window.React = React;
387
+ window.ReactDOM = ReactDOM;
388
+ (function(){
389
+ var files=window.__F__,entry=window.__E__,reg={};
390
+ function norm(from,id){
391
+ if(id==='react'||id==='react/jsx-runtime'||id==='react/jsx-dev-runtime')return'__react__';
392
+ if(id==='react-dom'||id==='react-dom/server')return'__reactdom__';
393
+ if(id==='react-dom/client')return'__reactdomclient__';
394
+ if(!id.startsWith('.')){return'__ext__:'+id;}
395
+ var dir=from.includes('/')?from.slice(0,from.lastIndexOf('/')+1):'';
396
+ var parts=(dir+id).split('/').reduce(function(a,p){
397
+ if(p==='..')a.pop();else if(p&&p!=='.')a.push(p);return a;
398
+ },[]);
399
+ var base=parts.join('/');
400
+ var exts=['','.tsx','.ts','.jsx','.js'];
401
+ for(var i=0;i<exts.length;i++){if(files[base+exts[i]]!=null)return base+exts[i];}
402
+ return base;
403
+ }
404
+ function makeReq(from){
405
+ return function(id){
406
+ if(id==='react'||id==='react/jsx-runtime'||id==='react/jsx-dev-runtime')return window.React;
407
+ if(id==='react-dom/client')return{createRoot:window.ReactDOM.createRoot.bind(window.ReactDOM)};
408
+ if(id==='react-dom')return window.ReactDOM;
409
+ var key=norm(from,id);
410
+ if(key.startsWith('__ext__:'))return{};
411
+ if(reg[key])return reg[key].exports;
412
+ for(var e of['.tsx','.ts','.jsx','.js']){if(reg[key+e])return reg[key+e].exports;}
413
+ console.warn('Module not found:',id,'from',from);return{};
414
+ };
415
+ }
416
+ function loadMod(name){
417
+ if(reg[name])return;
418
+ var src=files[name];if(src==null)return;
419
+ var m={exports:{}};
420
+ reg[name]=m;
421
+ try{
422
+ var out=Babel.transform(src,{
423
+ presets:[['react',{runtime:'classic'}],['typescript',{allExtensions:true,isTSX:true}]],
424
+ plugins:['transform-modules-commonjs','transform-dynamic-import'],
425
+ filename:name,sourceType:'module'
426
+ }).code;
427
+ (new Function('require','module','exports',out))(makeReq(name),m,m.exports);
428
+ }catch(e){throw new Error(name+': '+e.message);}
429
+ }
430
+ function deps(name){
431
+ var src=files[name]||'',re=/from\\s+['""]([^'"]+)['"]/g,d=[],m;
432
+ while((m=re.exec(src))!==null){
433
+ var k=norm(name,m[1]);
434
+ if(k&&!k.startsWith('__')&&files[k])d.push(k);
435
+ }
436
+ return d;
437
+ }
438
+ var vis=new Set(),order=[];
439
+ function visit(n){if(vis.has(n))return;vis.add(n);deps(n).forEach(visit);order.push(n);}
440
+ Object.keys(files).forEach(visit);
441
+ function showErr(msg){
442
+ var el=document.getElementById('__err');
443
+ el.style.display='block';el.innerText=msg;
444
+ try{parent.postMessage({type:'rlab-err',error:msg},'*');}catch(e){}
445
+ }
446
+ window.onerror=function(msg,s,l,c,err){showErr(err?err.message+'\\n'+(err.stack||''):String(msg));return true;};
447
+ window.addEventListener('unhandledrejection',function(e){showErr(e.reason&&e.reason.message?e.reason.message:String(e.reason));});
448
+ try{
449
+ order.forEach(loadMod);
450
+ var em=reg[entry];
451
+ if(!em)throw new Error('Entry not found: '+entry);
452
+ var Comp=em.exports.default;
453
+ if(typeof Comp!=='function')throw new Error('No default export (function/component) in '+entry);
454
+ // Expose a navigate helper so in-preview code can trigger URL bar changes:
455
+ // window.__nxNavigate('/dashboard')
456
+ window.__nxNavigate=function(to){try{parent.postMessage({type:'rlab-nav',to:to},'*');}catch(e){}};
457
+ var pageEl=React.createElement(Comp,null);
458
+ // In Next.js mode: wrap the page in app/layout.tsx if it exists
459
+ if(window.__NX__){
460
+ var lk=null;
461
+ for(var _le of['app/layout.tsx','app/layout.ts','app/layout.jsx','app/layout.js']){
462
+ if(reg[_le]){lk=_le;break;}
463
+ }
464
+ if(lk&&typeof reg[lk].exports.default==='function'){
465
+ pageEl=React.createElement(reg[lk].exports.default,null,pageEl);
466
+ }
467
+ }
468
+ ReactDOM.createRoot(document.getElementById('root')).render(
469
+ React.createElement(React.StrictMode,null,pageEl)
470
+ );
471
+ try{parent.postMessage({type:'rlab-ready'},'*');}catch(e){}
472
+ }catch(err){showErr(err.message+(err.stack?'\\n\\n'+err.stack:''));}
473
+ })();
474
+ </script>
475
+ </body>
476
+ </html>`;
477
+ }