ar9avg's picture
fix
b00a200
/**
* DemoMode β€” scripted autoplay showcase for SQL Agent OpenEnv.
*
* Right panel: live reward chart + GitHub-style prompt diff.
* Chat: single-difficulty rounds separated by GEPA evolution.
* No looping β€” scrollable at the end.
*/
import { useState, useRef, useCallback, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import {
LineChart, Line, XAxis, YAxis, CartesianGrid,
Tooltip, ResponsiveContainer, ReferenceLine,
} from 'recharts'
import {
Play, X, Zap, CheckCircle2, XCircle,
ChevronDown, ChevronUp, Sparkles, Loader2, GitCommitHorizontal,
} from 'lucide-react'
// ─── Prompts (3 generations) ─────────────────────────────────────────────────
const PROMPTS = [
`You are a SQL expert. Given a question and a SQLite schema, write correct SQL.
Rules:
- Output ONLY the SQL query
- Use SQLite syntax
- No markdown, no code fences`,
`You are a SQL expert. Given a question and a SQLite schema, write correct SQL.
Rules:
- Output ONLY the SQL query
- Use SQLite syntax
- No markdown, no code fences
- Always qualify column names with table aliases in JOINs
- Use t.column_name to avoid ambiguous column errors
- Verify every column name against the schema before use`,
`You are a SQL expert. Given a question and a SQLite schema, write correct SQL.
Rules:
- Output ONLY the SQL query
- Use SQLite syntax
- No markdown, no code fences
- Always qualify column names with table aliases in JOINs
- Use t.column_name to avoid ambiguous column errors
- Verify every column name against the schema before use
- For aggregations: include all non-aggregated columns in GROUP BY
- For revenue: use orders.total_price (not price or amount)
- For top-N: use ORDER BY … DESC LIMIT N`,
]
const SCORES = [0.42, 0.74, 0.91]
// ─── Scripted query data ─────────────────────────────────────────────────────
interface Attempt {
sql: string
error?: string
errorClass?: string
rlAction?: string
reward: number
rows?: Record<string, string | number>[]
}
interface QueryDef {
id: string
question: string
attempts: Attempt[]
}
const QUERIES: Record<string, QueryDef> = {
// Round 1 β€” simple queries, some fail
r1q1: {
id: 'r1q1',
question: 'Show all products',
attempts: [
{ sql: 'SELECT * FROM product', error: 'no such table: product', errorClass: 'NO_SUCH_TABLE', rlAction: 'FIX_TABLE', reward: -0.15 },
{ sql: 'SELECT * FROM products LIMIT 10', reward: 0.90,
rows: [{ id: 1, name: 'Wireless Headphones', category: 'Electronics', price: 79.99 }, { id: 2, name: 'Running Shoes', category: 'Footwear', price: 59.99 }, { id: 3, name: 'Coffee Maker', category: 'Kitchen', price: 49.99 }] },
],
},
r1q2: {
id: 'r1q2',
question: 'List all users from the USA',
attempts: [
{ sql: "SELECT * FROM users WHERE country = 'USA'", reward: 1.00,
rows: [{ id: 1, name: 'Alice Johnson', email: 'alice@example.com', country: 'USA' }, { id: 2, name: 'Bob Smith', email: 'bob@example.com', country: 'USA' }] },
],
},
// Round 2 β€” join queries, ambiguous columns fixed by GEPA
r2q1: {
id: 'r2q1',
question: 'Top 5 sellers by total revenue',
attempts: [
{ sql: `SELECT seller_id, SUM(total_price) as revenue\nFROM orders\nGROUP BY seller_id\nORDER BY revenue DESC\nLIMIT 5`, error: 'ambiguous column name: seller_id', errorClass: 'AMBIGUOUS_COLUMN', rlAction: 'FIX_COLUMN', reward: -0.20 },
{ sql: `SELECT s.name, SUM(o.total_price) as revenue\nFROM orders o\nJOIN sellers s ON o.user_id = s.id\nGROUP BY s.id, s.name\nORDER BY revenue DESC\nLIMIT 5`, reward: 0.80,
rows: [{ seller: 'TechStore Pro', revenue: 12840 }, { seller: 'StyleHub', revenue: 9320 }, { seller: 'GadgetWorld', revenue: 8750 }] },
],
},
r2q2: {
id: 'r2q2',
question: 'Average order value by country',
attempts: [
{ sql: `SELECT country, AVG(total_price) as avg_order\nFROM orders\nJOIN users ON user_id = users.id\nGROUP BY country`, error: 'ambiguous column name: country', errorClass: 'AMBIGUOUS_COLUMN', rlAction: 'FIX_COLUMN', reward: -0.15 },
{ sql: `SELECT u.country, ROUND(AVG(o.total_price), 2) as avg_order\nFROM orders o\nJOIN users u ON o.user_id = u.id\nGROUP BY u.country\nORDER BY avg_order DESC`, reward: 0.85,
rows: [{ country: 'USA', avg_order: 142.50 }, { country: 'UK', avg_order: 138.20 }, { country: 'Germany', avg_order: 121.80 }] },
],
},
// Round 3 β€” after GEPA 2, same joins succeed first try
r3q1: {
id: 'r3q1',
question: 'Top 5 sellers by total revenue',
attempts: [
{ sql: `SELECT s.name, SUM(o.total_price) as revenue\nFROM orders o\nJOIN sellers s ON o.user_id = s.id\nGROUP BY s.id, s.name\nORDER BY revenue DESC\nLIMIT 5`, reward: 1.00,
rows: [{ seller: 'TechStore Pro', revenue: 12840 }, { seller: 'StyleHub', revenue: 9320 }, { seller: 'GadgetWorld', revenue: 8750 }] },
],
},
r3q2: {
id: 'r3q2',
question: 'Monthly revenue for the last 6 months',
attempts: [
{ sql: `SELECT strftime('%Y-%m', created_at) as month,\n ROUND(SUM(total_price), 2) as revenue\nFROM orders\nGROUP BY month\nORDER BY month DESC\nLIMIT 6`, reward: 1.00,
rows: [{ month: '2024-11', revenue: 24180 }, { month: '2024-10', revenue: 21340 }, { month: '2024-09', revenue: 19800 }] },
],
},
}
const ROUND_1 = ['r1q1', 'r1q2']
const ROUND_2 = ['r2q1', 'r2q2']
const ROUND_3 = ['r3q1', 'r3q2']
// ─── GitHub-style diff ───────────────────────────────────────────────────────
interface DiffLine { type: 'add' | 'remove' | 'same'; text: string }
function diffPrompts(fromIdx: number, toIdx: number): DiffLine[] {
const oldLines = PROMPTS[fromIdx].split('\n')
const newLines = PROMPTS[toIdx].split('\n')
const result: DiffLine[] = []
// Simple patience diff: lines in both = same, only in old = remove, only in new = add
const oldSet = new Set(oldLines)
const newSet = new Set(newLines)
// Walk new lines, carrying old-only lines before matching
let oi = 0
for (let ni = 0; ni < newLines.length; ni++) {
const line = newLines[ni]
if (oldSet.has(line)) {
// Flush any old-only lines before this match
while (oi < oldLines.length && oldLines[oi] !== line) {
if (!newSet.has(oldLines[oi])) result.push({ type: 'remove', text: oldLines[oi] })
oi++
}
result.push({ type: 'same', text: line })
oi++
} else {
result.push({ type: 'add', text: line })
}
}
// Remaining old-only
while (oi < oldLines.length) {
if (!newSet.has(oldLines[oi])) result.push({ type: 'remove', text: oldLines[oi] })
oi++
}
return result
}
function GithubDiff({ fromIdx, toIdx }: { fromIdx: number; toIdx: number }) {
const lines = diffPrompts(fromIdx, toIdx)
const added = lines.filter((l) => l.type === 'add').length
const removed = lines.filter((l) => l.type === 'remove').length
return (
<div className="rounded-xl border overflow-hidden text-[11px] font-mono" style={{ borderColor: 'var(--border-color)' }}>
{/* Header */}
<div className="flex items-center gap-2 px-3 py-2 border-b" style={{ background: 'var(--bg-tertiary)', borderColor: 'var(--border-color)' }}>
<GitCommitHorizontal size={11} className="text-gray-500" />
<span className="text-gray-400 font-semibold">system_prompt.txt</span>
<span className="ml-auto flex items-center gap-2 text-[10px]">
<span className="text-green-400 font-bold">+{added}</span>
<span className="text-red-400 font-bold">βˆ’{removed}</span>
</span>
</div>
{/* Diff lines */}
<div className="max-h-52 overflow-y-auto" style={{ background: 'var(--bg-secondary)' }}>
{lines.map((line, i) => {
const bg = line.type === 'add' ? 'bg-green-500/10' : line.type === 'remove' ? 'bg-red-500/10' : ''
const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' '
const color = line.type === 'add' ? 'text-green-300' : line.type === 'remove' ? 'text-red-300' : 'text-gray-500'
return (
<div key={i} className={`flex items-start gap-2 px-3 py-0.5 leading-relaxed ${bg}`}>
<span className={`shrink-0 w-3 ${color} font-bold select-none`}>{prefix}</span>
<span className={color}>{line.text || <span className="opacity-0">β€”</span>}</span>
</div>
)
})}
</div>
</div>
)
}
// ─── Reward chart ────────────────────────────────────────────────────────────
interface RewardPoint { step: number; reward: number }
function RewardChart({ points }: { points: RewardPoint[] }) {
if (points.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-28 gap-1.5 text-gray-700">
<span className="text-[11px]">Rewards appear as agent runs</span>
</div>
)
}
return (
<ResponsiveContainer width="100%" height={110}>
<LineChart data={points} margin={{ top: 4, right: 8, bottom: 0, left: -20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" />
<XAxis dataKey="step" tick={{ fontSize: 9, fill: '#6b7280' }} />
<YAxis domain={[-0.5, 1.1]} tick={{ fontSize: 9, fill: '#6b7280' }} />
<Tooltip
contentStyle={{ background: '#1a1a2e', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, fontSize: 11 }}
labelStyle={{ color: '#9ca3af' }}
itemStyle={{ color: '#f97316' }}
formatter={(v: number) => [v.toFixed(2), 'reward']}
labelFormatter={(l) => `Step ${l}`}
/>
<ReferenceLine y={0} stroke="#ffffff18" strokeDasharray="3 3" />
<Line
type="monotone" dataKey="reward" name="Reward"
stroke="#f97316" strokeWidth={2}
dot={(props) => {
const { cx, cy, payload } = props
const color = (payload as RewardPoint).reward >= 0 ? '#22c55e' : '#ef4444'
return <circle key={`dot-${(payload as RewardPoint).step}`} cx={cx} cy={cy} r={3} fill={color} stroke="none" />
}}
activeDot={{ r: 4, fill: '#f97316' }}
isAnimationActive={false}
/>
</LineChart>
</ResponsiveContainer>
)
}
// ─── Right panel ─────────────────────────────────────────────────────────────
interface RightPanelProps {
gen: number
score: number
rewardPoints: RewardPoint[]
latestDiff: { from: number; to: number } | null
}
function RightPanel({ gen, score, rewardPoints, latestDiff }: RightPanelProps) {
return (
<div className="flex flex-col gap-4 px-4 py-4 overflow-y-auto h-full">
{/* Gen badge + score */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<Sparkles size={12} className="text-violet-400" />
<span className="text-[11px] font-semibold text-gray-400 uppercase tracking-wider">GEPA</span>
</div>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-violet-500/15 border border-violet-500/25 text-violet-400 font-semibold">
Gen {gen}
</span>
</div>
{/* Score bar */}
<div className="border rounded-xl p-3" style={{ background: 'var(--bg-tertiary)', borderColor: 'var(--border-color)' }}>
<div className="flex items-end gap-2 mb-2">
<span className="text-2xl font-bold text-green-400 tabular-nums leading-none">
{(score * 100).toFixed(0)}%
</span>
<span className="text-[10px] text-gray-600 mb-0.5">benchmark score</span>
</div>
<div className="h-1.5 rounded-full overflow-hidden" style={{ background: 'var(--border-color)' }}>
<motion.div
className="h-full rounded-full"
style={{ background: 'linear-gradient(90deg,#8b5cf6,#22c55e)' }}
animate={{ width: `${score * 100}%` }}
transition={{ duration: 0.9, ease: 'easeOut' }}
/>
</div>
{/* Generation history */}
<div className="mt-2.5 flex flex-col gap-1">
{SCORES.slice(0, gen + 1).map((s, i) => (
<div key={i} className="flex items-center gap-2 text-[10px]">
<span className="text-gray-600 w-10 shrink-0">Gen {i}</span>
<div className="flex-1 h-1 rounded-full overflow-hidden" style={{ background: 'var(--border-color)' }}>
<div className="h-full rounded-full transition-all duration-700"
style={{ width: `${s * 100}%`, background: i === gen ? 'linear-gradient(90deg,#8b5cf6,#22c55e)' : 'var(--text-dim)' }} />
</div>
<span className={`font-bold tabular-nums ${i === gen ? 'text-green-400' : 'text-gray-600'}`}>
{(s * 100).toFixed(0)}%
</span>
</div>
))}
</div>
</div>
{/* Reward chart */}
<div>
<div className="text-[10px] text-gray-600 font-semibold uppercase tracking-wider mb-2">
RL Reward per Step
</div>
<RewardChart points={rewardPoints} />
</div>
{/* GitHub diff β€” appears after each GEPA cycle */}
{latestDiff && (
<div>
<div className="flex items-center gap-1.5 mb-2">
<GitCommitHorizontal size={11} className="text-violet-400" />
<span className="text-[10px] text-gray-500 font-semibold uppercase tracking-wider">
Prompt diff β€” Gen {latestDiff.from} β†’ {latestDiff.to}
</span>
</div>
<GithubDiff fromIdx={latestDiff.from} toIdx={latestDiff.to} />
</div>
)}
</div>
)
}
// ─── SQL highlighter ─────────────────────────────────────────────────────────
function HighlightSQL({ sql }: { sql: string }) {
const re = /\b(SELECT|FROM|WHERE|JOIN|LEFT|RIGHT|ON|GROUP BY|ORDER BY|HAVING|LIMIT|AS|AND|OR|NOT|IN|IS|NULL|SUM|AVG|COUNT|ROUND|DISTINCT|DESC|ASC|strftime|JULIANDAY)\b/gi
const parts: React.ReactNode[] = []
let last = 0; let match: RegExpExecArray | null
const r = new RegExp(re.source, 'gi')
while ((match = r.exec(sql)) !== null) {
if (match.index > last) parts.push(<span key={`t${last}`}>{sql.slice(last, match.index)}</span>)
parts.push(<span key={`k${match.index}`} className="text-violet-300 font-semibold">{match[0]}</span>)
last = match.index + match[0].length
}
if (last < sql.length) parts.push(<span key="end">{sql.slice(last)}</span>)
return <>{parts}</>
}
// ─── Bubble types ─────────────────────────────────────────────────────────────
interface BaseBubble { id: string }
interface UserBubble extends BaseBubble { type: 'user'; text: string }
interface ThinkingBubble extends BaseBubble { type: 'thinking'; label: string }
interface SqlStreamBubble extends BaseBubble { type: 'sql_stream'; sql: string; attempt: number }
interface SqlErrBubble extends BaseBubble { type: 'sql_err'; sql: string; error: string; errorClass: string; rlAction: string; reward: number; attempt: number }
interface SqlOkBubble extends BaseBubble { type: 'sql_ok'; sql: string; rows: Record<string, string | number>[]; reward: number; attempt: number; firstTry: boolean }
interface GepaBubble extends BaseBubble { type: 'gepa'; fromGen: number; toGen: number; scoreFrom: number; scoreTo: number }
interface GroupBubble extends BaseBubble { type: 'group'; question: string; success: boolean; attempts: number; children: BubbleData[] }
type BubbleData = UserBubble | ThinkingBubble | SqlStreamBubble | SqlErrBubble | SqlOkBubble | GepaBubble | GroupBubble
let _id = 0
const uid = () => `b${++_id}`
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms))
// ─── Bubble renderer ─────────────────────────────────────────────────────────
function Bubble({ b }: { b: BubbleData }) {
const [open, setOpen] = useState(false)
if (b.type === 'user') return (
<div className="flex justify-end">
<div className="max-w-[78%] bg-violet-600/20 border border-violet-500/25 rounded-2xl rounded-tr-sm px-4 py-2.5">
<p className="text-sm text-white">{b.text}</p>
</div>
</div>
)
if (b.type === 'thinking') return (
<div className="flex items-center gap-2 px-1 text-xs text-gray-500">
<Loader2 size={11} className="animate-spin text-violet-400 shrink-0" />
{b.label}
</div>
)
if (b.type === 'sql_stream') return (
<div className="border rounded-xl overflow-hidden" style={{ borderColor: 'var(--border-color)' }}>
<div className="px-3 py-1.5 flex items-center gap-2" style={{ background: 'var(--bg-tertiary)' }}>
<span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
<span className="text-[10px] text-gray-600">attempt {b.attempt}</span>
<Loader2 size={9} className="animate-spin text-violet-400 ml-auto" />
</div>
<pre className="px-3 py-2 text-xs leading-relaxed whitespace-pre-wrap" style={{ background: 'rgba(139,92,246,0.05)' }}>
<HighlightSQL sql={b.sql} />
<span className="inline-block w-0.5 h-[1em] bg-violet-400 animate-pulse align-bottom ml-0.5" />
</pre>
</div>
)
if (b.type === 'sql_err') return (
<div className="flex flex-col gap-1.5">
<div className="border rounded-xl overflow-hidden" style={{ borderColor: 'var(--border-color)' }}>
<div className="px-3 py-1.5 flex items-center gap-2" style={{ background: 'var(--bg-tertiary)' }}>
<span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
<span className="text-[10px] text-gray-600">attempt {b.attempt}</span>
</div>
<pre className="px-3 py-2 text-xs leading-relaxed whitespace-pre-wrap" style={{ background: 'rgba(139,92,246,0.05)' }}>
<HighlightSQL sql={b.sql} />
</pre>
</div>
<div className="flex items-center gap-2 flex-wrap bg-red-500/8 border border-red-500/20 rounded-xl px-3 py-2">
<XCircle size={11} className="text-red-400 shrink-0" />
<span className="text-xs text-red-300 flex-1">{b.error}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-red-500/15 text-red-400 border border-red-500/20">{b.errorClass}</span>
<span className="flex items-center gap-1 text-[10px] font-semibold px-2 py-0.5 rounded-full border border-orange-500/30 bg-orange-500/10 text-orange-400">
<Zap size={8} />{b.rlAction}
</span>
<span className="text-[11px] font-bold text-red-400">{b.reward.toFixed(2)}</span>
</div>
</div>
)
if (b.type === 'sql_ok') return (
<div className="flex flex-col gap-1.5">
<div className="border rounded-xl overflow-hidden" style={{ borderColor: 'var(--border-color)' }}>
<div className="px-3 py-1.5 flex items-center gap-2" style={{ background: 'var(--bg-tertiary)' }}>
<span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
{b.firstTry && (
<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>
)}
<span className="ml-auto text-[11px] font-bold text-green-400">+{b.reward.toFixed(2)}</span>
</div>
<pre className="px-3 py-2 text-xs leading-relaxed whitespace-pre-wrap" style={{ background: 'rgba(139,92,246,0.05)' }}>
<HighlightSQL sql={b.sql} />
</pre>
</div>
<div className="flex items-center gap-1.5 text-[10px] px-0.5">
<CheckCircle2 size={11} className="text-green-400" />
<span className="text-green-400 font-semibold">Success</span>
<span className="text-gray-600">Β· {b.rows.length}+ rows returned</span>
</div>
<div className="rounded-xl border overflow-hidden text-[10px]" style={{ borderColor: 'var(--border-color)' }}>
<table className="w-full">
<thead>
<tr style={{ background: 'var(--bg-tertiary)' }}>
{Object.keys(b.rows[0] ?? {}).map((k) => (
<th key={k} className="px-2 py-1.5 text-left font-semibold text-gray-500 whitespace-nowrap">{k}</th>
))}
</tr>
</thead>
<tbody>
{b.rows.map((row, i) => (
<tr key={i} style={i % 2 === 0 ? { background: 'var(--bg-hover)' } : {}}>
{Object.values(row).map((v, j) => (
<td key={j} className="px-2 py-1 theme-text-secondary whitespace-nowrap">{String(v)}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
if (b.type === 'gepa') return (
<div className="border border-violet-500/25 rounded-2xl overflow-hidden bg-violet-500/5">
<div className="px-4 py-3 flex items-center gap-2 border-b border-violet-500/15">
<Sparkles size={12} className="text-violet-400" />
<span className="text-xs font-semibold text-violet-300">GEPA β€” System Prompt Evolved</span>
<span className="ml-auto text-[10px] text-violet-400/60">Gen {b.fromGen} β†’ {b.toGen}</span>
</div>
<div className="px-4 py-3 flex items-center gap-6">
<div className="text-center">
<div className="text-[10px] text-gray-600 mb-0.5">Before</div>
<div className="text-xl font-bold text-orange-400">{(b.scoreFrom * 100).toFixed(0)}%</div>
</div>
<div className="flex-1 flex flex-col items-center">
<div className="text-xs text-violet-400 mb-1">↑ improved</div>
<div className="w-full h-0.5 bg-gradient-to-r from-orange-400 to-green-400 rounded-full" />
</div>
<div className="text-center">
<div className="text-[10px] text-gray-600 mb-0.5">After</div>
<div className="text-xl font-bold text-green-400">{(b.scoreTo * 100).toFixed(0)}%</div>
</div>
</div>
<div className="px-4 pb-3">
<div className="text-[10px] text-gray-600 mb-1.5 flex items-center gap-1.5">
<GitCommitHorizontal size={10} />
Prompt diff (see sidebar for full view)
</div>
<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)' }}>
{diffPrompts(b.fromGen, b.toGen).filter((l) => l.type !== 'same').map((line, i) => (
<div key={i} className={`flex gap-2 px-2 py-0.5 ${line.type === 'add' ? 'bg-green-500/10' : 'bg-red-500/10'}`}>
<span className={`shrink-0 ${line.type === 'add' ? 'text-green-400' : 'text-red-400'}`}>{line.type === 'add' ? '+' : '-'}</span>
<span className={line.type === 'add' ? 'text-green-300' : 'text-red-300'}>{line.text}</span>
</div>
))}
</div>
</div>
</div>
)
if (b.type === 'group') return (
<div className="border rounded-2xl overflow-hidden" style={{ borderColor: 'var(--border-color)' }}>
<button
onClick={() => setOpen((v) => !v)}
className="w-full flex items-center gap-2 px-3 py-2.5 transition-colors text-left"
style={{ background: 'var(--bg-tertiary)' }}
onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--bg-hover-strong)')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'var(--bg-tertiary)')}
>
{b.success
? <CheckCircle2 size={12} className="text-green-400 shrink-0" />
: <XCircle size={12} className="text-red-400 shrink-0" />}
<span className="text-xs theme-text-secondary flex-1 truncate">{b.question}</span>
<span className="text-[10px] text-gray-600">{b.attempts} attempt{b.attempts !== 1 ? 's' : ''}</span>
{open ? <ChevronUp size={11} className="text-gray-600 shrink-0" /> : <ChevronDown size={11} className="text-gray-600 shrink-0" />}
</button>
<AnimatePresence>
{open && (
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.15 }} className="overflow-hidden">
<div className="p-3 flex flex-col gap-2.5 border-t" style={{ borderColor: 'var(--border-color)' }}>
{b.children.map((c) => <Bubble key={c.id} b={c} />)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)
return null
}
// ─── DemoMode ────────────────────────────────────────────────────────────────
export function DemoMode({ onClose }: { onClose: () => void }) {
const [bubbles, setBubbles] = useState<BubbleData[]>([])
const [appState, setAppState] = useState<'idle' | 'running' | 'done'>('idle')
const [gen, setGen] = useState(0)
const [score, setScore] = useState(SCORES[0])
const [rewardPoints, setRewardPoints] = useState<RewardPoint[]>([])
const [latestDiff, setLatestDiff] = useState<{ from: number; to: number } | null>(null)
const stepRef = useRef(0)
const cancel = useRef(false)
const bottomRef = useRef<HTMLDivElement>(null)
// Track all played queries and GEPA bubbles for end-of-demo collapse
const allQueriesRef = useRef<Array<{ def: QueryDef; children: BubbleData[] }>>([])
const gepaBubblesRef = useRef<BubbleData[]>([])
const scroll = useCallback(() => {
setTimeout(() => bottomRef.current?.scrollIntoView({ behavior: 'smooth' }), 60)
}, [])
const push = useCallback((b: BubbleData) => setBubbles((p) => [...p, b]), [])
const addReward = useCallback((reward: number) => {
stepRef.current += 1
setRewardPoints((p) => [...p, { step: stepRef.current, reward }])
}, [])
const typeUser = useCallback(async (text: string) => {
const id = uid()
push({ id, type: 'user', text: '' })
for (let i = 1; i <= text.length; i++) {
if (cancel.current) return
setBubbles((p) => p.map((b) => b.id === id ? { ...b, text: text.slice(0, i) } : b))
await sleep(32 + Math.random() * 22)
}
scroll(); await sleep(300)
}, [push, scroll])
const streamSQL = useCallback(async (sql: string, attempt: number) => {
const id = uid()
push({ id, type: 'sql_stream', sql: '', attempt })
let built = ''
for (const line of sql.split('\n')) {
if (cancel.current) return
built += (built ? '\n' : '') + line
setBubbles((p) => p.map((b) => b.id === id ? { ...b, sql: built } : b))
await sleep(85 + Math.random() * 70)
}
scroll(); await sleep(200)
setBubbles((p) => p.filter((b) => b.id !== id))
}, [push, scroll])
const playQuery = useCallback(async (def: QueryDef): Promise<BubbleData[]> => {
const children: BubbleData[] = []
await typeUser(def.question)
for (let i = 0; i < def.attempts.length; i++) {
if (cancel.current) return children
const att = def.attempts[i]
// Thinking
const thId = uid()
push({ id: thId, type: 'thinking', label: i === 0 ? 'Generating SQL…' : 'Applying repair strategy…' })
scroll(); await sleep(750)
if (cancel.current) return children
setBubbles((p) => p.filter((b) => b.id !== thId))
await streamSQL(att.sql, i + 1)
if (cancel.current) return children
addReward(att.reward)
if (att.error) {
const eb: SqlErrBubble = { id: uid(), type: 'sql_err', sql: att.sql, error: att.error, errorClass: att.errorClass ?? 'OTHER', rlAction: att.rlAction ?? 'REWRITE_FULL', reward: att.reward, attempt: i + 1 }
push(eb); children.push(eb); scroll(); await sleep(900)
} else {
const ob: SqlOkBubble = { id: uid(), type: 'sql_ok', sql: att.sql, rows: att.rows ?? [], reward: att.reward, attempt: i + 1, firstTry: i === 0 }
push(ob); children.push(ob); scroll(); await sleep(1100)
}
}
return children
}, [push, scroll, typeUser, streamSQL, addReward])
const playGepa = useCallback(async (fromGen: number, toGen: number) => {
const steps = ['Analyzing failure patterns…', 'Identifying missing rules from errors…', 'Rewriting system prompt…', 'Benchmarking candidate prompt…']
for (const label of steps) {
if (cancel.current) return
setBubbles((p) => {
const last = p[p.length - 1]
if (last?.type === 'thinking') return [...p.slice(0, -1), { id: last.id, type: 'thinking' as const, label }]
return [...p, { id: uid(), type: 'thinking' as const, label }]
})
scroll(); await sleep(1050)
}
setBubbles((p) => p.filter((b) => b.type !== 'thinking'))
// Animate score
const from = SCORES[fromGen], to = SCORES[toGen]
for (let i = 0; i <= 45; i++) {
if (cancel.current) return
setScore(from + (to - from) * (i / 45))
await sleep(18)
}
setGen(toGen)
setLatestDiff({ from: fromGen, to: toGen })
const gepaBubble: GepaBubble = { id: uid(), type: 'gepa', fromGen, toGen, scoreFrom: from, scoreTo: to }
gepaBubblesRef.current.push(gepaBubble)
push(gepaBubble)
scroll(); await sleep(1000)
}, [push, scroll])
const autoPlay = useCallback(async () => {
cancel.current = false
allQueriesRef.current = []
gepaBubblesRef.current = []
setBubbles([]); setGen(0); setScore(SCORES[0]); setRewardPoints([]); setLatestDiff(null)
stepRef.current = 0; setAppState('running')
await sleep(300)
for (const id of ROUND_1) {
if (cancel.current) break
const children = await playQuery(QUERIES[id])
if (!cancel.current) {
allQueriesRef.current.push({ def: QUERIES[id], children })
await sleep(600)
}
}
if (!cancel.current) { await playGepa(0, 1); await sleep(500) }
for (const id of ROUND_2) {
if (cancel.current) break
const children = await playQuery(QUERIES[id])
if (!cancel.current) {
allQueriesRef.current.push({ def: QUERIES[id], children })
await sleep(600)
}
}
if (!cancel.current) { await playGepa(1, 2); await sleep(500) }
for (const id of ROUND_3) {
if (cancel.current) break
const children = await playQuery(QUERIES[id])
if (!cancel.current) {
allQueriesRef.current.push({ def: QUERIES[id], children })
await sleep(600)
}
}
if (!cancel.current) {
// Collapse all queries at once, rebuilding the bubble list
const queries = allQueriesRef.current
const gepas = gepaBubblesRef.current
const result: BubbleData[] = []
// Round 1 groups
for (let i = 0; i < ROUND_1.length && i < queries.length; i++) {
const { def, children } = queries[i]
const lastAtt = def.attempts[def.attempts.length - 1]
result.push({ id: uid(), type: 'group', question: def.question, success: !lastAtt.error, attempts: def.attempts.length, children })
}
// GEPA 0β†’1
if (gepas[0]) result.push(gepas[0])
// Round 2 groups
for (let i = 0; i < ROUND_2.length; i++) {
const qi = ROUND_1.length + i
if (qi >= queries.length) break
const { def, children } = queries[qi]
const lastAtt = def.attempts[def.attempts.length - 1]
result.push({ id: uid(), type: 'group', question: def.question, success: !lastAtt.error, attempts: def.attempts.length, children })
}
// GEPA 1β†’2
if (gepas[1]) result.push(gepas[1])
// Round 3 groups
for (let i = 0; i < ROUND_3.length; i++) {
const qi = ROUND_1.length + ROUND_2.length + i
if (qi >= queries.length) break
const { def, children } = queries[qi]
const lastAtt = def.attempts[def.attempts.length - 1]
result.push({ id: uid(), type: 'group', question: def.question, success: !lastAtt.error, attempts: def.attempts.length, children })
}
setBubbles(result)
await sleep(300)
scroll()
setAppState('done')
}
}, [playQuery, playGepa, scroll])
useEffect(() => () => { cancel.current = true }, [])
return (
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] flex flex-col"
style={{ background: 'var(--bg-primary)' }}
>
{/* Header */}
<div className="shrink-0 flex items-center justify-between px-4 py-3 border-b theme-border" style={{ background: 'var(--bg-secondary)' }}>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-violet-500/15 border border-violet-500/25">
<Play size={9} className="text-violet-400" fill="currentColor" />
<span className="text-[11px] font-semibold text-violet-300">Demo</span>
</div>
<span className="text-xs text-gray-600 hidden sm:block">
RL repair loop Β· GEPA prompt evolution Β· 42% β†’ 91%
</span>
</div>
<button onClick={onClose} className="p-1.5 rounded-lg hover:bg-white/5 text-gray-500 hover:text-white transition-colors">
<X size={16} />
</button>
</div>
{/* Body */}
<div className="flex flex-1 overflow-hidden">
{/* Chat */}
<div className="flex-1 flex flex-col overflow-hidden">
<div className="flex-1 overflow-y-auto px-4 py-4">
{appState === 'idle' ? (
<div className="flex flex-col items-center justify-center h-full gap-5 text-center px-6">
<div className="w-14 h-14 rounded-2xl flex items-center justify-center" style={{ background: 'linear-gradient(135deg,#3b0764,#1e3a5f)', boxShadow: '0 12px 32px rgba(139,92,246,0.3)' }}>
<Play size={24} className="text-white ml-0.5" fill="currentColor" />
</div>
<div>
<h2 className="text-base font-bold text-white mb-1.5">SQL Agent OpenEnv β€” Live Demo</h2>
<p className="text-sm text-gray-500 max-w-sm leading-relaxed">
Watch the agent fail, self-repair using LinUCB, then improve through two GEPA prompt evolution cycles.
</p>
</div>
<div className="flex flex-col gap-1.5 text-xs text-gray-600 max-w-xs text-left">
{['Round 1 β€” simple queries, RL repairs table/column errors', 'GEPA cycle 1 β€” prompt learns alias rules (42%β†’74%)', 'Round 2 β€” join queries, ambiguous column errors repaired', 'GEPA cycle 2 β€” prompt learns aggregation rules (74%β†’91%)', 'Round 3 β€” same queries, first-try success'].map((s, i) => (
<div key={i} className="flex items-start gap-2">
<div className="w-4 h-4 rounded-full bg-violet-500/20 border border-violet-500/30 flex items-center justify-center text-[9px] text-violet-400 font-bold shrink-0 mt-0.5">{i + 1}</div>
{s}
</div>
))}
</div>
<button
onClick={() => void autoPlay()}
className="flex items-center gap-2 px-6 py-3 rounded-2xl font-semibold text-sm text-white active:scale-95 transition-transform"
style={{ background: 'linear-gradient(135deg,#7c3aed,#2563eb)', boxShadow: '0 8px 24px rgba(124,58,237,0.4)' }}
>
<Play size={13} fill="currentColor" />
Start Demo
</button>
</div>
) : (
<div className="flex flex-col gap-4 max-w-2xl mx-auto">
<AnimatePresence initial={false}>
{bubbles.map((b) => (
<motion.div key={b.id} initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.18 }}>
<Bubble b={b} />
</motion.div>
))}
</AnimatePresence>
{appState === 'done' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="border border-violet-500/20 rounded-2xl p-5 bg-violet-500/5 text-center mt-2">
<div className="text-3xl font-bold text-violet-300 mb-1 tabular-nums">91%</div>
<div className="text-xs text-gray-500 max-w-xs mx-auto leading-relaxed">
Agent improved from 42% β†’ 91% accuracy through RL repair strategies and two GEPA prompt evolution cycles.
</div>
</motion.div>
)}
<div ref={bottomRef} />
</div>
)}
</div>
</div>
{/* Right panel */}
<aside className="hidden lg:flex flex-col w-72 border-l theme-border overflow-hidden shrink-0" style={{ background: 'var(--bg-secondary)' }}>
<RightPanel gen={gen} score={score} rewardPoints={rewardPoints} latestDiff={latestDiff} />
</aside>
</div>
</motion.div>
)
}