ar9avg commited on
Commit
cc67cd2
·
1 Parent(s): 2d33bcd

fix: light mode contrast, collapse queries on demo completion

Browse files
frontend/src/components/DemoMode.tsx CHANGED
@@ -176,9 +176,9 @@ function GithubDiff({ fromIdx, toIdx }: { fromIdx: number; toIdx: number }) {
176
  const removed = lines.filter((l) => l.type === 'remove').length
177
 
178
  return (
179
- <div className="rounded-xl border border-white/[0.06] overflow-hidden text-[11px] font-mono">
180
  {/* Header */}
181
- <div className="flex items-center gap-2 px-3 py-2 bg-white/[0.03] border-b border-white/[0.05]">
182
  <GitCommitHorizontal size={11} className="text-gray-500" />
183
  <span className="text-gray-400 font-semibold">system_prompt.txt</span>
184
  <span className="ml-auto flex items-center gap-2 text-[10px]">
@@ -187,7 +187,7 @@ function GithubDiff({ fromIdx, toIdx }: { fromIdx: number; toIdx: number }) {
187
  </span>
188
  </div>
189
  {/* Diff lines */}
190
- <div className="max-h-52 overflow-y-auto">
191
  {lines.map((line, i) => {
192
  const bg = line.type === 'add' ? 'bg-green-500/10' : line.type === 'remove' ? 'bg-red-500/10' : ''
193
  const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' '
@@ -270,14 +270,14 @@ function RightPanel({ gen, score, rewardPoints, latestDiff }: RightPanelProps) {
270
  </div>
271
 
272
  {/* Score bar */}
273
- <div className="bg-white/[0.03] border border-white/[0.06] rounded-xl p-3">
274
  <div className="flex items-end gap-2 mb-2">
275
  <span className="text-2xl font-bold text-green-400 tabular-nums leading-none">
276
  {(score * 100).toFixed(0)}%
277
  </span>
278
  <span className="text-[10px] text-gray-600 mb-0.5">benchmark score</span>
279
  </div>
280
- <div className="h-1.5 bg-white/[0.05] rounded-full overflow-hidden">
281
  <motion.div
282
  className="h-full rounded-full"
283
  style={{ background: 'linear-gradient(90deg,#8b5cf6,#22c55e)' }}
@@ -290,9 +290,9 @@ function RightPanel({ gen, score, rewardPoints, latestDiff }: RightPanelProps) {
290
  {SCORES.slice(0, gen + 1).map((s, i) => (
291
  <div key={i} className="flex items-center gap-2 text-[10px]">
292
  <span className="text-gray-600 w-10 shrink-0">Gen {i}</span>
293
- <div className="flex-1 h-1 bg-white/[0.05] rounded-full overflow-hidden">
294
  <div className="h-full rounded-full transition-all duration-700"
295
- style={{ width: `${s * 100}%`, background: i === gen ? 'linear-gradient(90deg,#8b5cf6,#22c55e)' : '#374151' }} />
296
  </div>
297
  <span className={`font-bold tabular-nums ${i === gen ? 'text-green-400' : 'text-gray-600'}`}>
298
  {(s * 100).toFixed(0)}%
@@ -379,13 +379,13 @@ function Bubble({ b }: { b: BubbleData }) {
379
  )
380
 
381
  if (b.type === 'sql_stream') return (
382
- <div className="border border-white/[0.06] rounded-xl overflow-hidden">
383
- <div className="px-3 py-1.5 bg-white/[0.02] flex items-center gap-2">
384
  <span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
385
  <span className="text-[10px] text-gray-600">attempt {b.attempt}</span>
386
  <Loader2 size={9} className="animate-spin text-violet-400 ml-auto" />
387
  </div>
388
- <pre className="px-3 py-2 text-xs leading-relaxed whitespace-pre-wrap" style={{ color: 'rgba(221,214,254,0.8)', background: 'rgba(139,92,246,0.05)' }}>
389
  <HighlightSQL sql={b.sql} />
390
  <span className="inline-block w-0.5 h-[1em] bg-violet-400 animate-pulse align-bottom ml-0.5" />
391
  </pre>
@@ -394,12 +394,12 @@ function Bubble({ b }: { b: BubbleData }) {
394
 
395
  if (b.type === 'sql_err') return (
396
  <div className="flex flex-col gap-1.5">
397
- <div className="border border-white/[0.06] rounded-xl overflow-hidden">
398
- <div className="px-3 py-1.5 bg-white/[0.02] flex items-center gap-2">
399
  <span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
400
  <span className="text-[10px] text-gray-600">attempt {b.attempt}</span>
401
  </div>
402
- <pre className="px-3 py-2 text-xs leading-relaxed whitespace-pre-wrap" style={{ color: 'rgba(221,214,254,0.8)', background: 'rgba(139,92,246,0.05)' }}>
403
  <HighlightSQL sql={b.sql} />
404
  </pre>
405
  </div>
@@ -417,15 +417,15 @@ function Bubble({ b }: { b: BubbleData }) {
417
 
418
  if (b.type === 'sql_ok') return (
419
  <div className="flex flex-col gap-1.5">
420
- <div className="border border-white/[0.06] rounded-xl overflow-hidden">
421
- <div className="px-3 py-1.5 bg-white/[0.02] flex items-center gap-2">
422
  <span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
423
  {b.firstTry && (
424
  <span className="text-[10px] px-1.5 py-0.5 rounded-full bg-green-500/15 border border-green-500/25 text-green-400 font-semibold">first try ✓</span>
425
  )}
426
  <span className="ml-auto text-[11px] font-bold text-green-400">+{b.reward.toFixed(2)}</span>
427
  </div>
428
- <pre className="px-3 py-2 text-xs leading-relaxed whitespace-pre-wrap" style={{ color: 'rgba(221,214,254,0.8)', background: 'rgba(139,92,246,0.05)' }}>
429
  <HighlightSQL sql={b.sql} />
430
  </pre>
431
  </div>
@@ -434,10 +434,10 @@ function Bubble({ b }: { b: BubbleData }) {
434
  <span className="text-green-400 font-semibold">Success</span>
435
  <span className="text-gray-600">· {b.rows.length}+ rows returned</span>
436
  </div>
437
- <div className="rounded-xl border border-white/[0.06] overflow-hidden text-[10px]">
438
  <table className="w-full">
439
  <thead>
440
- <tr className="bg-white/[0.03]">
441
  {Object.keys(b.rows[0] ?? {}).map((k) => (
442
  <th key={k} className="px-2 py-1.5 text-left font-semibold text-gray-500 whitespace-nowrap">{k}</th>
443
  ))}
@@ -445,9 +445,9 @@ function Bubble({ b }: { b: BubbleData }) {
445
  </thead>
446
  <tbody>
447
  {b.rows.map((row, i) => (
448
- <tr key={i} className={i % 2 === 0 ? 'bg-white/[0.01]' : ''}>
449
  {Object.values(row).map((v, j) => (
450
- <td key={j} className="px-2 py-1 text-gray-300 whitespace-nowrap">{String(v)}</td>
451
  ))}
452
  </tr>
453
  ))}
@@ -483,7 +483,7 @@ function Bubble({ b }: { b: BubbleData }) {
483
  <GitCommitHorizontal size={10} />
484
  Prompt diff (see sidebar for full view)
485
  </div>
486
- <div className="rounded-lg border border-white/[0.05] overflow-hidden text-[10px] font-mono max-h-24 overflow-y-auto">
487
  {diffPrompts(b.fromGen, b.toGen).filter((l) => l.type !== 'same').map((line, i) => (
488
  <div key={i} className={`flex gap-2 px-2 py-0.5 ${line.type === 'add' ? 'bg-green-500/10' : 'bg-red-500/10'}`}>
489
  <span className={`shrink-0 ${line.type === 'add' ? 'text-green-400' : 'text-red-400'}`}>{line.type === 'add' ? '+' : '-'}</span>
@@ -496,22 +496,25 @@ function Bubble({ b }: { b: BubbleData }) {
496
  )
497
 
498
  if (b.type === 'group') return (
499
- <div className="border border-white/[0.06] rounded-2xl overflow-hidden">
500
  <button
501
  onClick={() => setOpen((v) => !v)}
502
- className="w-full flex items-center gap-2 px-3 py-2.5 bg-white/[0.02] hover:bg-white/[0.04] transition-colors text-left"
 
 
 
503
  >
504
  {b.success
505
  ? <CheckCircle2 size={12} className="text-green-400 shrink-0" />
506
  : <XCircle size={12} className="text-red-400 shrink-0" />}
507
- <span className="text-xs text-gray-300 flex-1 truncate">{b.question}</span>
508
  <span className="text-[10px] text-gray-600">{b.attempts} attempt{b.attempts !== 1 ? 's' : ''}</span>
509
  {open ? <ChevronUp size={11} className="text-gray-600 shrink-0" /> : <ChevronDown size={11} className="text-gray-600 shrink-0" />}
510
  </button>
511
  <AnimatePresence>
512
  {open && (
513
  <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.15 }} className="overflow-hidden">
514
- <div className="p-3 flex flex-col gap-2.5 border-t border-white/[0.04]">
515
  {b.children.map((c) => <Bubble key={c.id} b={c} />)}
516
  </div>
517
  </motion.div>
@@ -535,6 +538,9 @@ export function DemoMode({ onClose }: { onClose: () => void }) {
535
  const stepRef = useRef(0)
536
  const cancel = useRef(false)
537
  const bottomRef = useRef<HTMLDivElement>(null)
 
 
 
538
 
539
  const scroll = useCallback(() => {
540
  setTimeout(() => bottomRef.current?.scrollIntoView({ behavior: 'smooth' }), 60)
@@ -603,17 +609,6 @@ export function DemoMode({ onClose }: { onClose: () => void }) {
603
  return children
604
  }, [push, scroll, typeUser, streamSQL, addReward])
605
 
606
- const collapseQuery = useCallback((def: QueryDef, children: BubbleData[]) => {
607
- const lastAtt = def.attempts[def.attempts.length - 1]
608
- setBubbles((prev) => {
609
- const userIdx = [...prev].reverse().findIndex((b) => b.type === 'user' && (b as UserBubble).text === def.question)
610
- if (userIdx < 0) return prev
611
- const fromIdx = prev.length - 1 - userIdx
612
- const group: GroupBubble = { id: uid(), type: 'group', question: def.question, success: !lastAtt.error, attempts: def.attempts.length, children }
613
- return [...prev.slice(0, fromIdx), group]
614
- })
615
- }, [])
616
-
617
  const playGepa = useCallback(async (fromGen: number, toGen: number) => {
618
  const steps = ['Analyzing failure patterns…', 'Identifying missing rules from errors…', 'Rewriting system prompt…', 'Benchmarking candidate prompt…']
619
  for (const label of steps) {
@@ -637,12 +632,16 @@ export function DemoMode({ onClose }: { onClose: () => void }) {
637
  setGen(toGen)
638
  setLatestDiff({ from: fromGen, to: toGen })
639
 
640
- push({ id: uid(), type: 'gepa', fromGen, toGen, scoreFrom: from, scoreTo: to })
 
 
641
  scroll(); await sleep(1000)
642
  }, [push, scroll])
643
 
644
  const autoPlay = useCallback(async () => {
645
  cancel.current = false
 
 
646
  setBubbles([]); setGen(0); setScore(SCORES[0]); setRewardPoints([]); setLatestDiff(null)
647
  stepRef.current = 0; setAppState('running')
648
  await sleep(300)
@@ -650,28 +649,71 @@ export function DemoMode({ onClose }: { onClose: () => void }) {
650
  for (const id of ROUND_1) {
651
  if (cancel.current) break
652
  const children = await playQuery(QUERIES[id])
653
- if (cancel.current) break
654
- await sleep(350); collapseQuery(QUERIES[id], children); await sleep(600)
 
 
655
  }
656
  if (!cancel.current) { await playGepa(0, 1); await sleep(500) }
657
 
658
  for (const id of ROUND_2) {
659
  if (cancel.current) break
660
  const children = await playQuery(QUERIES[id])
661
- if (cancel.current) break
662
- await sleep(350); collapseQuery(QUERIES[id], children); await sleep(600)
 
 
663
  }
664
  if (!cancel.current) { await playGepa(1, 2); await sleep(500) }
665
 
666
  for (const id of ROUND_3) {
667
  if (cancel.current) break
668
  const children = await playQuery(QUERIES[id])
669
- if (cancel.current) break
670
- await sleep(350); collapseQuery(QUERIES[id], children); await sleep(600)
 
 
671
  }
672
 
673
- if (!cancel.current) setAppState('done')
674
- }, [playQuery, collapseQuery, playGepa])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675
 
676
  useEffect(() => () => { cancel.current = true }, [])
677
 
@@ -682,7 +724,7 @@ export function DemoMode({ onClose }: { onClose: () => void }) {
682
  style={{ background: 'var(--bg-primary)' }}
683
  >
684
  {/* Header */}
685
- <div className="shrink-0 flex items-center justify-between px-4 py-3 border-b border-white/[0.06]" style={{ background: 'var(--bg-secondary)' }}>
686
  <div className="flex items-center gap-3">
687
  <div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-violet-500/15 border border-violet-500/25">
688
  <Play size={9} className="text-violet-400" fill="currentColor" />
@@ -756,7 +798,7 @@ export function DemoMode({ onClose }: { onClose: () => void }) {
756
  </div>
757
 
758
  {/* Right panel */}
759
- <aside className="hidden lg:flex flex-col w-72 border-l border-white/[0.06] overflow-hidden shrink-0" style={{ background: 'var(--bg-secondary)' }}>
760
  <RightPanel gen={gen} score={score} rewardPoints={rewardPoints} latestDiff={latestDiff} />
761
  </aside>
762
  </div>
 
176
  const removed = lines.filter((l) => l.type === 'remove').length
177
 
178
  return (
179
+ <div className="rounded-xl border overflow-hidden text-[11px] font-mono" style={{ borderColor: 'var(--border-color)' }}>
180
  {/* Header */}
181
+ <div className="flex items-center gap-2 px-3 py-2 border-b" style={{ background: 'var(--bg-tertiary)', borderColor: 'var(--border-color)' }}>
182
  <GitCommitHorizontal size={11} className="text-gray-500" />
183
  <span className="text-gray-400 font-semibold">system_prompt.txt</span>
184
  <span className="ml-auto flex items-center gap-2 text-[10px]">
 
187
  </span>
188
  </div>
189
  {/* Diff lines */}
190
+ <div className="max-h-52 overflow-y-auto" style={{ background: 'var(--bg-secondary)' }}>
191
  {lines.map((line, i) => {
192
  const bg = line.type === 'add' ? 'bg-green-500/10' : line.type === 'remove' ? 'bg-red-500/10' : ''
193
  const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' '
 
270
  </div>
271
 
272
  {/* Score bar */}
273
+ <div className="border rounded-xl p-3" style={{ background: 'var(--bg-tertiary)', borderColor: 'var(--border-color)' }}>
274
  <div className="flex items-end gap-2 mb-2">
275
  <span className="text-2xl font-bold text-green-400 tabular-nums leading-none">
276
  {(score * 100).toFixed(0)}%
277
  </span>
278
  <span className="text-[10px] text-gray-600 mb-0.5">benchmark score</span>
279
  </div>
280
+ <div className="h-1.5 rounded-full overflow-hidden" style={{ background: 'var(--border-color)' }}>
281
  <motion.div
282
  className="h-full rounded-full"
283
  style={{ background: 'linear-gradient(90deg,#8b5cf6,#22c55e)' }}
 
290
  {SCORES.slice(0, gen + 1).map((s, i) => (
291
  <div key={i} className="flex items-center gap-2 text-[10px]">
292
  <span className="text-gray-600 w-10 shrink-0">Gen {i}</span>
293
+ <div className="flex-1 h-1 rounded-full overflow-hidden" style={{ background: 'var(--border-color)' }}>
294
  <div className="h-full rounded-full transition-all duration-700"
295
+ style={{ width: `${s * 100}%`, background: i === gen ? 'linear-gradient(90deg,#8b5cf6,#22c55e)' : 'var(--text-dim)' }} />
296
  </div>
297
  <span className={`font-bold tabular-nums ${i === gen ? 'text-green-400' : 'text-gray-600'}`}>
298
  {(s * 100).toFixed(0)}%
 
379
  )
380
 
381
  if (b.type === 'sql_stream') return (
382
+ <div className="border rounded-xl overflow-hidden" style={{ borderColor: 'var(--border-color)' }}>
383
+ <div className="px-3 py-1.5 flex items-center gap-2" style={{ background: 'var(--bg-tertiary)' }}>
384
  <span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
385
  <span className="text-[10px] text-gray-600">attempt {b.attempt}</span>
386
  <Loader2 size={9} className="animate-spin text-violet-400 ml-auto" />
387
  </div>
388
+ <pre className="px-3 py-2 text-xs leading-relaxed whitespace-pre-wrap" style={{ background: 'rgba(139,92,246,0.05)' }}>
389
  <HighlightSQL sql={b.sql} />
390
  <span className="inline-block w-0.5 h-[1em] bg-violet-400 animate-pulse align-bottom ml-0.5" />
391
  </pre>
 
394
 
395
  if (b.type === 'sql_err') return (
396
  <div className="flex flex-col gap-1.5">
397
+ <div className="border rounded-xl overflow-hidden" style={{ borderColor: 'var(--border-color)' }}>
398
+ <div className="px-3 py-1.5 flex items-center gap-2" style={{ background: 'var(--bg-tertiary)' }}>
399
  <span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
400
  <span className="text-[10px] text-gray-600">attempt {b.attempt}</span>
401
  </div>
402
+ <pre className="px-3 py-2 text-xs leading-relaxed whitespace-pre-wrap" style={{ background: 'rgba(139,92,246,0.05)' }}>
403
  <HighlightSQL sql={b.sql} />
404
  </pre>
405
  </div>
 
417
 
418
  if (b.type === 'sql_ok') return (
419
  <div className="flex flex-col gap-1.5">
420
+ <div className="border rounded-xl overflow-hidden" style={{ borderColor: 'var(--border-color)' }}>
421
+ <div className="px-3 py-1.5 flex items-center gap-2" style={{ background: 'var(--bg-tertiary)' }}>
422
  <span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
423
  {b.firstTry && (
424
  <span className="text-[10px] px-1.5 py-0.5 rounded-full bg-green-500/15 border border-green-500/25 text-green-400 font-semibold">first try ✓</span>
425
  )}
426
  <span className="ml-auto text-[11px] font-bold text-green-400">+{b.reward.toFixed(2)}</span>
427
  </div>
428
+ <pre className="px-3 py-2 text-xs leading-relaxed whitespace-pre-wrap" style={{ background: 'rgba(139,92,246,0.05)' }}>
429
  <HighlightSQL sql={b.sql} />
430
  </pre>
431
  </div>
 
434
  <span className="text-green-400 font-semibold">Success</span>
435
  <span className="text-gray-600">· {b.rows.length}+ rows returned</span>
436
  </div>
437
+ <div className="rounded-xl border overflow-hidden text-[10px]" style={{ borderColor: 'var(--border-color)' }}>
438
  <table className="w-full">
439
  <thead>
440
+ <tr style={{ background: 'var(--bg-tertiary)' }}>
441
  {Object.keys(b.rows[0] ?? {}).map((k) => (
442
  <th key={k} className="px-2 py-1.5 text-left font-semibold text-gray-500 whitespace-nowrap">{k}</th>
443
  ))}
 
445
  </thead>
446
  <tbody>
447
  {b.rows.map((row, i) => (
448
+ <tr key={i} style={i % 2 === 0 ? { background: 'var(--bg-hover)' } : {}}>
449
  {Object.values(row).map((v, j) => (
450
+ <td key={j} className="px-2 py-1 theme-text-secondary whitespace-nowrap">{String(v)}</td>
451
  ))}
452
  </tr>
453
  ))}
 
483
  <GitCommitHorizontal size={10} />
484
  Prompt diff (see sidebar for full view)
485
  </div>
486
+ <div className="rounded-lg border overflow-hidden text-[10px] font-mono max-h-24 overflow-y-auto" style={{ borderColor: 'var(--border-color)', background: 'var(--bg-secondary)' }}>
487
  {diffPrompts(b.fromGen, b.toGen).filter((l) => l.type !== 'same').map((line, i) => (
488
  <div key={i} className={`flex gap-2 px-2 py-0.5 ${line.type === 'add' ? 'bg-green-500/10' : 'bg-red-500/10'}`}>
489
  <span className={`shrink-0 ${line.type === 'add' ? 'text-green-400' : 'text-red-400'}`}>{line.type === 'add' ? '+' : '-'}</span>
 
496
  )
497
 
498
  if (b.type === 'group') return (
499
+ <div className="border rounded-2xl overflow-hidden" style={{ borderColor: 'var(--border-color)' }}>
500
  <button
501
  onClick={() => setOpen((v) => !v)}
502
+ className="w-full flex items-center gap-2 px-3 py-2.5 transition-colors text-left"
503
+ style={{ background: 'var(--bg-tertiary)' }}
504
+ onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--bg-hover-strong)')}
505
+ onMouseLeave={(e) => (e.currentTarget.style.background = 'var(--bg-tertiary)')}
506
  >
507
  {b.success
508
  ? <CheckCircle2 size={12} className="text-green-400 shrink-0" />
509
  : <XCircle size={12} className="text-red-400 shrink-0" />}
510
+ <span className="text-xs theme-text-secondary flex-1 truncate">{b.question}</span>
511
  <span className="text-[10px] text-gray-600">{b.attempts} attempt{b.attempts !== 1 ? 's' : ''}</span>
512
  {open ? <ChevronUp size={11} className="text-gray-600 shrink-0" /> : <ChevronDown size={11} className="text-gray-600 shrink-0" />}
513
  </button>
514
  <AnimatePresence>
515
  {open && (
516
  <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.15 }} className="overflow-hidden">
517
+ <div className="p-3 flex flex-col gap-2.5 border-t" style={{ borderColor: 'var(--border-color)' }}>
518
  {b.children.map((c) => <Bubble key={c.id} b={c} />)}
519
  </div>
520
  </motion.div>
 
538
  const stepRef = useRef(0)
539
  const cancel = useRef(false)
540
  const bottomRef = useRef<HTMLDivElement>(null)
541
+ // Track all played queries and GEPA bubbles for end-of-demo collapse
542
+ const allQueriesRef = useRef<Array<{ def: QueryDef; children: BubbleData[] }>>([])
543
+ const gepaBubblesRef = useRef<BubbleData[]>([])
544
 
545
  const scroll = useCallback(() => {
546
  setTimeout(() => bottomRef.current?.scrollIntoView({ behavior: 'smooth' }), 60)
 
609
  return children
610
  }, [push, scroll, typeUser, streamSQL, addReward])
611
 
 
 
 
 
 
 
 
 
 
 
 
612
  const playGepa = useCallback(async (fromGen: number, toGen: number) => {
613
  const steps = ['Analyzing failure patterns…', 'Identifying missing rules from errors…', 'Rewriting system prompt…', 'Benchmarking candidate prompt…']
614
  for (const label of steps) {
 
632
  setGen(toGen)
633
  setLatestDiff({ from: fromGen, to: toGen })
634
 
635
+ const gepaBubble: GepaBubble = { id: uid(), type: 'gepa', fromGen, toGen, scoreFrom: from, scoreTo: to }
636
+ gepaBubblesRef.current.push(gepaBubble)
637
+ push(gepaBubble)
638
  scroll(); await sleep(1000)
639
  }, [push, scroll])
640
 
641
  const autoPlay = useCallback(async () => {
642
  cancel.current = false
643
+ allQueriesRef.current = []
644
+ gepaBubblesRef.current = []
645
  setBubbles([]); setGen(0); setScore(SCORES[0]); setRewardPoints([]); setLatestDiff(null)
646
  stepRef.current = 0; setAppState('running')
647
  await sleep(300)
 
649
  for (const id of ROUND_1) {
650
  if (cancel.current) break
651
  const children = await playQuery(QUERIES[id])
652
+ if (!cancel.current) {
653
+ allQueriesRef.current.push({ def: QUERIES[id], children })
654
+ await sleep(600)
655
+ }
656
  }
657
  if (!cancel.current) { await playGepa(0, 1); await sleep(500) }
658
 
659
  for (const id of ROUND_2) {
660
  if (cancel.current) break
661
  const children = await playQuery(QUERIES[id])
662
+ if (!cancel.current) {
663
+ allQueriesRef.current.push({ def: QUERIES[id], children })
664
+ await sleep(600)
665
+ }
666
  }
667
  if (!cancel.current) { await playGepa(1, 2); await sleep(500) }
668
 
669
  for (const id of ROUND_3) {
670
  if (cancel.current) break
671
  const children = await playQuery(QUERIES[id])
672
+ if (!cancel.current) {
673
+ allQueriesRef.current.push({ def: QUERIES[id], children })
674
+ await sleep(600)
675
+ }
676
  }
677
 
678
+ if (!cancel.current) {
679
+ // Collapse all queries at once, rebuilding the bubble list
680
+ const queries = allQueriesRef.current
681
+ const gepas = gepaBubblesRef.current
682
+ const result: BubbleData[] = []
683
+
684
+ // Round 1 groups
685
+ for (let i = 0; i < ROUND_1.length && i < queries.length; i++) {
686
+ const { def, children } = queries[i]
687
+ const lastAtt = def.attempts[def.attempts.length - 1]
688
+ result.push({ id: uid(), type: 'group', question: def.question, success: !lastAtt.error, attempts: def.attempts.length, children })
689
+ }
690
+ // GEPA 0→1
691
+ if (gepas[0]) result.push(gepas[0])
692
+ // Round 2 groups
693
+ for (let i = 0; i < ROUND_2.length; i++) {
694
+ const qi = ROUND_1.length + i
695
+ if (qi >= queries.length) break
696
+ const { def, children } = queries[qi]
697
+ const lastAtt = def.attempts[def.attempts.length - 1]
698
+ result.push({ id: uid(), type: 'group', question: def.question, success: !lastAtt.error, attempts: def.attempts.length, children })
699
+ }
700
+ // GEPA 1→2
701
+ if (gepas[1]) result.push(gepas[1])
702
+ // Round 3 groups
703
+ for (let i = 0; i < ROUND_3.length; i++) {
704
+ const qi = ROUND_1.length + ROUND_2.length + i
705
+ if (qi >= queries.length) break
706
+ const { def, children } = queries[qi]
707
+ const lastAtt = def.attempts[def.attempts.length - 1]
708
+ result.push({ id: uid(), type: 'group', question: def.question, success: !lastAtt.error, attempts: def.attempts.length, children })
709
+ }
710
+
711
+ setBubbles(result)
712
+ await sleep(300)
713
+ scroll()
714
+ setAppState('done')
715
+ }
716
+ }, [playQuery, playGepa, scroll])
717
 
718
  useEffect(() => () => { cancel.current = true }, [])
719
 
 
724
  style={{ background: 'var(--bg-primary)' }}
725
  >
726
  {/* Header */}
727
+ <div className="shrink-0 flex items-center justify-between px-4 py-3 border-b theme-border" style={{ background: 'var(--bg-secondary)' }}>
728
  <div className="flex items-center gap-3">
729
  <div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-violet-500/15 border border-violet-500/25">
730
  <Play size={9} className="text-violet-400" fill="currentColor" />
 
798
  </div>
799
 
800
  {/* Right panel */}
801
+ <aside className="hidden lg:flex flex-col w-72 border-l theme-border overflow-hidden shrink-0" style={{ background: 'var(--bg-secondary)' }}>
802
  <RightPanel gen={gen} score={score} rewardPoints={rewardPoints} latestDiff={latestDiff} />
803
  </aside>
804
  </div>
frontend/src/index.css CHANGED
@@ -177,7 +177,10 @@ body {
177
  [data-theme="light"] .text-violet-300 { color: #7c3aed !important; }
178
  [data-theme="light"] .text-violet-400 { color: #7c3aed !important; }
179
  [data-theme="light"] .text-green-400 { color: #15803d !important; }
 
180
  [data-theme="light"] .text-red-400 { color: #b91c1c !important; }
 
 
181
  [data-theme="light"] pre {
182
  background-color: var(--bg-tertiary) !important;
183
  color: #374151 !important;
@@ -185,3 +188,15 @@ body {
185
  [data-theme="light"] .recharts-cartesian-grid line {
186
  stroke: rgba(0, 0, 0, 0.06) !important;
187
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  [data-theme="light"] .text-violet-300 { color: #7c3aed !important; }
178
  [data-theme="light"] .text-violet-400 { color: #7c3aed !important; }
179
  [data-theme="light"] .text-green-400 { color: #15803d !important; }
180
+ [data-theme="light"] .text-green-300 { color: #16a34a !important; }
181
  [data-theme="light"] .text-red-400 { color: #b91c1c !important; }
182
+ [data-theme="light"] .text-red-300 { color: #dc2626 !important; }
183
+ [data-theme="light"] .text-orange-400 { color: #c2410c !important; }
184
  [data-theme="light"] pre {
185
  background-color: var(--bg-tertiary) !important;
186
  color: #374151 !important;
 
188
  [data-theme="light"] .recharts-cartesian-grid line {
189
  stroke: rgba(0, 0, 0, 0.06) !important;
190
  }
191
+ /* Make borders using white opacity visible in light mode */
192
+ [data-theme="light"] [class*="border-white/"] {
193
+ border-color: var(--border-color) !important;
194
+ }
195
+ /* Make subtle white-opacity section headers visible in light mode */
196
+ [data-theme="light"] [class*="bg-white/[0.0"] {
197
+ background-color: rgba(0, 0, 0, 0.035) !important;
198
+ }
199
+ /* Hover states on white-opacity backgrounds */
200
+ [data-theme="light"] [class*="hover:bg-white/"]:hover {
201
+ background-color: var(--bg-hover-strong) !important;
202
+ }