create-interview-cockpit 0.9.0 → 0.11.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 +1 -0
- package/template/client/src/components/CodeContextPanel.tsx +853 -850
- package/template/client/src/components/CodeRunnerModal.tsx +2273 -7
- package/template/client/src/reactLab.ts +418 -49
- package/template/client/tsconfig.tsbuildinfo +1 -30
- package/template/cockpit.json +1 -1
- package/template/server/src/index.ts +4 -0
|
@@ -320,7 +320,7 @@ export default function CodeContextPanel() {
|
|
|
320
320
|
const filter = search.toLowerCase();
|
|
321
321
|
|
|
322
322
|
return (
|
|
323
|
-
<div className="w-72 border-l border-slate-800 flex flex-col bg-slate-900/30 shrink-0">
|
|
323
|
+
<div className="w-72 h-full min-h-0 border-l border-slate-800 flex flex-col bg-slate-900/30 shrink-0 overflow-hidden">
|
|
324
324
|
{/* Header */}
|
|
325
325
|
<div className="border-b border-slate-800 px-3 py-2">
|
|
326
326
|
<div className="flex items-center justify-between mb-2">
|
|
@@ -340,350 +340,368 @@ export default function CodeContextPanel() {
|
|
|
340
340
|
</div>
|
|
341
341
|
</div>
|
|
342
342
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<div className="
|
|
347
|
-
<div className="flex items-center
|
|
348
|
-
<
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
<button
|
|
354
|
-
onClick={clearSnippets}
|
|
355
|
-
className="text-[10px] text-red-400/60 hover:text-red-400"
|
|
356
|
-
>
|
|
357
|
-
Clear all
|
|
358
|
-
</button>
|
|
359
|
-
</div>
|
|
360
|
-
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
361
|
-
{codeSnippets.map((s) => (
|
|
362
|
-
<div
|
|
363
|
-
key={s.id}
|
|
364
|
-
className="flex items-start gap-1 text-xs bg-amber-500/10 border border-amber-500/20 rounded px-1.5 py-1 group"
|
|
365
|
-
>
|
|
366
|
-
<div className="flex-1 min-w-0">
|
|
367
|
-
<span className="text-amber-400 font-medium">
|
|
368
|
-
{s.fileName}
|
|
369
|
-
</span>
|
|
370
|
-
<span className="text-slate-500 mx-1">›</span>
|
|
371
|
-
<span className="text-slate-500">
|
|
372
|
-
line{s.startLine !== s.endLine ? "s" : ""}{" "}
|
|
373
|
-
{s.startLine === s.endLine
|
|
374
|
-
? s.startLine
|
|
375
|
-
: `${s.startLine}–${s.endLine}`}
|
|
376
|
-
</span>
|
|
377
|
-
<p className="text-[10px] font-mono text-slate-600 mt-0.5 truncate">
|
|
378
|
-
{s.code.split("\n")[0]}
|
|
379
|
-
</p>
|
|
380
|
-
</div>
|
|
381
|
-
<button
|
|
382
|
-
onClick={() => removeSnippet(s.id)}
|
|
383
|
-
className="shrink-0 mt-0.5 text-slate-600 hover:text-red-400 transition-colors"
|
|
384
|
-
>
|
|
385
|
-
<X className="w-2.5 h-2.5" />
|
|
386
|
-
</button>
|
|
343
|
+
<div className="flex-1 min-h-0 overflow-y-auto">
|
|
344
|
+
{/* Snippets section */}
|
|
345
|
+
{codeSnippets.length > 0 && (
|
|
346
|
+
<div className="border-b border-slate-800 px-3 py-2">
|
|
347
|
+
<div className="flex items-center justify-between mb-1">
|
|
348
|
+
<div className="flex items-center gap-1">
|
|
349
|
+
<Scissors className="w-3 h-3 text-amber-400/70" />
|
|
350
|
+
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
351
|
+
Snippets ({codeSnippets.length})
|
|
352
|
+
</span>
|
|
387
353
|
</div>
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
)}
|
|
392
|
-
|
|
393
|
-
{/* Selected summary */}
|
|
394
|
-
{selectedFiles.length > 0 && (
|
|
395
|
-
<div className="border-b border-slate-800 px-3 py-2">
|
|
396
|
-
<div className="flex items-center justify-between">
|
|
397
|
-
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
398
|
-
Selected ({selectedFiles.length})
|
|
399
|
-
</span>
|
|
400
|
-
<button
|
|
401
|
-
onClick={() => {
|
|
402
|
-
setSelectedFiles([]);
|
|
403
|
-
if (currentQuestion) updateCodeContext(currentQuestion.id, []);
|
|
404
|
-
}}
|
|
405
|
-
className="text-[10px] text-red-400/60 hover:text-red-400"
|
|
406
|
-
>
|
|
407
|
-
Clear all
|
|
408
|
-
</button>
|
|
409
|
-
</div>
|
|
410
|
-
<div className="mt-1 space-y-0.5 max-h-24 overflow-y-auto">
|
|
411
|
-
{selectedFiles.map((f) => (
|
|
412
|
-
<div
|
|
413
|
-
key={f}
|
|
414
|
-
className="flex items-center gap-1 text-xs text-cyan-400 bg-cyan-500/10 rounded px-1.5 py-0.5 group"
|
|
354
|
+
<button
|
|
355
|
+
onClick={clearSnippets}
|
|
356
|
+
className="text-[10px] text-red-400/60 hover:text-red-400"
|
|
415
357
|
>
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
{
|
|
423
|
-
|
|
424
|
-
<button
|
|
425
|
-
onClick={() => toggleFile(f)}
|
|
426
|
-
className="shrink-0 hover:text-red-400"
|
|
358
|
+
Clear all
|
|
359
|
+
</button>
|
|
360
|
+
</div>
|
|
361
|
+
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
362
|
+
{codeSnippets.map((s) => (
|
|
363
|
+
<div
|
|
364
|
+
key={s.id}
|
|
365
|
+
className="flex items-start gap-1 text-xs bg-amber-500/10 border border-amber-500/20 rounded px-1.5 py-1 group"
|
|
427
366
|
>
|
|
428
|
-
<
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
367
|
+
<div className="flex-1 min-w-0">
|
|
368
|
+
<span className="text-amber-400 font-medium">
|
|
369
|
+
{s.fileName}
|
|
370
|
+
</span>
|
|
371
|
+
<span className="text-slate-500 mx-1">›</span>
|
|
372
|
+
<span className="text-slate-500">
|
|
373
|
+
line{s.startLine !== s.endLine ? "s" : ""}{" "}
|
|
374
|
+
{s.startLine === s.endLine
|
|
375
|
+
? s.startLine
|
|
376
|
+
: `${s.startLine}–${s.endLine}`}
|
|
377
|
+
</span>
|
|
378
|
+
<p className="text-[10px] font-mono text-slate-600 mt-0.5 truncate">
|
|
379
|
+
{s.code.split("\n")[0]}
|
|
380
|
+
</p>
|
|
381
|
+
</div>
|
|
382
|
+
<button
|
|
383
|
+
onClick={() => removeSnippet(s.id)}
|
|
384
|
+
className="shrink-0 mt-0.5 text-slate-600 hover:text-red-400 transition-colors"
|
|
385
|
+
>
|
|
386
|
+
<X className="w-2.5 h-2.5" />
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
))}
|
|
390
|
+
</div>
|
|
450
391
|
</div>
|
|
451
|
-
) : (
|
|
452
|
-
tree.children
|
|
453
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
454
|
-
.map((child) => (
|
|
455
|
-
<FolderNode
|
|
456
|
-
key={child.path}
|
|
457
|
-
node={child}
|
|
458
|
-
selectedFiles={selectedFiles}
|
|
459
|
-
onToggleFile={toggleFile}
|
|
460
|
-
onToggleFolder={toggleFolder}
|
|
461
|
-
expandedFolders={expandedFolders}
|
|
462
|
-
onToggleExpand={toggleExpand}
|
|
463
|
-
depth={0}
|
|
464
|
-
filter={filter}
|
|
465
|
-
onOpenFile={setViewingFile}
|
|
466
|
-
/>
|
|
467
|
-
))
|
|
468
392
|
)}
|
|
469
|
-
{/* Root-level files (if any) */}
|
|
470
|
-
{tree.files
|
|
471
|
-
.filter((f) => !filter || f.toLowerCase().includes(filter))
|
|
472
|
-
.map((filePath) => {
|
|
473
|
-
const isSelected = selectedFiles.includes(filePath);
|
|
474
|
-
return (
|
|
475
|
-
<div
|
|
476
|
-
key={filePath}
|
|
477
|
-
className={`flex items-center px-4 py-0.5 group ${
|
|
478
|
-
isSelected ? "text-cyan-400" : "text-slate-500"
|
|
479
|
-
}`}
|
|
480
|
-
>
|
|
481
|
-
<button
|
|
482
|
-
onClick={() => toggleFile(filePath)}
|
|
483
|
-
className="flex items-center gap-1.5 flex-1 min-w-0 hover:text-slate-300 transition-colors text-xs text-left"
|
|
484
|
-
>
|
|
485
|
-
<File className="w-3 h-3 shrink-0" />
|
|
486
|
-
<span className="truncate">{filePath}</span>
|
|
487
|
-
</button>
|
|
488
|
-
<button
|
|
489
|
-
onClick={() => setViewingFile(filePath)}
|
|
490
|
-
className="shrink-0 mr-1 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
|
|
491
|
-
title="View file"
|
|
492
|
-
>
|
|
493
|
-
<Eye className="w-3 h-3" />
|
|
494
|
-
</button>
|
|
495
|
-
</div>
|
|
496
|
-
);
|
|
497
|
-
})}
|
|
498
|
-
</div>
|
|
499
393
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
<div className="flex items-center gap-1">
|
|
505
|
-
<Terminal className="w-3 h-3 text-emerald-400/70" />
|
|
394
|
+
{/* Selected summary */}
|
|
395
|
+
{selectedFiles.length > 0 && (
|
|
396
|
+
<div className="border-b border-slate-800 px-3 py-2">
|
|
397
|
+
<div className="flex items-center justify-between">
|
|
506
398
|
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
507
|
-
|
|
508
|
-
{
|
|
509
|
-
(currentQuestion.contextFiles || []).filter(
|
|
510
|
-
(f) => f.origin === "user",
|
|
511
|
-
).length
|
|
512
|
-
}
|
|
513
|
-
)
|
|
399
|
+
Selected ({selectedFiles.length})
|
|
514
400
|
</span>
|
|
401
|
+
<button
|
|
402
|
+
onClick={() => {
|
|
403
|
+
setSelectedFiles([]);
|
|
404
|
+
if (currentQuestion)
|
|
405
|
+
updateCodeContext(currentQuestion.id, []);
|
|
406
|
+
}}
|
|
407
|
+
className="text-[10px] text-red-400/60 hover:text-red-400"
|
|
408
|
+
>
|
|
409
|
+
Clear all
|
|
410
|
+
</button>
|
|
515
411
|
</div>
|
|
516
|
-
<
|
|
517
|
-
|
|
518
|
-
className="p-0.5 rounded text-slate-600 hover:text-emerald-400 hover:bg-slate-700 transition-colors"
|
|
519
|
-
title="Open Code Runner"
|
|
520
|
-
>
|
|
521
|
-
<Plus className="w-3.5 h-3.5" />
|
|
522
|
-
</button>
|
|
523
|
-
</div>
|
|
524
|
-
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
525
|
-
{(currentQuestion.contextFiles || [])
|
|
526
|
-
.filter((f) => f.origin === "user")
|
|
527
|
-
.map((cf) => (
|
|
412
|
+
<div className="mt-1 space-y-0.5 max-h-24 overflow-y-auto">
|
|
413
|
+
{selectedFiles.map((f) => (
|
|
528
414
|
<div
|
|
529
|
-
key={
|
|
530
|
-
className="flex items-center gap-1 text-xs
|
|
415
|
+
key={f}
|
|
416
|
+
className="flex items-center gap-1 text-xs text-cyan-400 bg-cyan-500/10 rounded px-1.5 py-0.5 group"
|
|
531
417
|
>
|
|
532
|
-
<
|
|
533
|
-
className="text-emerald-400 font-medium truncate flex-1"
|
|
534
|
-
title={cf.label || cf.originalName}
|
|
535
|
-
>
|
|
536
|
-
{cf.label || cf.originalName}
|
|
537
|
-
</span>
|
|
418
|
+
<Check className="w-2.5 h-2.5 shrink-0" />
|
|
538
419
|
<button
|
|
539
|
-
onClick={
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
)
|
|
543
|
-
.then((r) => r.json())
|
|
544
|
-
.then((d) => d.content);
|
|
545
|
-
openCodeRunner(
|
|
546
|
-
content,
|
|
547
|
-
cf.language ?? "typescript",
|
|
548
|
-
cf.id,
|
|
549
|
-
);
|
|
550
|
-
}}
|
|
551
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-emerald-400 transition-all"
|
|
552
|
-
title="Open in Code Runner"
|
|
420
|
+
onClick={() => setViewingFile(f)}
|
|
421
|
+
className="truncate flex-1 text-left hover:underline"
|
|
422
|
+
title="View file"
|
|
553
423
|
>
|
|
554
|
-
|
|
424
|
+
{f.split("/").pop()}
|
|
555
425
|
</button>
|
|
556
426
|
<button
|
|
557
|
-
onClick={() =>
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
561
|
-
title="Remove"
|
|
427
|
+
onClick={() => toggleFile(f)}
|
|
428
|
+
className="shrink-0 hover:text-red-400"
|
|
562
429
|
>
|
|
563
|
-
<
|
|
430
|
+
<X className="w-2.5 h-2.5" />
|
|
564
431
|
</button>
|
|
565
432
|
</div>
|
|
566
433
|
))}
|
|
567
|
-
|
|
568
|
-
(f) => f.origin === "user",
|
|
569
|
-
).length === 0 && (
|
|
570
|
-
<p className="text-[10px] text-slate-700 italic">
|
|
571
|
-
Save code from the runner to see it here
|
|
572
|
-
</p>
|
|
573
|
-
)}
|
|
434
|
+
</div>
|
|
574
435
|
</div>
|
|
575
|
-
|
|
576
|
-
)}
|
|
436
|
+
)}
|
|
577
437
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
.
|
|
596
|
-
.map((
|
|
438
|
+
{/* Tree browser */}
|
|
439
|
+
<div className="py-1">
|
|
440
|
+
{!currentQuestion ? (
|
|
441
|
+
<div className="p-3 text-center">
|
|
442
|
+
<p className="text-xs text-slate-600">Select a question first</p>
|
|
443
|
+
</div>
|
|
444
|
+
) : availableFiles.length === 0 ? (
|
|
445
|
+
<div className="p-3 text-center">
|
|
446
|
+
<p className="text-xs text-slate-600">
|
|
447
|
+
Set CODE_CONTEXT_DIR in .env
|
|
448
|
+
</p>
|
|
449
|
+
<p className="text-xs text-slate-700 mt-1">
|
|
450
|
+
to browse project files
|
|
451
|
+
</p>
|
|
452
|
+
</div>
|
|
453
|
+
) : (
|
|
454
|
+
tree.children
|
|
455
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
456
|
+
.map((child) => (
|
|
457
|
+
<FolderNode
|
|
458
|
+
key={child.path}
|
|
459
|
+
node={child}
|
|
460
|
+
selectedFiles={selectedFiles}
|
|
461
|
+
onToggleFile={toggleFile}
|
|
462
|
+
onToggleFolder={toggleFolder}
|
|
463
|
+
expandedFolders={expandedFolders}
|
|
464
|
+
onToggleExpand={toggleExpand}
|
|
465
|
+
depth={0}
|
|
466
|
+
filter={filter}
|
|
467
|
+
onOpenFile={setViewingFile}
|
|
468
|
+
/>
|
|
469
|
+
))
|
|
470
|
+
)}
|
|
471
|
+
{/* Root-level files (if any) */}
|
|
472
|
+
{tree.files
|
|
473
|
+
.filter((f) => !filter || f.toLowerCase().includes(filter))
|
|
474
|
+
.map((filePath) => {
|
|
475
|
+
const isSelected = selectedFiles.includes(filePath);
|
|
476
|
+
return (
|
|
597
477
|
<div
|
|
598
|
-
key={
|
|
599
|
-
className=
|
|
478
|
+
key={filePath}
|
|
479
|
+
className={`flex items-center px-4 py-0.5 group ${
|
|
480
|
+
isSelected ? "text-cyan-400" : "text-slate-500"
|
|
481
|
+
}`}
|
|
600
482
|
>
|
|
601
|
-
<span
|
|
602
|
-
className="text-violet-300 font-medium truncate flex-1"
|
|
603
|
-
title={cf.label || cf.originalName}
|
|
604
|
-
>
|
|
605
|
-
{cf.label || cf.originalName}
|
|
606
|
-
</span>
|
|
607
483
|
<button
|
|
608
|
-
onClick={
|
|
609
|
-
|
|
610
|
-
`/api/context-files/${cf.id}/content`,
|
|
611
|
-
)
|
|
612
|
-
.then((r) => r.json())
|
|
613
|
-
.then((d) => d.content);
|
|
614
|
-
openCodeRunner(
|
|
615
|
-
content,
|
|
616
|
-
cf.language ?? "typescript",
|
|
617
|
-
cf.id,
|
|
618
|
-
);
|
|
619
|
-
}}
|
|
620
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
|
|
621
|
-
title="Open in Code Runner"
|
|
484
|
+
onClick={() => toggleFile(filePath)}
|
|
485
|
+
className="flex items-center gap-1.5 flex-1 min-w-0 hover:text-slate-300 transition-colors text-xs text-left"
|
|
622
486
|
>
|
|
623
|
-
<
|
|
487
|
+
<File className="w-3 h-3 shrink-0" />
|
|
488
|
+
<span className="truncate">{filePath}</span>
|
|
624
489
|
</button>
|
|
625
490
|
<button
|
|
626
|
-
onClick={() =>
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
630
|
-
title="Remove"
|
|
491
|
+
onClick={() => setViewingFile(filePath)}
|
|
492
|
+
className="shrink-0 mr-1 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
|
|
493
|
+
title="View file"
|
|
631
494
|
>
|
|
632
|
-
<
|
|
495
|
+
<Eye className="w-3 h-3" />
|
|
633
496
|
</button>
|
|
634
497
|
</div>
|
|
635
|
-
)
|
|
636
|
-
|
|
637
|
-
(f) => f.origin === "ai",
|
|
638
|
-
).length === 0 && (
|
|
639
|
-
<p className="text-[10px] text-slate-700 italic">
|
|
640
|
-
Save AI code blocks to see them here
|
|
641
|
-
</p>
|
|
642
|
-
)}
|
|
643
|
-
</div>
|
|
498
|
+
);
|
|
499
|
+
})}
|
|
644
500
|
</div>
|
|
645
|
-
)}
|
|
646
501
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
502
|
+
{/* ── My Code section ─────────────────────────────────── */}
|
|
503
|
+
{currentQuestion && (
|
|
504
|
+
<div className="border-t border-slate-800 px-3 py-2">
|
|
505
|
+
<div className="flex items-center justify-between mb-1">
|
|
506
|
+
<div className="flex items-center gap-1">
|
|
507
|
+
<Terminal className="w-3 h-3 text-emerald-400/70" />
|
|
508
|
+
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
509
|
+
My Code (
|
|
510
|
+
{
|
|
511
|
+
(currentQuestion.contextFiles || []).filter(
|
|
512
|
+
(f) => f.origin === "user",
|
|
513
|
+
).length
|
|
514
|
+
}
|
|
515
|
+
)
|
|
516
|
+
</span>
|
|
517
|
+
</div>
|
|
518
|
+
<button
|
|
519
|
+
onClick={() => openCodeRunner()}
|
|
520
|
+
className="p-0.5 rounded text-slate-600 hover:text-emerald-400 hover:bg-slate-700 transition-colors"
|
|
521
|
+
title="Open Code Runner"
|
|
522
|
+
>
|
|
523
|
+
<Plus className="w-3.5 h-3.5" />
|
|
524
|
+
</button>
|
|
525
|
+
</div>
|
|
526
|
+
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
527
|
+
{(currentQuestion.contextFiles || [])
|
|
528
|
+
.filter((f) => f.origin === "user")
|
|
529
|
+
.map((cf) => (
|
|
530
|
+
<div
|
|
531
|
+
key={cf.id}
|
|
532
|
+
className="flex items-center gap-1 text-xs bg-emerald-500/10 border border-emerald-500/20 rounded px-1.5 py-1 group"
|
|
533
|
+
>
|
|
534
|
+
<span
|
|
535
|
+
className="text-emerald-400 font-medium truncate flex-1"
|
|
536
|
+
title={cf.label || cf.originalName}
|
|
537
|
+
>
|
|
538
|
+
{cf.label || cf.originalName}
|
|
539
|
+
</span>
|
|
540
|
+
<button
|
|
541
|
+
onClick={async () => {
|
|
542
|
+
const content = await fetch(
|
|
543
|
+
`/api/context-files/${cf.id}/content`,
|
|
544
|
+
)
|
|
545
|
+
.then((r) => r.json())
|
|
546
|
+
.then((d) => d.content);
|
|
547
|
+
openCodeRunner(
|
|
548
|
+
content,
|
|
549
|
+
cf.language ?? "typescript",
|
|
550
|
+
cf.id,
|
|
551
|
+
);
|
|
552
|
+
}}
|
|
553
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-emerald-400 transition-all"
|
|
554
|
+
title="Open in Code Runner"
|
|
555
|
+
>
|
|
556
|
+
<Play className="w-3 h-3" />
|
|
557
|
+
</button>
|
|
558
|
+
<button
|
|
559
|
+
onClick={() =>
|
|
560
|
+
removeQuestionFile(currentQuestion.id, cf.id)
|
|
561
|
+
}
|
|
562
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
563
|
+
title="Remove"
|
|
564
|
+
>
|
|
565
|
+
<Trash2 className="w-3 h-3" />
|
|
566
|
+
</button>
|
|
567
|
+
</div>
|
|
568
|
+
))}
|
|
569
|
+
{(currentQuestion.contextFiles || []).filter(
|
|
570
|
+
(f) => f.origin === "user",
|
|
571
|
+
).length === 0 && (
|
|
572
|
+
<p className="text-[10px] text-slate-700 italic">
|
|
573
|
+
Save code from the runner to see it here
|
|
574
|
+
</p>
|
|
575
|
+
)}
|
|
576
|
+
</div>
|
|
661
577
|
</div>
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
578
|
+
)}
|
|
579
|
+
|
|
580
|
+
{/* ── AI Generated section ──────────────────────────── */}
|
|
581
|
+
{currentQuestion && (
|
|
582
|
+
<div className="border-t border-slate-800 px-3 py-2">
|
|
583
|
+
<div className="flex items-center gap-1 mb-1">
|
|
584
|
+
<Sparkles className="w-3 h-3 text-violet-400/70" />
|
|
585
|
+
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
586
|
+
AI Generated (
|
|
587
|
+
{
|
|
588
|
+
(currentQuestion.contextFiles || []).filter(
|
|
589
|
+
(f) => f.origin === "ai",
|
|
590
|
+
).length
|
|
591
|
+
}
|
|
592
|
+
)
|
|
593
|
+
</span>
|
|
594
|
+
</div>
|
|
595
|
+
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
596
|
+
{(currentQuestion.contextFiles || [])
|
|
597
|
+
.filter((f) => f.origin === "ai")
|
|
598
|
+
.map((cf) => (
|
|
599
|
+
<div
|
|
600
|
+
key={cf.id}
|
|
601
|
+
className="flex items-center gap-1 text-xs bg-violet-500/10 border border-violet-500/20 rounded px-1.5 py-1 group"
|
|
602
|
+
>
|
|
671
603
|
<span
|
|
672
|
-
className="text-
|
|
604
|
+
className="text-violet-300 font-medium truncate flex-1"
|
|
673
605
|
title={cf.label || cf.originalName}
|
|
674
606
|
>
|
|
675
607
|
{cf.label || cf.originalName}
|
|
676
608
|
</span>
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
609
|
+
<button
|
|
610
|
+
onClick={async () => {
|
|
611
|
+
const content = await fetch(
|
|
612
|
+
`/api/context-files/${cf.id}/content`,
|
|
613
|
+
)
|
|
614
|
+
.then((r) => r.json())
|
|
615
|
+
.then((d) => d.content);
|
|
616
|
+
openCodeRunner(
|
|
617
|
+
content,
|
|
618
|
+
cf.language ?? "typescript",
|
|
619
|
+
cf.id,
|
|
620
|
+
);
|
|
621
|
+
}}
|
|
622
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
|
|
623
|
+
title="Open in Code Runner"
|
|
624
|
+
>
|
|
625
|
+
<Play className="w-3 h-3" />
|
|
626
|
+
</button>
|
|
627
|
+
<button
|
|
628
|
+
onClick={() =>
|
|
629
|
+
removeQuestionFile(currentQuestion.id, cf.id)
|
|
630
|
+
}
|
|
631
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
632
|
+
title="Remove"
|
|
633
|
+
>
|
|
634
|
+
<Trash2 className="w-3 h-3" />
|
|
635
|
+
</button>
|
|
636
|
+
</div>
|
|
637
|
+
))}
|
|
638
|
+
{(currentQuestion.contextFiles || []).filter(
|
|
639
|
+
(f) => f.origin === "ai",
|
|
640
|
+
).length === 0 && (
|
|
641
|
+
<p className="text-[10px] text-slate-700 italic">
|
|
642
|
+
Save AI code blocks to see them here
|
|
643
|
+
</p>
|
|
644
|
+
)}
|
|
645
|
+
</div>
|
|
646
|
+
</div>
|
|
647
|
+
)}
|
|
648
|
+
|
|
649
|
+
{/* ── Sandboxes section ───────────────────────── */}
|
|
650
|
+
{currentQuestion && (
|
|
651
|
+
<div className="border-t border-slate-800 px-3 py-2">
|
|
652
|
+
<div className="flex items-center gap-1 mb-1">
|
|
653
|
+
<Server className="w-3 h-3 text-slate-500/70" />
|
|
654
|
+
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
655
|
+
Sandboxes (
|
|
656
|
+
{
|
|
657
|
+
(currentQuestion.contextFiles || []).filter(
|
|
658
|
+
(f) => f.origin === "sandbox",
|
|
659
|
+
).length
|
|
660
|
+
}
|
|
661
|
+
)
|
|
662
|
+
</span>
|
|
663
|
+
</div>
|
|
664
|
+
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
665
|
+
{(currentQuestion.contextFiles || [])
|
|
666
|
+
.filter((f) => f.origin === "sandbox")
|
|
667
|
+
.map((cf) => (
|
|
668
|
+
<div
|
|
669
|
+
key={cf.id}
|
|
670
|
+
className="flex items-center gap-1 text-xs bg-slate-500/10 border border-slate-500/20 rounded px-1.5 py-1 group"
|
|
671
|
+
>
|
|
672
|
+
{renamingId !== cf.id && (
|
|
673
|
+
<span
|
|
674
|
+
className="text-slate-300 font-medium truncate flex-1"
|
|
675
|
+
title={cf.label || cf.originalName}
|
|
676
|
+
>
|
|
677
|
+
{cf.label || cf.originalName}
|
|
678
|
+
</span>
|
|
679
|
+
)}
|
|
680
|
+
{renamingId === cf.id ? (
|
|
681
|
+
<>
|
|
682
|
+
<input
|
|
683
|
+
autoFocus
|
|
684
|
+
value={renameValue}
|
|
685
|
+
onChange={(e) => setRenameValue(e.target.value)}
|
|
686
|
+
onKeyDown={async (e) => {
|
|
687
|
+
if (e.key === "Enter") {
|
|
688
|
+
e.preventDefault();
|
|
689
|
+
if (renameValue.trim()) {
|
|
690
|
+
await renameContextFile(
|
|
691
|
+
currentQuestion.id,
|
|
692
|
+
cf.id,
|
|
693
|
+
renameValue.trim(),
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
setRenamingId(null);
|
|
697
|
+
} else if (e.key === "Escape") {
|
|
698
|
+
setRenamingId(null);
|
|
699
|
+
}
|
|
700
|
+
}}
|
|
701
|
+
className="w-28 bg-slate-900 border border-violet-600/50 rounded px-1.5 py-0.5 text-[11px] text-slate-200 outline-none focus:border-violet-500 shrink-0"
|
|
702
|
+
/>
|
|
703
|
+
<button
|
|
704
|
+
onClick={async () => {
|
|
687
705
|
if (renameValue.trim()) {
|
|
688
706
|
await renameContextFile(
|
|
689
707
|
currentQuestion.id,
|
|
@@ -692,589 +710,574 @@ export default function CodeContextPanel() {
|
|
|
692
710
|
);
|
|
693
711
|
}
|
|
694
712
|
setRenamingId(null);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
713
|
+
}}
|
|
714
|
+
className="shrink-0 p-0.5 rounded text-violet-400 hover:bg-violet-600/20 transition-colors"
|
|
715
|
+
title="Confirm"
|
|
716
|
+
>
|
|
717
|
+
<Check className="w-3 h-3" />
|
|
718
|
+
</button>
|
|
719
|
+
<button
|
|
720
|
+
onClick={() => setRenamingId(null)}
|
|
721
|
+
className="shrink-0 p-0.5 rounded text-slate-500 hover:text-slate-300 transition-colors"
|
|
722
|
+
title="Cancel"
|
|
723
|
+
>
|
|
724
|
+
<X className="w-3 h-3" />
|
|
725
|
+
</button>
|
|
726
|
+
</>
|
|
727
|
+
) : (
|
|
728
|
+
<>
|
|
729
|
+
<button
|
|
730
|
+
onClick={() => {
|
|
731
|
+
setRenamingId(cf.id);
|
|
732
|
+
setRenameValue(cf.label || cf.originalName);
|
|
733
|
+
}}
|
|
734
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-300 transition-all"
|
|
735
|
+
title="Rename"
|
|
736
|
+
>
|
|
737
|
+
<Pencil className="w-3 h-3" />
|
|
738
|
+
</button>
|
|
739
|
+
<button
|
|
740
|
+
onClick={async () => {
|
|
741
|
+
try {
|
|
742
|
+
const raw = await fetch(
|
|
743
|
+
`/api/context-files/${cf.id}/content`,
|
|
744
|
+
)
|
|
745
|
+
.then((r) => r.json())
|
|
746
|
+
.then((d) => d.content as string);
|
|
747
|
+
const parsed = JSON.parse(raw) as {
|
|
748
|
+
serverCode: string;
|
|
749
|
+
serverLang: string;
|
|
750
|
+
clientCode: string;
|
|
751
|
+
clientLang: string;
|
|
752
|
+
clientType?: "script" | "react" | "nextjs";
|
|
753
|
+
reactFiles?: Record<string, string>;
|
|
754
|
+
reactActiveFile?: string;
|
|
755
|
+
};
|
|
756
|
+
openSandbox(
|
|
757
|
+
parsed.serverCode,
|
|
758
|
+
parsed.serverLang,
|
|
759
|
+
parsed.clientCode,
|
|
760
|
+
parsed.clientLang,
|
|
761
|
+
cf.id,
|
|
762
|
+
parsed.clientType
|
|
763
|
+
? {
|
|
764
|
+
clientType: parsed.clientType,
|
|
765
|
+
reactFiles: parsed.reactFiles,
|
|
766
|
+
reactActiveFile: parsed.reactActiveFile,
|
|
767
|
+
}
|
|
768
|
+
: undefined,
|
|
769
|
+
);
|
|
770
|
+
} catch {
|
|
771
|
+
/* malformed — ignore */
|
|
772
|
+
}
|
|
773
|
+
}}
|
|
774
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
|
|
775
|
+
title="Open in Sandbox"
|
|
776
|
+
>
|
|
777
|
+
<Play className="w-3 h-3" />
|
|
778
|
+
</button>
|
|
779
|
+
<button
|
|
780
|
+
onClick={() =>
|
|
781
|
+
removeQuestionFile(currentQuestion.id, cf.id)
|
|
709
782
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
783
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
784
|
+
title="Remove"
|
|
785
|
+
>
|
|
786
|
+
<Trash2 className="w-3 h-3" />
|
|
787
|
+
</button>
|
|
788
|
+
</>
|
|
789
|
+
)}
|
|
790
|
+
</div>
|
|
791
|
+
))}
|
|
792
|
+
{(currentQuestion.contextFiles || []).filter(
|
|
793
|
+
(f) => f.origin === "sandbox",
|
|
794
|
+
).length === 0 && (
|
|
795
|
+
<p className="text-[10px] text-slate-700 italic">
|
|
796
|
+
Save a sandbox to see it here
|
|
797
|
+
</p>
|
|
798
|
+
)}
|
|
799
|
+
</div>
|
|
800
|
+
</div>
|
|
801
|
+
)}
|
|
802
|
+
|
|
803
|
+
{currentQuestion && (
|
|
804
|
+
<div className="border-t border-slate-800 px-3 py-2">
|
|
805
|
+
<div className="flex items-center justify-between mb-1">
|
|
806
|
+
<div className="flex items-center gap-1">
|
|
807
|
+
<Globe className="w-3 h-3 text-cyan-400/70" />
|
|
808
|
+
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
809
|
+
Infra Labs (
|
|
810
|
+
{
|
|
811
|
+
(currentQuestion.contextFiles || []).filter(
|
|
812
|
+
(f) => f.origin === "infra",
|
|
813
|
+
).length
|
|
814
|
+
}
|
|
815
|
+
)
|
|
816
|
+
</span>
|
|
817
|
+
</div>
|
|
818
|
+
<button
|
|
819
|
+
onClick={() => openInfraLab()}
|
|
820
|
+
className="p-0.5 rounded text-slate-600 hover:text-cyan-400 hover:bg-slate-700 transition-colors"
|
|
821
|
+
title="Open Infrastructure Lab"
|
|
822
|
+
>
|
|
823
|
+
<Plus className="w-3.5 h-3.5" />
|
|
824
|
+
</button>
|
|
825
|
+
</div>
|
|
826
|
+
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
827
|
+
{(currentQuestion.contextFiles || [])
|
|
828
|
+
.filter((f) => f.origin === "infra")
|
|
829
|
+
.map((cf) => (
|
|
830
|
+
<div
|
|
831
|
+
key={cf.id}
|
|
832
|
+
className="flex items-center gap-1 text-xs bg-cyan-500/10 border border-cyan-500/20 rounded px-1.5 py-1 group"
|
|
833
|
+
>
|
|
834
|
+
{renamingId !== cf.id && (
|
|
835
|
+
<span
|
|
836
|
+
className="text-cyan-200 font-medium truncate flex-1"
|
|
837
|
+
title={cf.label || cf.originalName}
|
|
734
838
|
>
|
|
735
|
-
|
|
736
|
-
</
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
839
|
+
{cf.label || cf.originalName}
|
|
840
|
+
</span>
|
|
841
|
+
)}
|
|
842
|
+
{renamingId === cf.id ? (
|
|
843
|
+
<>
|
|
844
|
+
<input
|
|
845
|
+
autoFocus
|
|
846
|
+
value={renameValue}
|
|
847
|
+
onChange={(e) => setRenameValue(e.target.value)}
|
|
848
|
+
onKeyDown={async (e) => {
|
|
849
|
+
if (e.key === "Enter") {
|
|
850
|
+
e.preventDefault();
|
|
851
|
+
if (renameValue.trim()) {
|
|
852
|
+
await renameContextFile(
|
|
853
|
+
currentQuestion.id,
|
|
854
|
+
cf.id,
|
|
855
|
+
renameValue.trim(),
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
setRenamingId(null);
|
|
859
|
+
} else if (e.key === "Escape") {
|
|
860
|
+
setRenamingId(null);
|
|
861
|
+
}
|
|
862
|
+
}}
|
|
863
|
+
className="w-28 bg-slate-900 border border-cyan-600/50 rounded px-1.5 py-0.5 text-[11px] text-slate-200 outline-none focus:border-cyan-500 shrink-0"
|
|
864
|
+
/>
|
|
865
|
+
<button
|
|
866
|
+
onClick={async () => {
|
|
867
|
+
if (renameValue.trim()) {
|
|
868
|
+
await renameContextFile(
|
|
869
|
+
currentQuestion.id,
|
|
870
|
+
cf.id,
|
|
871
|
+
renameValue.trim(),
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
setRenamingId(null);
|
|
875
|
+
}}
|
|
876
|
+
className="shrink-0 p-0.5 rounded text-cyan-400 hover:bg-cyan-600/20 transition-colors"
|
|
877
|
+
title="Confirm"
|
|
878
|
+
>
|
|
879
|
+
<Check className="w-3 h-3" />
|
|
880
|
+
</button>
|
|
881
|
+
<button
|
|
882
|
+
onClick={() => setRenamingId(null)}
|
|
883
|
+
className="shrink-0 p-0.5 rounded text-slate-500 hover:text-slate-300 transition-colors"
|
|
884
|
+
title="Cancel"
|
|
885
|
+
>
|
|
886
|
+
<X className="w-3 h-3" />
|
|
887
|
+
</button>
|
|
888
|
+
</>
|
|
889
|
+
) : (
|
|
890
|
+
<>
|
|
891
|
+
<button
|
|
892
|
+
onClick={() => {
|
|
893
|
+
setRenamingId(cf.id);
|
|
894
|
+
setRenameValue(cf.label || cf.originalName);
|
|
895
|
+
}}
|
|
896
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-300 transition-all"
|
|
897
|
+
title="Rename"
|
|
898
|
+
>
|
|
899
|
+
<Pencil className="w-3 h-3" />
|
|
900
|
+
</button>
|
|
901
|
+
<button
|
|
902
|
+
onClick={async () => {
|
|
740
903
|
const raw = await fetch(
|
|
741
904
|
`/api/context-files/${cf.id}/content`,
|
|
742
905
|
)
|
|
743
906
|
.then((r) => r.json())
|
|
744
907
|
.then((d) => d.content as string);
|
|
745
|
-
const parsed =
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
parsed.serverLang,
|
|
757
|
-
parsed.clientCode,
|
|
758
|
-
parsed.clientLang,
|
|
759
|
-
cf.id,
|
|
760
|
-
parsed.clientType
|
|
761
|
-
? {
|
|
762
|
-
clientType: parsed.clientType,
|
|
763
|
-
reactFiles: parsed.reactFiles,
|
|
764
|
-
reactActiveFile: parsed.reactActiveFile,
|
|
765
|
-
}
|
|
766
|
-
: undefined,
|
|
767
|
-
);
|
|
768
|
-
} catch {
|
|
769
|
-
/* malformed — ignore */
|
|
908
|
+
const parsed = parseInfraLabWorkspace(raw);
|
|
909
|
+
if (parsed) openInfraLab(parsed, cf.id);
|
|
910
|
+
}}
|
|
911
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
|
|
912
|
+
title="Open in Infrastructure Lab"
|
|
913
|
+
>
|
|
914
|
+
<Play className="w-3 h-3" />
|
|
915
|
+
</button>
|
|
916
|
+
<button
|
|
917
|
+
onClick={() =>
|
|
918
|
+
removeQuestionFile(currentQuestion.id, cf.id)
|
|
770
919
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
</div>
|
|
789
|
-
))}
|
|
790
|
-
{(currentQuestion.contextFiles || []).filter(
|
|
791
|
-
(f) => f.origin === "sandbox",
|
|
792
|
-
).length === 0 && (
|
|
793
|
-
<p className="text-[10px] text-slate-700 italic">
|
|
794
|
-
Save a sandbox to see it here
|
|
795
|
-
</p>
|
|
796
|
-
)}
|
|
920
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
921
|
+
title="Remove"
|
|
922
|
+
>
|
|
923
|
+
<Trash2 className="w-3 h-3" />
|
|
924
|
+
</button>
|
|
925
|
+
</>
|
|
926
|
+
)}
|
|
927
|
+
</div>
|
|
928
|
+
))}
|
|
929
|
+
{(currentQuestion.contextFiles || []).filter(
|
|
930
|
+
(f) => f.origin === "infra",
|
|
931
|
+
).length === 0 && (
|
|
932
|
+
<p className="text-[10px] text-slate-700 italic">
|
|
933
|
+
Save an infra lab to reopen it here
|
|
934
|
+
</p>
|
|
935
|
+
)}
|
|
936
|
+
</div>
|
|
797
937
|
</div>
|
|
798
|
-
|
|
799
|
-
)}
|
|
938
|
+
)}
|
|
800
939
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
<div className="
|
|
804
|
-
<div className="flex items-center
|
|
805
|
-
<
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
(
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
940
|
+
{/* ── React Labs section ───────────────────── */}
|
|
941
|
+
{currentQuestion && (
|
|
942
|
+
<div className="border-t border-slate-800 px-3 py-2">
|
|
943
|
+
<div className="flex items-center justify-between mb-1">
|
|
944
|
+
<div className="flex items-center gap-1">
|
|
945
|
+
<Atom className="w-3 h-3 text-cyan-400/70" />
|
|
946
|
+
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
947
|
+
React Labs (
|
|
948
|
+
{
|
|
949
|
+
(currentQuestion.contextFiles || []).filter(
|
|
950
|
+
(f) => f.origin === "react",
|
|
951
|
+
).length
|
|
952
|
+
}
|
|
953
|
+
)
|
|
954
|
+
</span>
|
|
955
|
+
</div>
|
|
956
|
+
<button
|
|
957
|
+
onClick={() => openReactLab()}
|
|
958
|
+
className="p-0.5 rounded text-slate-600 hover:text-cyan-400 hover:bg-slate-700 transition-colors"
|
|
959
|
+
title="Open React Lab"
|
|
960
|
+
>
|
|
961
|
+
<Plus className="w-3.5 h-3.5" />
|
|
962
|
+
</button>
|
|
815
963
|
</div>
|
|
816
|
-
<
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
825
|
-
{(currentQuestion.contextFiles || [])
|
|
826
|
-
.filter((f) => f.origin === "infra")
|
|
827
|
-
.map((cf) => (
|
|
828
|
-
<div
|
|
829
|
-
key={cf.id}
|
|
830
|
-
className="flex items-center gap-1 text-xs bg-cyan-500/10 border border-cyan-500/20 rounded px-1.5 py-1 group"
|
|
831
|
-
>
|
|
832
|
-
{renamingId !== cf.id && (
|
|
964
|
+
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
965
|
+
{(currentQuestion.contextFiles || [])
|
|
966
|
+
.filter((f) => f.origin === "react")
|
|
967
|
+
.map((cf) => (
|
|
968
|
+
<div
|
|
969
|
+
key={cf.id}
|
|
970
|
+
className="flex items-center gap-1 text-xs bg-cyan-500/10 border border-cyan-500/20 rounded px-1.5 py-1 group"
|
|
971
|
+
>
|
|
833
972
|
<span
|
|
834
973
|
className="text-cyan-200 font-medium truncate flex-1"
|
|
835
974
|
title={cf.label || cf.originalName}
|
|
836
975
|
>
|
|
837
976
|
{cf.label || cf.originalName}
|
|
838
977
|
</span>
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
<input
|
|
843
|
-
autoFocus
|
|
844
|
-
value={renameValue}
|
|
845
|
-
onChange={(e) => setRenameValue(e.target.value)}
|
|
846
|
-
onKeyDown={async (e) => {
|
|
847
|
-
if (e.key === "Enter") {
|
|
848
|
-
e.preventDefault();
|
|
849
|
-
if (renameValue.trim()) {
|
|
850
|
-
await renameContextFile(
|
|
851
|
-
currentQuestion.id,
|
|
852
|
-
cf.id,
|
|
853
|
-
renameValue.trim(),
|
|
854
|
-
);
|
|
855
|
-
}
|
|
856
|
-
setRenamingId(null);
|
|
857
|
-
} else if (e.key === "Escape") {
|
|
858
|
-
setRenamingId(null);
|
|
859
|
-
}
|
|
860
|
-
}}
|
|
861
|
-
className="w-28 bg-slate-900 border border-cyan-600/50 rounded px-1.5 py-0.5 text-[11px] text-slate-200 outline-none focus:border-cyan-500 shrink-0"
|
|
862
|
-
/>
|
|
863
|
-
<button
|
|
864
|
-
onClick={async () => {
|
|
865
|
-
if (renameValue.trim()) {
|
|
866
|
-
await renameContextFile(
|
|
867
|
-
currentQuestion.id,
|
|
868
|
-
cf.id,
|
|
869
|
-
renameValue.trim(),
|
|
870
|
-
);
|
|
871
|
-
}
|
|
872
|
-
setRenamingId(null);
|
|
873
|
-
}}
|
|
874
|
-
className="shrink-0 p-0.5 rounded text-cyan-400 hover:bg-cyan-600/20 transition-colors"
|
|
875
|
-
title="Confirm"
|
|
876
|
-
>
|
|
877
|
-
<Check className="w-3 h-3" />
|
|
878
|
-
</button>
|
|
879
|
-
<button
|
|
880
|
-
onClick={() => setRenamingId(null)}
|
|
881
|
-
className="shrink-0 p-0.5 rounded text-slate-500 hover:text-slate-300 transition-colors"
|
|
882
|
-
title="Cancel"
|
|
883
|
-
>
|
|
884
|
-
<X className="w-3 h-3" />
|
|
885
|
-
</button>
|
|
886
|
-
</>
|
|
887
|
-
) : (
|
|
888
|
-
<>
|
|
889
|
-
<button
|
|
890
|
-
onClick={() => {
|
|
891
|
-
setRenamingId(cf.id);
|
|
892
|
-
setRenameValue(cf.label || cf.originalName);
|
|
893
|
-
}}
|
|
894
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-300 transition-all"
|
|
895
|
-
title="Rename"
|
|
896
|
-
>
|
|
897
|
-
<Pencil className="w-3 h-3" />
|
|
898
|
-
</button>
|
|
899
|
-
<button
|
|
900
|
-
onClick={async () => {
|
|
978
|
+
<button
|
|
979
|
+
onClick={async () => {
|
|
980
|
+
try {
|
|
901
981
|
const raw = await fetch(
|
|
902
982
|
`/api/context-files/${cf.id}/content`,
|
|
903
983
|
)
|
|
904
984
|
.then((r) => r.json())
|
|
905
985
|
.then((d) => d.content as string);
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
986
|
+
// Try new extended sandbox format first
|
|
987
|
+
const ext = JSON.parse(raw) as {
|
|
988
|
+
clientType?: string;
|
|
989
|
+
reactFiles?: Record<string, string>;
|
|
990
|
+
reactActiveFile?: string;
|
|
991
|
+
serverCode?: string;
|
|
992
|
+
serverLang?: string;
|
|
993
|
+
};
|
|
994
|
+
if (ext?.clientType === "react" && ext.reactFiles) {
|
|
995
|
+
openReactLab(
|
|
996
|
+
{
|
|
997
|
+
version: 1,
|
|
998
|
+
type: "react",
|
|
999
|
+
label: cf.label || "React Lab",
|
|
1000
|
+
activeFile:
|
|
1001
|
+
ext.reactActiveFile ??
|
|
1002
|
+
Object.keys(ext.reactFiles)[0] ??
|
|
1003
|
+
"App.tsx",
|
|
1004
|
+
files: ext.reactFiles,
|
|
1005
|
+
},
|
|
1006
|
+
cf.id,
|
|
1007
|
+
ext.serverCode,
|
|
1008
|
+
ext.serverLang,
|
|
1009
|
+
);
|
|
1010
|
+
} else {
|
|
1011
|
+
// Fall back to old FrontendLabWorkspace format
|
|
1012
|
+
const ws = parseFrontendLabWorkspace(raw);
|
|
1013
|
+
if (ws) openReactLab(ws, cf.id);
|
|
1014
|
+
}
|
|
1015
|
+
} catch {
|
|
1016
|
+
/* ignore */
|
|
917
1017
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1018
|
+
}}
|
|
1019
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
|
|
1020
|
+
title="Open in React Lab"
|
|
1021
|
+
>
|
|
1022
|
+
<Play className="w-3 h-3" />
|
|
1023
|
+
</button>
|
|
1024
|
+
<button
|
|
1025
|
+
onClick={() =>
|
|
1026
|
+
removeQuestionFile(currentQuestion.id, cf.id)
|
|
1027
|
+
}
|
|
1028
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
1029
|
+
title="Remove"
|
|
1030
|
+
>
|
|
1031
|
+
<Trash2 className="w-3 h-3" />
|
|
1032
|
+
</button>
|
|
1033
|
+
</div>
|
|
1034
|
+
))}
|
|
1035
|
+
{(currentQuestion.contextFiles || []).filter(
|
|
1036
|
+
(f) => f.origin === "react",
|
|
1037
|
+
).length === 0 && (
|
|
1038
|
+
<p className="text-[10px] text-slate-700 italic">
|
|
1039
|
+
Save a React lab to reopen it here
|
|
1040
|
+
</p>
|
|
1041
|
+
)}
|
|
1042
|
+
</div>
|
|
934
1043
|
</div>
|
|
935
|
-
|
|
936
|
-
)}
|
|
1044
|
+
)}
|
|
937
1045
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1046
|
+
{/* ── Next.js Labs section ──────────────────── */}
|
|
1047
|
+
{currentQuestion && (
|
|
1048
|
+
<div className="border-t border-slate-800 px-3 py-2">
|
|
1049
|
+
<div className="flex items-center justify-between mb-1">
|
|
1050
|
+
<div className="flex items-center gap-1">
|
|
1051
|
+
<Layout className="w-3 h-3 text-violet-400/70" />
|
|
1052
|
+
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
1053
|
+
Next.js Labs (
|
|
1054
|
+
{
|
|
1055
|
+
(currentQuestion.contextFiles || []).filter(
|
|
1056
|
+
(f) => f.origin === "nextjs",
|
|
1057
|
+
).length
|
|
1058
|
+
}
|
|
1059
|
+
)
|
|
1060
|
+
</span>
|
|
1061
|
+
</div>
|
|
1062
|
+
<button
|
|
1063
|
+
onClick={() => openNextLab()}
|
|
1064
|
+
className="p-0.5 rounded text-slate-600 hover:text-violet-400 hover:bg-slate-700 transition-colors"
|
|
1065
|
+
title="Open Next.js Lab"
|
|
1066
|
+
>
|
|
1067
|
+
<Plus className="w-3.5 h-3.5" />
|
|
1068
|
+
</button>
|
|
953
1069
|
</div>
|
|
954
|
-
<
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
</div>
|
|
962
|
-
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
963
|
-
{(currentQuestion.contextFiles || [])
|
|
964
|
-
.filter((f) => f.origin === "react")
|
|
965
|
-
.map((cf) => (
|
|
966
|
-
<div
|
|
967
|
-
key={cf.id}
|
|
968
|
-
className="flex items-center gap-1 text-xs bg-cyan-500/10 border border-cyan-500/20 rounded px-1.5 py-1 group"
|
|
969
|
-
>
|
|
970
|
-
<span
|
|
971
|
-
className="text-cyan-200 font-medium truncate flex-1"
|
|
972
|
-
title={cf.label || cf.originalName}
|
|
1070
|
+
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
1071
|
+
{(currentQuestion.contextFiles || [])
|
|
1072
|
+
.filter((f) => f.origin === "nextjs")
|
|
1073
|
+
.map((cf) => (
|
|
1074
|
+
<div
|
|
1075
|
+
key={cf.id}
|
|
1076
|
+
className="flex items-center gap-1 text-xs bg-violet-500/10 border border-violet-500/20 rounded px-1.5 py-1 group"
|
|
973
1077
|
>
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1078
|
+
<span
|
|
1079
|
+
className="text-violet-200 font-medium truncate flex-1"
|
|
1080
|
+
title={cf.label || cf.originalName}
|
|
1081
|
+
>
|
|
1082
|
+
{cf.label || cf.originalName}
|
|
1083
|
+
</span>
|
|
1084
|
+
<button
|
|
1085
|
+
onClick={async () => {
|
|
1086
|
+
try {
|
|
1087
|
+
const raw = await fetch(
|
|
1088
|
+
`/api/context-files/${cf.id}/content`,
|
|
1089
|
+
)
|
|
1090
|
+
.then((r) => r.json())
|
|
1091
|
+
.then((d) => d.content as string);
|
|
1092
|
+
const ext = JSON.parse(raw) as {
|
|
1093
|
+
clientType?: string;
|
|
1094
|
+
reactFiles?: Record<string, string>;
|
|
1095
|
+
reactActiveFile?: string;
|
|
1096
|
+
serverCode?: string;
|
|
1097
|
+
serverLang?: string;
|
|
1098
|
+
};
|
|
1099
|
+
if (ext?.clientType === "nextjs" && ext.reactFiles) {
|
|
1100
|
+
openNextLab(
|
|
1101
|
+
{
|
|
1102
|
+
version: 1,
|
|
1103
|
+
type: "nextjs",
|
|
1104
|
+
label: cf.label || "Next.js Lab",
|
|
1105
|
+
activeFile:
|
|
1106
|
+
ext.reactActiveFile ??
|
|
1107
|
+
Object.keys(ext.reactFiles)[0] ??
|
|
1108
|
+
"app/page.tsx",
|
|
1109
|
+
files: ext.reactFiles,
|
|
1110
|
+
},
|
|
1111
|
+
cf.id,
|
|
1112
|
+
ext.serverCode,
|
|
1113
|
+
ext.serverLang,
|
|
1114
|
+
);
|
|
1115
|
+
} else {
|
|
1116
|
+
const ws = parseFrontendLabWorkspace(raw);
|
|
1117
|
+
if (ws) openNextLab(ws, cf.id);
|
|
1118
|
+
}
|
|
1119
|
+
} catch {
|
|
1120
|
+
/* ignore */
|
|
1012
1121
|
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1122
|
+
}}
|
|
1123
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
|
|
1124
|
+
title="Open in Next.js Lab"
|
|
1125
|
+
>
|
|
1126
|
+
<Play className="w-3 h-3" />
|
|
1127
|
+
</button>
|
|
1128
|
+
<button
|
|
1129
|
+
onClick={() =>
|
|
1130
|
+
removeQuestionFile(currentQuestion.id, cf.id)
|
|
1015
1131
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
</
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
</div>
|
|
1032
|
-
))}
|
|
1033
|
-
{(currentQuestion.contextFiles || []).filter(
|
|
1034
|
-
(f) => f.origin === "react",
|
|
1035
|
-
).length === 0 && (
|
|
1036
|
-
<p className="text-[10px] text-slate-700 italic">
|
|
1037
|
-
Save a React lab to reopen it here
|
|
1038
|
-
</p>
|
|
1039
|
-
)}
|
|
1132
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
1133
|
+
title="Remove"
|
|
1134
|
+
>
|
|
1135
|
+
<Trash2 className="w-3 h-3" />
|
|
1136
|
+
</button>
|
|
1137
|
+
</div>
|
|
1138
|
+
))}
|
|
1139
|
+
{(currentQuestion.contextFiles || []).filter(
|
|
1140
|
+
(f) => f.origin === "nextjs",
|
|
1141
|
+
).length === 0 && (
|
|
1142
|
+
<p className="text-[10px] text-slate-700 italic">
|
|
1143
|
+
Save a Next.js lab to reopen it here
|
|
1144
|
+
</p>
|
|
1145
|
+
)}
|
|
1146
|
+
</div>
|
|
1040
1147
|
</div>
|
|
1041
|
-
|
|
1042
|
-
)}
|
|
1148
|
+
)}
|
|
1043
1149
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1150
|
+
{/* ── Webpack Module Federation Labs section ───────────── */}
|
|
1151
|
+
{currentQuestion && (
|
|
1152
|
+
<div className="border-t border-slate-800 px-3 py-2">
|
|
1153
|
+
<div className="flex items-center justify-between mb-1">
|
|
1154
|
+
<div className="flex items-center gap-1">
|
|
1155
|
+
<Server className="w-3 h-3 text-emerald-400/70" />
|
|
1156
|
+
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
|
1157
|
+
Webpack MF Labs (
|
|
1158
|
+
{
|
|
1159
|
+
(currentQuestion.contextFiles || []).filter(
|
|
1160
|
+
(f) => f.origin === "module-federation",
|
|
1161
|
+
).length
|
|
1162
|
+
}
|
|
1163
|
+
)
|
|
1164
|
+
</span>
|
|
1165
|
+
</div>
|
|
1166
|
+
<button
|
|
1167
|
+
onClick={() => openModuleFederationLab()}
|
|
1168
|
+
className="p-0.5 rounded text-slate-600 hover:text-emerald-400 hover:bg-slate-700 transition-colors"
|
|
1169
|
+
title="Open Webpack Module Federation Lab"
|
|
1170
|
+
>
|
|
1171
|
+
<Plus className="w-3.5 h-3.5" />
|
|
1172
|
+
</button>
|
|
1059
1173
|
</div>
|
|
1060
|
-
<
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
</div>
|
|
1068
|
-
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
1069
|
-
{(currentQuestion.contextFiles || [])
|
|
1070
|
-
.filter((f) => f.origin === "nextjs")
|
|
1071
|
-
.map((cf) => (
|
|
1072
|
-
<div
|
|
1073
|
-
key={cf.id}
|
|
1074
|
-
className="flex items-center gap-1 text-xs bg-violet-500/10 border border-violet-500/20 rounded px-1.5 py-1 group"
|
|
1075
|
-
>
|
|
1076
|
-
<span
|
|
1077
|
-
className="text-violet-200 font-medium truncate flex-1"
|
|
1078
|
-
title={cf.label || cf.originalName}
|
|
1174
|
+
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
1175
|
+
{(currentQuestion.contextFiles || [])
|
|
1176
|
+
.filter((f) => f.origin === "module-federation")
|
|
1177
|
+
.map((cf) => (
|
|
1178
|
+
<div
|
|
1179
|
+
key={cf.id}
|
|
1180
|
+
className="flex items-center gap-1 text-xs bg-emerald-500/10 border border-emerald-500/20 rounded px-1.5 py-1 group"
|
|
1079
1181
|
>
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1182
|
+
<span
|
|
1183
|
+
className="text-emerald-200 font-medium truncate flex-1"
|
|
1184
|
+
title={cf.label || cf.originalName}
|
|
1185
|
+
>
|
|
1186
|
+
{cf.label || cf.originalName}
|
|
1187
|
+
</span>
|
|
1188
|
+
<button
|
|
1189
|
+
onClick={async () => {
|
|
1190
|
+
try {
|
|
1191
|
+
const raw = await fetch(
|
|
1192
|
+
`/api/context-files/${cf.id}/content`,
|
|
1193
|
+
)
|
|
1194
|
+
.then((r) => r.json())
|
|
1195
|
+
.then((d) => d.content as string);
|
|
1196
|
+
const ext = JSON.parse(raw) as {
|
|
1197
|
+
clientType?: string;
|
|
1198
|
+
reactFiles?: Record<string, string>;
|
|
1199
|
+
reactActiveFile?: string;
|
|
1200
|
+
serverCode?: string;
|
|
1201
|
+
serverLang?: string;
|
|
1202
|
+
};
|
|
1203
|
+
if (
|
|
1204
|
+
ext?.clientType === "module-federation" &&
|
|
1205
|
+
ext.reactFiles
|
|
1206
|
+
) {
|
|
1207
|
+
openModuleFederationLab(
|
|
1208
|
+
{
|
|
1209
|
+
version: 1,
|
|
1210
|
+
type: "module-federation",
|
|
1211
|
+
label:
|
|
1212
|
+
cf.label || "Webpack Module Federation Lab",
|
|
1213
|
+
activeFile:
|
|
1214
|
+
ext.reactActiveFile ??
|
|
1215
|
+
Object.keys(ext.reactFiles)[0] ??
|
|
1216
|
+
"apps/host/webpack.config.js",
|
|
1217
|
+
files: ext.reactFiles,
|
|
1218
|
+
},
|
|
1219
|
+
cf.id,
|
|
1220
|
+
ext.serverCode,
|
|
1221
|
+
ext.serverLang,
|
|
1222
|
+
);
|
|
1223
|
+
} else {
|
|
1224
|
+
const ws = parseFrontendLabWorkspace(raw);
|
|
1225
|
+
if (ws?.type === "module-federation") {
|
|
1226
|
+
openModuleFederationLab(ws, cf.id);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
} catch {
|
|
1230
|
+
/* ignore */
|
|
1116
1231
|
}
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1232
|
+
}}
|
|
1233
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-emerald-400 transition-all"
|
|
1234
|
+
title="Open in Webpack Module Federation Lab"
|
|
1235
|
+
>
|
|
1236
|
+
<Play className="w-3 h-3" />
|
|
1237
|
+
</button>
|
|
1238
|
+
<button
|
|
1239
|
+
onClick={() =>
|
|
1240
|
+
removeQuestionFile(currentQuestion.id, cf.id)
|
|
1119
1241
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
</
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
</div>
|
|
1136
|
-
))}
|
|
1137
|
-
{(currentQuestion.contextFiles || []).filter(
|
|
1138
|
-
(f) => f.origin === "nextjs",
|
|
1139
|
-
).length === 0 && (
|
|
1140
|
-
<p className="text-[10px] text-slate-700 italic">
|
|
1141
|
-
Save a Next.js lab to reopen it here
|
|
1142
|
-
</p>
|
|
1143
|
-
)}
|
|
1242
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
1243
|
+
title="Remove"
|
|
1244
|
+
>
|
|
1245
|
+
<Trash2 className="w-3 h-3" />
|
|
1246
|
+
</button>
|
|
1247
|
+
</div>
|
|
1248
|
+
))}
|
|
1249
|
+
{(currentQuestion.contextFiles || []).filter(
|
|
1250
|
+
(f) => f.origin === "module-federation",
|
|
1251
|
+
).length === 0 && (
|
|
1252
|
+
<p className="text-[10px] text-slate-700 italic">
|
|
1253
|
+
Save a webpack module federation lab to reopen it here
|
|
1254
|
+
</p>
|
|
1255
|
+
)}
|
|
1256
|
+
</div>
|
|
1144
1257
|
</div>
|
|
1145
|
-
|
|
1146
|
-
)}
|
|
1258
|
+
)}
|
|
1147
1259
|
|
|
1148
|
-
|
|
1149
|
-
{currentQuestion && (
|
|
1260
|
+
{/* ── Notes section ────────────────────────────────────── */}
|
|
1150
1261
|
<div className="border-t border-slate-800 px-3 py-2">
|
|
1151
|
-
<
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
<button
|
|
1165
|
-
onClick={() => openModuleFederationLab()}
|
|
1166
|
-
className="p-0.5 rounded text-slate-600 hover:text-emerald-400 hover:bg-slate-700 transition-colors"
|
|
1167
|
-
title="Open Webpack Module Federation Lab"
|
|
1168
|
-
>
|
|
1169
|
-
<Plus className="w-3.5 h-3.5" />
|
|
1170
|
-
</button>
|
|
1171
|
-
</div>
|
|
1172
|
-
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
|
1173
|
-
{(currentQuestion.contextFiles || [])
|
|
1174
|
-
.filter((f) => f.origin === "module-federation")
|
|
1175
|
-
.map((cf) => (
|
|
1176
|
-
<div
|
|
1177
|
-
key={cf.id}
|
|
1178
|
-
className="flex items-center gap-1 text-xs bg-emerald-500/10 border border-emerald-500/20 rounded px-1.5 py-1 group"
|
|
1179
|
-
>
|
|
1180
|
-
<span
|
|
1181
|
-
className="text-emerald-200 font-medium truncate flex-1"
|
|
1182
|
-
title={cf.label || cf.originalName}
|
|
1183
|
-
>
|
|
1184
|
-
{cf.label || cf.originalName}
|
|
1185
|
-
</span>
|
|
1186
|
-
<button
|
|
1187
|
-
onClick={async () => {
|
|
1188
|
-
try {
|
|
1189
|
-
const raw = await fetch(
|
|
1190
|
-
`/api/context-files/${cf.id}/content`,
|
|
1191
|
-
)
|
|
1192
|
-
.then((r) => r.json())
|
|
1193
|
-
.then((d) => d.content as string);
|
|
1194
|
-
const ext = JSON.parse(raw) as {
|
|
1195
|
-
clientType?: string;
|
|
1196
|
-
reactFiles?: Record<string, string>;
|
|
1197
|
-
reactActiveFile?: string;
|
|
1198
|
-
serverCode?: string;
|
|
1199
|
-
serverLang?: string;
|
|
1200
|
-
};
|
|
1201
|
-
if (
|
|
1202
|
-
ext?.clientType === "module-federation" &&
|
|
1203
|
-
ext.reactFiles
|
|
1204
|
-
) {
|
|
1205
|
-
openModuleFederationLab(
|
|
1206
|
-
{
|
|
1207
|
-
version: 1,
|
|
1208
|
-
type: "module-federation",
|
|
1209
|
-
label:
|
|
1210
|
-
cf.label || "Webpack Module Federation Lab",
|
|
1211
|
-
activeFile:
|
|
1212
|
-
ext.reactActiveFile ??
|
|
1213
|
-
Object.keys(ext.reactFiles)[0] ??
|
|
1214
|
-
"apps/host/webpack.config.js",
|
|
1215
|
-
files: ext.reactFiles,
|
|
1216
|
-
},
|
|
1217
|
-
cf.id,
|
|
1218
|
-
ext.serverCode,
|
|
1219
|
-
ext.serverLang,
|
|
1220
|
-
);
|
|
1221
|
-
} else {
|
|
1222
|
-
const ws = parseFrontendLabWorkspace(raw);
|
|
1223
|
-
if (ws?.type === "module-federation") {
|
|
1224
|
-
openModuleFederationLab(ws, cf.id);
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
} catch {
|
|
1228
|
-
/* ignore */
|
|
1229
|
-
}
|
|
1230
|
-
}}
|
|
1231
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-emerald-400 transition-all"
|
|
1232
|
-
title="Open in Webpack Module Federation Lab"
|
|
1233
|
-
>
|
|
1234
|
-
<Play className="w-3 h-3" />
|
|
1235
|
-
</button>
|
|
1236
|
-
<button
|
|
1237
|
-
onClick={() =>
|
|
1238
|
-
removeQuestionFile(currentQuestion.id, cf.id)
|
|
1239
|
-
}
|
|
1240
|
-
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
|
|
1241
|
-
title="Remove"
|
|
1242
|
-
>
|
|
1243
|
-
<Trash2 className="w-3 h-3" />
|
|
1244
|
-
</button>
|
|
1245
|
-
</div>
|
|
1246
|
-
))}
|
|
1247
|
-
{(currentQuestion.contextFiles || []).filter(
|
|
1248
|
-
(f) => f.origin === "module-federation",
|
|
1249
|
-
).length === 0 && (
|
|
1250
|
-
<p className="text-[10px] text-slate-700 italic">
|
|
1251
|
-
Save a webpack module federation lab to reopen it here
|
|
1252
|
-
</p>
|
|
1262
|
+
<button
|
|
1263
|
+
onClick={() => setNotesOpen((v) => !v)}
|
|
1264
|
+
className="w-full flex items-center gap-2 group"
|
|
1265
|
+
>
|
|
1266
|
+
<NotebookPen className="w-3 h-3 text-amber-400/70 shrink-0" />
|
|
1267
|
+
<span className="text-[10px] uppercase tracking-wider text-slate-600 flex-1 text-left">
|
|
1268
|
+
Notes
|
|
1269
|
+
</span>
|
|
1270
|
+
{hasNotes && (
|
|
1271
|
+
<span
|
|
1272
|
+
className="w-1.5 h-1.5 rounded-full bg-amber-400/70 shrink-0"
|
|
1273
|
+
title="Has notes"
|
|
1274
|
+
/>
|
|
1253
1275
|
)}
|
|
1254
|
-
|
|
1276
|
+
<span className="text-[10px] text-slate-700 group-hover:text-slate-500 transition-colors">
|
|
1277
|
+
{notesOpen ? "close" : "open"}
|
|
1278
|
+
</span>
|
|
1279
|
+
</button>
|
|
1255
1280
|
</div>
|
|
1256
|
-
)}
|
|
1257
|
-
|
|
1258
|
-
{/* ── Notes section ────────────────────────────────────── */}
|
|
1259
|
-
<div className="border-t border-slate-800 px-3 py-2">
|
|
1260
|
-
<button
|
|
1261
|
-
onClick={() => setNotesOpen((v) => !v)}
|
|
1262
|
-
className="w-full flex items-center gap-2 group"
|
|
1263
|
-
>
|
|
1264
|
-
<NotebookPen className="w-3 h-3 text-amber-400/70 shrink-0" />
|
|
1265
|
-
<span className="text-[10px] uppercase tracking-wider text-slate-600 flex-1 text-left">
|
|
1266
|
-
Notes
|
|
1267
|
-
</span>
|
|
1268
|
-
{hasNotes && (
|
|
1269
|
-
<span
|
|
1270
|
-
className="w-1.5 h-1.5 rounded-full bg-amber-400/70 shrink-0"
|
|
1271
|
-
title="Has notes"
|
|
1272
|
-
/>
|
|
1273
|
-
)}
|
|
1274
|
-
<span className="text-[10px] text-slate-700 group-hover:text-slate-500 transition-colors">
|
|
1275
|
-
{notesOpen ? "close" : "open"}
|
|
1276
|
-
</span>
|
|
1277
|
-
</button>
|
|
1278
1281
|
</div>
|
|
1279
1282
|
|
|
1280
1283
|
{viewingFile && (
|