Spaces:
Sleeping
Sleeping
| /** | |
| * 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> | |
| ) | |
| } | |