create-interview-cockpit 0.9.0 → 0.10.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.
@@ -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
- {/* Snippets section */}
344
- {codeSnippets.length > 0 && (
345
- <div className="border-b border-slate-800 px-3 py-2">
346
- <div className="flex items-center justify-between mb-1">
347
- <div className="flex items-center gap-1">
348
- <Scissors className="w-3 h-3 text-amber-400/70" />
349
- <span className="text-[10px] uppercase tracking-wider text-slate-600">
350
- Snippets ({codeSnippets.length})
351
- </span>
352
- </div>
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">&rsaquo;</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
- </div>
390
- </div>
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
- <Check className="w-2.5 h-2.5 shrink-0" />
417
- <button
418
- onClick={() => setViewingFile(f)}
419
- className="truncate flex-1 text-left hover:underline"
420
- title="View file"
421
- >
422
- {f.split("/").pop()}
423
- </button>
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
- <X className="w-2.5 h-2.5" />
429
- </button>
430
- </div>
431
- ))}
432
- </div>
433
- </div>
434
- )}
435
-
436
- {/* Tree browser */}
437
- <div className="flex-1 overflow-y-auto py-1">
438
- {!currentQuestion ? (
439
- <div className="p-3 text-center">
440
- <p className="text-xs text-slate-600">Select a question first</p>
441
- </div>
442
- ) : availableFiles.length === 0 ? (
443
- <div className="p-3 text-center">
444
- <p className="text-xs text-slate-600">
445
- Set CODE_CONTEXT_DIR in .env
446
- </p>
447
- <p className="text-xs text-slate-700 mt-1">
448
- to browse project files
449
- </p>
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">&rsaquo;</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
- {/* ── My Code section ─────────────────────────────────── */}
501
- {currentQuestion && (
502
- <div className="border-t border-slate-800 px-3 py-2">
503
- <div className="flex items-center justify-between mb-1">
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
- My Code (
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
- <button
517
- onClick={() => openCodeRunner()}
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={cf.id}
530
- className="flex items-center gap-1 text-xs bg-emerald-500/10 border border-emerald-500/20 rounded px-1.5 py-1 group"
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
- <span
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={async () => {
540
- const content = await fetch(
541
- `/api/context-files/${cf.id}/content`,
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
- <Play className="w-3 h-3" />
424
+ {f.split("/").pop()}
555
425
  </button>
556
426
  <button
557
- onClick={() =>
558
- removeQuestionFile(currentQuestion.id, cf.id)
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
- <Trash2 className="w-3 h-3" />
430
+ <X className="w-2.5 h-2.5" />
564
431
  </button>
565
432
  </div>
566
433
  ))}
567
- {(currentQuestion.contextFiles || []).filter(
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
- </div>
576
- )}
436
+ )}
577
437
 
578
- {/* ── AI Generated section ──────────────────────────── */}
579
- {currentQuestion && (
580
- <div className="border-t border-slate-800 px-3 py-2">
581
- <div className="flex items-center gap-1 mb-1">
582
- <Sparkles className="w-3 h-3 text-violet-400/70" />
583
- <span className="text-[10px] uppercase tracking-wider text-slate-600">
584
- AI Generated (
585
- {
586
- (currentQuestion.contextFiles || []).filter(
587
- (f) => f.origin === "ai",
588
- ).length
589
- }
590
- )
591
- </span>
592
- </div>
593
- <div className="space-y-0.5 max-h-32 overflow-y-auto">
594
- {(currentQuestion.contextFiles || [])
595
- .filter((f) => f.origin === "ai")
596
- .map((cf) => (
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={cf.id}
599
- className="flex items-center gap-1 text-xs bg-violet-500/10 border border-violet-500/20 rounded px-1.5 py-1 group"
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={async () => {
609
- const content = await fetch(
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
- <Play className="w-3 h-3" />
487
+ <File className="w-3 h-3 shrink-0" />
488
+ <span className="truncate">{filePath}</span>
624
489
  </button>
625
490
  <button
626
- onClick={() =>
627
- removeQuestionFile(currentQuestion.id, cf.id)
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
- <Trash2 className="w-3 h-3" />
495
+ <Eye className="w-3 h-3" />
633
496
  </button>
634
497
  </div>
635
- ))}
636
- {(currentQuestion.contextFiles || []).filter(
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
- {/* ── Sandboxes section ───────────────────────── */}
648
- {currentQuestion && (
649
- <div className="border-t border-slate-800 px-3 py-2">
650
- <div className="flex items-center gap-1 mb-1">
651
- <Server className="w-3 h-3 text-slate-500/70" />
652
- <span className="text-[10px] uppercase tracking-wider text-slate-600">
653
- Sandboxes (
654
- {
655
- (currentQuestion.contextFiles || []).filter(
656
- (f) => f.origin === "sandbox",
657
- ).length
658
- }
659
- )
660
- </span>
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
- <div className="space-y-0.5 max-h-32 overflow-y-auto">
663
- {(currentQuestion.contextFiles || [])
664
- .filter((f) => f.origin === "sandbox")
665
- .map((cf) => (
666
- <div
667
- key={cf.id}
668
- className="flex items-center gap-1 text-xs bg-slate-500/10 border border-slate-500/20 rounded px-1.5 py-1 group"
669
- >
670
- {renamingId !== cf.id && (
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-slate-300 font-medium truncate flex-1"
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
- {renamingId === cf.id ? (
679
- <>
680
- <input
681
- autoFocus
682
- value={renameValue}
683
- onChange={(e) => setRenameValue(e.target.value)}
684
- onKeyDown={async (e) => {
685
- if (e.key === "Enter") {
686
- e.preventDefault();
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
- } else if (e.key === "Escape") {
696
- setRenamingId(null);
697
- }
698
- }}
699
- 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"
700
- />
701
- <button
702
- onClick={async () => {
703
- if (renameValue.trim()) {
704
- await renameContextFile(
705
- currentQuestion.id,
706
- cf.id,
707
- renameValue.trim(),
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
- setRenamingId(null);
711
- }}
712
- className="shrink-0 p-0.5 rounded text-violet-400 hover:bg-violet-600/20 transition-colors"
713
- title="Confirm"
714
- >
715
- <Check className="w-3 h-3" />
716
- </button>
717
- <button
718
- onClick={() => setRenamingId(null)}
719
- className="shrink-0 p-0.5 rounded text-slate-500 hover:text-slate-300 transition-colors"
720
- title="Cancel"
721
- >
722
- <X className="w-3 h-3" />
723
- </button>
724
- </>
725
- ) : (
726
- <>
727
- <button
728
- onClick={() => {
729
- setRenamingId(cf.id);
730
- setRenameValue(cf.label || cf.originalName);
731
- }}
732
- className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-300 transition-all"
733
- title="Rename"
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
- <Pencil className="w-3 h-3" />
736
- </button>
737
- <button
738
- onClick={async () => {
739
- try {
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 = JSON.parse(raw) as {
746
- serverCode: string;
747
- serverLang: string;
748
- clientCode: string;
749
- clientLang: string;
750
- clientType?: "script" | "react" | "nextjs";
751
- reactFiles?: Record<string, string>;
752
- reactActiveFile?: string;
753
- };
754
- openSandbox(
755
- parsed.serverCode,
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
- className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
773
- title="Open in Sandbox"
774
- >
775
- <Play className="w-3 h-3" />
776
- </button>
777
- <button
778
- onClick={() =>
779
- removeQuestionFile(currentQuestion.id, cf.id)
780
- }
781
- className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
782
- title="Remove"
783
- >
784
- <Trash2 className="w-3 h-3" />
785
- </button>
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
- </div>
799
- )}
938
+ )}
800
939
 
801
- {currentQuestion && (
802
- <div className="border-t border-slate-800 px-3 py-2">
803
- <div className="flex items-center justify-between mb-1">
804
- <div className="flex items-center gap-1">
805
- <Globe className="w-3 h-3 text-cyan-400/70" />
806
- <span className="text-[10px] uppercase tracking-wider text-slate-600">
807
- Infra Labs (
808
- {
809
- (currentQuestion.contextFiles || []).filter(
810
- (f) => f.origin === "infra",
811
- ).length
812
- }
813
- )
814
- </span>
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
- <button
817
- onClick={() => openInfraLab()}
818
- className="p-0.5 rounded text-slate-600 hover:text-cyan-400 hover:bg-slate-700 transition-colors"
819
- title="Open Infrastructure Lab"
820
- >
821
- <Plus className="w-3.5 h-3.5" />
822
- </button>
823
- </div>
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
- {renamingId === cf.id ? (
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
- const parsed = parseInfraLabWorkspace(raw);
907
- if (parsed) openInfraLab(parsed, cf.id);
908
- }}
909
- className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
910
- title="Open in Infrastructure Lab"
911
- >
912
- <Play className="w-3 h-3" />
913
- </button>
914
- <button
915
- onClick={() =>
916
- removeQuestionFile(currentQuestion.id, cf.id)
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
- className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
919
- title="Remove"
920
- >
921
- <Trash2 className="w-3 h-3" />
922
- </button>
923
- </>
924
- )}
925
- </div>
926
- ))}
927
- {(currentQuestion.contextFiles || []).filter(
928
- (f) => f.origin === "infra",
929
- ).length === 0 && (
930
- <p className="text-[10px] text-slate-700 italic">
931
- Save an infra lab to reopen it here
932
- </p>
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
- </div>
936
- )}
1044
+ )}
937
1045
 
938
- {/* ── React Labs section ───────────────────── */}
939
- {currentQuestion && (
940
- <div className="border-t border-slate-800 px-3 py-2">
941
- <div className="flex items-center justify-between mb-1">
942
- <div className="flex items-center gap-1">
943
- <Atom className="w-3 h-3 text-cyan-400/70" />
944
- <span className="text-[10px] uppercase tracking-wider text-slate-600">
945
- React Labs (
946
- {
947
- (currentQuestion.contextFiles || []).filter(
948
- (f) => f.origin === "react",
949
- ).length
950
- }
951
- )
952
- </span>
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
- <button
955
- onClick={() => openReactLab()}
956
- className="p-0.5 rounded text-slate-600 hover:text-cyan-400 hover:bg-slate-700 transition-colors"
957
- title="Open React Lab"
958
- >
959
- <Plus className="w-3.5 h-3.5" />
960
- </button>
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
- {cf.label || cf.originalName}
975
- </span>
976
- <button
977
- onClick={async () => {
978
- try {
979
- const raw = await fetch(
980
- `/api/context-files/${cf.id}/content`,
981
- )
982
- .then((r) => r.json())
983
- .then((d) => d.content as string);
984
- // Try new extended sandbox format first
985
- const ext = JSON.parse(raw) as {
986
- clientType?: string;
987
- reactFiles?: Record<string, string>;
988
- reactActiveFile?: string;
989
- serverCode?: string;
990
- serverLang?: string;
991
- };
992
- if (ext?.clientType === "react" && ext.reactFiles) {
993
- openReactLab(
994
- {
995
- version: 1,
996
- type: "react",
997
- label: cf.label || "React Lab",
998
- activeFile:
999
- ext.reactActiveFile ??
1000
- Object.keys(ext.reactFiles)[0] ??
1001
- "App.tsx",
1002
- files: ext.reactFiles,
1003
- },
1004
- cf.id,
1005
- ext.serverCode,
1006
- ext.serverLang,
1007
- );
1008
- } else {
1009
- // Fall back to old FrontendLabWorkspace format
1010
- const ws = parseFrontendLabWorkspace(raw);
1011
- if (ws) openReactLab(ws, cf.id);
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
- } catch {
1014
- /* ignore */
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
- className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
1018
- title="Open in React Lab"
1019
- >
1020
- <Play className="w-3 h-3" />
1021
- </button>
1022
- <button
1023
- onClick={() =>
1024
- removeQuestionFile(currentQuestion.id, cf.id)
1025
- }
1026
- className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
1027
- title="Remove"
1028
- >
1029
- <Trash2 className="w-3 h-3" />
1030
- </button>
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
- </div>
1042
- )}
1148
+ )}
1043
1149
 
1044
- {/* ── Next.js Labs section ──────────────────── */}
1045
- {currentQuestion && (
1046
- <div className="border-t border-slate-800 px-3 py-2">
1047
- <div className="flex items-center justify-between mb-1">
1048
- <div className="flex items-center gap-1">
1049
- <Layout className="w-3 h-3 text-violet-400/70" />
1050
- <span className="text-[10px] uppercase tracking-wider text-slate-600">
1051
- Next.js Labs (
1052
- {
1053
- (currentQuestion.contextFiles || []).filter(
1054
- (f) => f.origin === "nextjs",
1055
- ).length
1056
- }
1057
- )
1058
- </span>
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
- <button
1061
- onClick={() => openNextLab()}
1062
- className="p-0.5 rounded text-slate-600 hover:text-violet-400 hover:bg-slate-700 transition-colors"
1063
- title="Open Next.js Lab"
1064
- >
1065
- <Plus className="w-3.5 h-3.5" />
1066
- </button>
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
- {cf.label || cf.originalName}
1081
- </span>
1082
- <button
1083
- onClick={async () => {
1084
- try {
1085
- const raw = await fetch(
1086
- `/api/context-files/${cf.id}/content`,
1087
- )
1088
- .then((r) => r.json())
1089
- .then((d) => d.content as string);
1090
- const ext = JSON.parse(raw) as {
1091
- clientType?: string;
1092
- reactFiles?: Record<string, string>;
1093
- reactActiveFile?: string;
1094
- serverCode?: string;
1095
- serverLang?: string;
1096
- };
1097
- if (ext?.clientType === "nextjs" && ext.reactFiles) {
1098
- openNextLab(
1099
- {
1100
- version: 1,
1101
- type: "nextjs",
1102
- label: cf.label || "Next.js Lab",
1103
- activeFile:
1104
- ext.reactActiveFile ??
1105
- Object.keys(ext.reactFiles)[0] ??
1106
- "app/page.tsx",
1107
- files: ext.reactFiles,
1108
- },
1109
- cf.id,
1110
- ext.serverCode,
1111
- ext.serverLang,
1112
- );
1113
- } else {
1114
- const ws = parseFrontendLabWorkspace(raw);
1115
- if (ws) openNextLab(ws, cf.id);
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
- } catch {
1118
- /* ignore */
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
- className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-violet-400 transition-all"
1122
- title="Open in Next.js Lab"
1123
- >
1124
- <Play className="w-3 h-3" />
1125
- </button>
1126
- <button
1127
- onClick={() =>
1128
- removeQuestionFile(currentQuestion.id, cf.id)
1129
- }
1130
- className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all"
1131
- title="Remove"
1132
- >
1133
- <Trash2 className="w-3 h-3" />
1134
- </button>
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
- </div>
1146
- )}
1258
+ )}
1147
1259
 
1148
- {/* ── Webpack Module Federation Labs section ───────────── */}
1149
- {currentQuestion && (
1260
+ {/* ── Notes section ────────────────────────────────────── */}
1150
1261
  <div className="border-t border-slate-800 px-3 py-2">
1151
- <div className="flex items-center justify-between mb-1">
1152
- <div className="flex items-center gap-1">
1153
- <Server className="w-3 h-3 text-emerald-400/70" />
1154
- <span className="text-[10px] uppercase tracking-wider text-slate-600">
1155
- Webpack MF Labs (
1156
- {
1157
- (currentQuestion.contextFiles || []).filter(
1158
- (f) => f.origin === "module-federation",
1159
- ).length
1160
- }
1161
- )
1162
- </span>
1163
- </div>
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
- </div>
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 && (