Spaces:
Sleeping
Sleeping
feat: demo mode with reward chart, github diff, single-difficulty rounds, no loop
Browse files- frontend/src/components/DemoMode.tsx +459 -602
frontend/src/components/DemoMode.tsx
CHANGED
|
@@ -1,56 +1,60 @@
|
|
| 1 |
/**
|
| 2 |
* DemoMode β scripted autoplay showcase for SQL Agent OpenEnv.
|
| 3 |
*
|
| 4 |
-
*
|
| 5 |
-
*
|
|
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
import { useState, useRef, useCallback, useEffect } from 'react'
|
| 9 |
import { motion, AnimatePresence } from 'framer-motion'
|
| 10 |
import {
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
} from 'lucide-react'
|
| 14 |
|
| 15 |
-
// βββ
|
| 16 |
|
| 17 |
const PROMPTS = [
|
| 18 |
-
|
| 19 |
-
`You are a SQL expert. Given a question and a SQLite database schema, write correct SQL.
|
| 20 |
|
| 21 |
Rules:
|
| 22 |
- Output ONLY the SQL query
|
| 23 |
- Use SQLite syntax
|
| 24 |
- No markdown, no code fences`,
|
| 25 |
|
| 26 |
-
|
| 27 |
-
`You are a SQL expert. Given a question and a SQLite database schema, write correct SQL.
|
| 28 |
|
| 29 |
Rules:
|
| 30 |
- Output ONLY the SQL query
|
| 31 |
- Use SQLite syntax
|
| 32 |
- No markdown, no code fences
|
| 33 |
- Always qualify column names with table aliases in JOINs
|
| 34 |
-
- Use t.column_name
|
| 35 |
-
-
|
| 36 |
|
| 37 |
-
|
| 38 |
-
`You are a SQL expert. Given a question and a SQLite database schema, write correct SQL.
|
| 39 |
|
| 40 |
Rules:
|
| 41 |
- Output ONLY the SQL query
|
| 42 |
- Use SQLite syntax
|
| 43 |
- No markdown, no code fences
|
| 44 |
- Always qualify column names with table aliases in JOINs
|
| 45 |
-
- Use t.column_name
|
| 46 |
-
-
|
| 47 |
-
- For aggregations:
|
| 48 |
-
- For revenue
|
| 49 |
-
- For
|
| 50 |
]
|
| 51 |
|
| 52 |
const SCORES = [0.42, 0.74, 0.91]
|
| 53 |
|
|
|
|
|
|
|
| 54 |
interface Attempt {
|
| 55 |
sql: string
|
| 56 |
error?: string
|
|
@@ -63,222 +67,312 @@ interface Attempt {
|
|
| 63 |
interface QueryDef {
|
| 64 |
id: string
|
| 65 |
question: string
|
| 66 |
-
badge: string
|
| 67 |
-
difficulty: 'Easy' | 'Medium' | 'Hard'
|
| 68 |
attempts: Attempt[]
|
| 69 |
}
|
| 70 |
|
| 71 |
const QUERIES: Record<string, QueryDef> = {
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
| 75 |
attempts: [
|
| 76 |
-
{
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
errorClass: 'NO_SUCH_TABLE',
|
| 80 |
-
rlAction: 'FIX_TABLE',
|
| 81 |
-
reward: -0.15,
|
| 82 |
-
},
|
| 83 |
-
{
|
| 84 |
-
sql: 'SELECT * FROM products',
|
| 85 |
-
reward: 0.90,
|
| 86 |
-
rows: [
|
| 87 |
-
{ id: 1, name: 'Wireless Headphones', category: 'Electronics', price: 79.99 },
|
| 88 |
-
{ id: 2, name: 'Running Shoes', category: 'Footwear', price: 59.99 },
|
| 89 |
-
{ id: 3, name: 'Coffee Maker', category: 'Kitchen', price: 49.99 },
|
| 90 |
-
],
|
| 91 |
-
},
|
| 92 |
],
|
| 93 |
},
|
| 94 |
-
|
| 95 |
-
id: '
|
| 96 |
-
|
| 97 |
attempts: [
|
| 98 |
-
{
|
| 99 |
-
|
| 100 |
-
FROM orders
|
| 101 |
-
GROUP BY seller_id
|
| 102 |
-
ORDER BY revenue DESC
|
| 103 |
-
LIMIT 5`,
|
| 104 |
-
error: 'no such column: price',
|
| 105 |
-
errorClass: 'NO_SUCH_COLUMN',
|
| 106 |
-
rlAction: 'FIX_COLUMN',
|
| 107 |
-
reward: -0.20,
|
| 108 |
-
},
|
| 109 |
-
{
|
| 110 |
-
sql: `SELECT seller_id, SUM(total_price) as revenue
|
| 111 |
-
FROM orders
|
| 112 |
-
GROUP BY seller_id
|
| 113 |
-
ORDER BY revenue DESC
|
| 114 |
-
LIMIT 5`,
|
| 115 |
-
error: 'ambiguous column name: seller_id',
|
| 116 |
-
errorClass: 'AMBIGUOUS_COLUMN',
|
| 117 |
-
rlAction: 'FIX_TABLE',
|
| 118 |
-
reward: -0.25,
|
| 119 |
-
},
|
| 120 |
-
{
|
| 121 |
-
sql: `SELECT s.name, SUM(o.total_price) as revenue
|
| 122 |
-
FROM orders o
|
| 123 |
-
JOIN sellers s ON o.user_id = s.id
|
| 124 |
-
GROUP BY s.id, s.name
|
| 125 |
-
ORDER BY revenue DESC
|
| 126 |
-
LIMIT 5`,
|
| 127 |
-
reward: 0.70,
|
| 128 |
-
rows: [
|
| 129 |
-
{ name: 'TechStore Pro', revenue: 12840 },
|
| 130 |
-
{ name: 'StyleHub', revenue: 9320 },
|
| 131 |
-
{ name: 'GadgetWorld', revenue: 8750 },
|
| 132 |
-
{ name: 'HomeGoods Co', revenue: 7200 },
|
| 133 |
-
{ name: 'SportZone', revenue: 6100 },
|
| 134 |
-
],
|
| 135 |
-
},
|
| 136 |
],
|
| 137 |
},
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
| 141 |
attempts: [
|
| 142 |
-
{
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
JOIN users ON user_id = users.id
|
| 146 |
-
GROUP BY country`,
|
| 147 |
-
error: 'ambiguous column name: country',
|
| 148 |
-
errorClass: 'AMBIGUOUS_COLUMN',
|
| 149 |
-
rlAction: 'FIX_COLUMN',
|
| 150 |
-
reward: -0.15,
|
| 151 |
-
},
|
| 152 |
-
{
|
| 153 |
-
sql: `SELECT u.country, ROUND(AVG(o.total_price), 2) as avg_order
|
| 154 |
-
FROM orders o
|
| 155 |
-
JOIN users u ON o.user_id = u.id
|
| 156 |
-
GROUP BY u.country
|
| 157 |
-
ORDER BY avg_order DESC`,
|
| 158 |
-
reward: 0.85,
|
| 159 |
-
rows: [
|
| 160 |
-
{ country: 'USA', avg_order: 142.50 },
|
| 161 |
-
{ country: 'UK', avg_order: 138.20 },
|
| 162 |
-
{ country: 'Germany', avg_order: 121.80 },
|
| 163 |
-
{ country: 'Canada', avg_order: 118.40 },
|
| 164 |
-
],
|
| 165 |
-
},
|
| 166 |
],
|
| 167 |
},
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
badge: 'Easy', difficulty: 'Easy',
|
| 172 |
attempts: [
|
| 173 |
-
{
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
rows: [
|
| 177 |
-
{ id: 1, name: 'Wireless Headphones', category: 'Electronics', price: 79.99 },
|
| 178 |
-
{ id: 2, name: 'Running Shoes', category: 'Footwear', price: 59.99 },
|
| 179 |
-
{ id: 3, name: 'Coffee Maker', category: 'Kitchen', price: 49.99 },
|
| 180 |
-
],
|
| 181 |
-
},
|
| 182 |
],
|
| 183 |
},
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
| 187 |
attempts: [
|
| 188 |
-
{
|
| 189 |
-
|
| 190 |
-
FROM orders o
|
| 191 |
-
JOIN sellers s ON o.user_id = s.id
|
| 192 |
-
GROUP BY s.id, s.name
|
| 193 |
-
ORDER BY revenue DESC
|
| 194 |
-
LIMIT 5`,
|
| 195 |
-
reward: 0.90,
|
| 196 |
-
rows: [
|
| 197 |
-
{ name: 'TechStore Pro', revenue: 12840 },
|
| 198 |
-
{ name: 'StyleHub', revenue: 9320 },
|
| 199 |
-
{ name: 'GadgetWorld', revenue: 8750 },
|
| 200 |
-
],
|
| 201 |
-
},
|
| 202 |
],
|
| 203 |
},
|
| 204 |
-
|
| 205 |
-
id: '
|
| 206 |
-
|
| 207 |
attempts: [
|
| 208 |
-
{
|
| 209 |
-
|
| 210 |
-
FROM orders o
|
| 211 |
-
JOIN users u ON o.user_id = u.id
|
| 212 |
-
GROUP BY u.country
|
| 213 |
-
ORDER BY avg_order DESC`,
|
| 214 |
-
reward: 1.00,
|
| 215 |
-
rows: [
|
| 216 |
-
{ country: 'USA', avg_order: 142.50 },
|
| 217 |
-
{ country: 'UK', avg_order: 138.20 },
|
| 218 |
-
{ country: 'Germany', avg_order: 121.80 },
|
| 219 |
-
],
|
| 220 |
-
},
|
| 221 |
],
|
| 222 |
},
|
| 223 |
}
|
| 224 |
|
| 225 |
-
const ROUND_1 = ['
|
| 226 |
-
const ROUND_2 = ['
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
-
|
| 229 |
|
| 230 |
-
|
| 231 |
-
const
|
|
|
|
|
|
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
}
|
| 236 |
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
| 238 |
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
-
/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
function HighlightSQL({ sql }: { sql: string }) {
|
| 253 |
-
const
|
| 254 |
const parts: React.ReactNode[] = []
|
| 255 |
-
let last = 0
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
while ((match = re.exec(sql)) !== null) {
|
| 259 |
if (match.index > last) parts.push(<span key={`t${last}`}>{sql.slice(last, match.index)}</span>)
|
| 260 |
parts.push(<span key={`k${match.index}`} className="text-violet-300 font-semibold">{match[0]}</span>)
|
| 261 |
last = match.index + match[0].length
|
| 262 |
}
|
| 263 |
-
if (last < sql.length) parts.push(<span key="
|
| 264 |
return <>{parts}</>
|
| 265 |
}
|
| 266 |
|
| 267 |
-
// βββ Bubble
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
function Bubble({ b }: { b: BubbleData }) {
|
| 270 |
const [open, setOpen] = useState(false)
|
| 271 |
|
| 272 |
if (b.type === 'user') return (
|
| 273 |
<div className="flex justify-end">
|
| 274 |
-
<div className="max-w-[
|
| 275 |
<p className="text-sm text-white">{b.text}</p>
|
| 276 |
</div>
|
| 277 |
</div>
|
| 278 |
)
|
| 279 |
|
| 280 |
if (b.type === 'thinking') return (
|
| 281 |
-
<div className="flex items-center gap-2 text-xs text-gray-500
|
| 282 |
<Loader2 size={11} className="animate-spin text-violet-400 shrink-0" />
|
| 283 |
{b.label}
|
| 284 |
</div>
|
|
@@ -288,10 +382,10 @@ function Bubble({ b }: { b: BubbleData }) {
|
|
| 288 |
<div className="border border-white/[0.06] rounded-xl overflow-hidden">
|
| 289 |
<div className="px-3 py-1.5 bg-white/[0.02] flex items-center gap-2">
|
| 290 |
<span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
|
| 291 |
-
<span className="text-[10px] text-gray-600">
|
| 292 |
<Loader2 size={9} className="animate-spin text-violet-400 ml-auto" />
|
| 293 |
</div>
|
| 294 |
-
<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.
|
| 295 |
<HighlightSQL sql={b.sql} />
|
| 296 |
<span className="inline-block w-0.5 h-[1em] bg-violet-400 animate-pulse align-bottom ml-0.5" />
|
| 297 |
</pre>
|
|
@@ -303,26 +397,20 @@ function Bubble({ b }: { b: BubbleData }) {
|
|
| 303 |
<div className="border border-white/[0.06] rounded-xl overflow-hidden">
|
| 304 |
<div className="px-3 py-1.5 bg-white/[0.02] flex items-center gap-2">
|
| 305 |
<span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
|
| 306 |
-
<span className="text-[10px] text-gray-600">
|
| 307 |
</div>
|
| 308 |
-
<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.
|
| 309 |
<HighlightSQL sql={b.sql} />
|
| 310 |
</pre>
|
| 311 |
</div>
|
| 312 |
-
<div className="flex items-
|
| 313 |
-
<XCircle size={11} className="
|
| 314 |
-
<
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
</
|
| 320 |
-
<div className="flex items-center gap-1.5 shrink-0">
|
| 321 |
-
<span className="inline-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">
|
| 322 |
-
<Zap size={8} />{b.rlAction}
|
| 323 |
-
</span>
|
| 324 |
-
<span className="text-[11px] font-bold text-red-400">{b.reward.toFixed(2)}</span>
|
| 325 |
-
</div>
|
| 326 |
</div>
|
| 327 |
</div>
|
| 328 |
)
|
|
@@ -333,41 +421,38 @@ function Bubble({ b }: { b: BubbleData }) {
|
|
| 333 |
<div className="px-3 py-1.5 bg-white/[0.02] flex items-center gap-2">
|
| 334 |
<span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
|
| 335 |
{b.firstTry && (
|
| 336 |
-
<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>
|
| 337 |
)}
|
|
|
|
| 338 |
</div>
|
| 339 |
-
<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.
|
| 340 |
<HighlightSQL sql={b.sql} />
|
| 341 |
</pre>
|
| 342 |
</div>
|
| 343 |
-
<div className="flex items-center gap-
|
| 344 |
<CheckCircle2 size={11} className="text-green-400" />
|
| 345 |
<span className="text-green-400 font-semibold">Success</span>
|
| 346 |
-
<span className="text-gray-600">Β· {b.rows.length} rows</span>
|
| 347 |
-
<span className="ml-auto text-[11px] font-bold text-green-400">+{b.reward.toFixed(2)}</span>
|
| 348 |
</div>
|
| 349 |
-
{/* Results mini-table */}
|
| 350 |
<div className="rounded-xl border border-white/[0.06] overflow-hidden text-[10px]">
|
| 351 |
-
<
|
| 352 |
-
<
|
| 353 |
-
<
|
| 354 |
-
|
| 355 |
-
{
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
))}
|
| 358 |
</tr>
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
<tr key={i} className={i % 2 === 0 ? 'bg-white/[0.01]' : ''}>
|
| 363 |
-
{Object.values(row).map((v, j) => (
|
| 364 |
-
<td key={j} className="px-2 py-1 text-gray-300 whitespace-nowrap">{String(v)}</td>
|
| 365 |
-
))}
|
| 366 |
-
</tr>
|
| 367 |
-
))}
|
| 368 |
-
</tbody>
|
| 369 |
-
</table>
|
| 370 |
-
</div>
|
| 371 |
</div>
|
| 372 |
</div>
|
| 373 |
)
|
|
@@ -375,201 +460,104 @@ function Bubble({ b }: { b: BubbleData }) {
|
|
| 375 |
if (b.type === 'gepa') return (
|
| 376 |
<div className="border border-violet-500/25 rounded-2xl overflow-hidden bg-violet-500/5">
|
| 377 |
<div className="px-4 py-3 flex items-center gap-2 border-b border-violet-500/15">
|
| 378 |
-
<Sparkles size={
|
| 379 |
-
<span className="text-xs font-semibold text-violet-300">GEPA Prompt
|
| 380 |
-
<span className="ml-auto text-[10px] text-violet-400/
|
| 381 |
</div>
|
| 382 |
-
<div className="px-4 py-3 flex items-center gap-
|
| 383 |
-
<div className="
|
| 384 |
-
<div className="text-[10px] text-gray-600">Before</div>
|
| 385 |
-
<div className="text-
|
| 386 |
</div>
|
| 387 |
-
<
|
| 388 |
-
|
| 389 |
-
<div className="
|
| 390 |
-
<div className="text-lg font-bold text-green-400">{(b.scoreTo * 100).toFixed(0)}%</div>
|
| 391 |
</div>
|
| 392 |
-
<div className="
|
| 393 |
-
|
|
|
|
| 394 |
</div>
|
| 395 |
</div>
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
const color = b.difficulty === 'Easy' ? 'text-green-400' : b.difficulty === 'Hard' ? 'text-red-400' : 'text-amber-400'
|
| 401 |
-
return (
|
| 402 |
-
<div className="border border-white/[0.06] rounded-2xl overflow-hidden">
|
| 403 |
-
<button
|
| 404 |
-
onClick={() => setOpen((v) => !v)}
|
| 405 |
-
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"
|
| 406 |
-
>
|
| 407 |
-
{b.success
|
| 408 |
-
? <CheckCircle2 size={12} className="text-green-400 shrink-0" />
|
| 409 |
-
: <XCircle size={12} className="text-red-400 shrink-0" />
|
| 410 |
-
}
|
| 411 |
-
<span className="text-xs text-gray-300 flex-1 truncate">{b.question}</span>
|
| 412 |
-
<span className={`text-[10px] font-semibold px-1.5 py-0.5 rounded-full border ${color} border-current/30 bg-current/10`}>{b.badge}</span>
|
| 413 |
-
<span className="text-[10px] text-gray-600">{b.attempts} attempt{b.attempts !== 1 ? 's' : ''}</span>
|
| 414 |
-
{open ? <ChevronUp size={11} className="text-gray-600 shrink-0" /> : <ChevronDown size={11} className="text-gray-600 shrink-0" />}
|
| 415 |
-
</button>
|
| 416 |
-
<AnimatePresence>
|
| 417 |
-
{open && (
|
| 418 |
-
<motion.div
|
| 419 |
-
initial={{ height: 0, opacity: 0 }}
|
| 420 |
-
animate={{ height: 'auto', opacity: 1 }}
|
| 421 |
-
exit={{ height: 0, opacity: 0 }}
|
| 422 |
-
transition={{ duration: 0.15 }}
|
| 423 |
-
className="overflow-hidden"
|
| 424 |
-
>
|
| 425 |
-
<div className="p-3 flex flex-col gap-2.5 border-t border-white/[0.04]">
|
| 426 |
-
{b.children.map((child) => <Bubble key={child.id} b={child} />)}
|
| 427 |
-
</div>
|
| 428 |
-
</motion.div>
|
| 429 |
-
)}
|
| 430 |
-
</AnimatePresence>
|
| 431 |
-
</div>
|
| 432 |
-
)
|
| 433 |
-
}
|
| 434 |
-
|
| 435 |
-
return null
|
| 436 |
-
}
|
| 437 |
-
|
| 438 |
-
// βββ Right panel β prompt + score βββββββββββββββββββββββββββββββββββββββββββββ
|
| 439 |
-
|
| 440 |
-
function PromptPanel({ gen, score }: { gen: number; score: number }) {
|
| 441 |
-
const [open, setOpen] = useState(false)
|
| 442 |
-
const prompt = PROMPTS[gen] ?? PROMPTS[0]
|
| 443 |
-
const lines = prompt.split('\n')
|
| 444 |
-
|
| 445 |
-
return (
|
| 446 |
-
<div className="flex flex-col gap-3 px-4 py-4">
|
| 447 |
-
<div className="flex items-center justify-between">
|
| 448 |
-
<div className="flex items-center gap-2">
|
| 449 |
-
<Sparkles size={12} className="text-violet-400" />
|
| 450 |
-
<span className="text-[11px] font-semibold text-gray-400 uppercase tracking-wider">GEPA Prompt</span>
|
| 451 |
</div>
|
| 452 |
-
<
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
<div>
|
| 460 |
-
<div className="text-[10px] text-gray-600 mb-0.5">Benchmark score</div>
|
| 461 |
-
<div className="text-2xl font-bold text-green-400 tabular-nums">{(score * 100).toFixed(0)}%</div>
|
| 462 |
-
</div>
|
| 463 |
-
<div className="flex-1 h-1.5 bg-white/[0.05] rounded-full overflow-hidden">
|
| 464 |
-
<motion.div
|
| 465 |
-
className="h-full bg-gradient-to-r from-violet-500 to-green-400 rounded-full"
|
| 466 |
-
animate={{ width: `${score * 100}%` }}
|
| 467 |
-
transition={{ duration: 0.8, ease: 'easeOut' }}
|
| 468 |
-
/>
|
| 469 |
</div>
|
| 470 |
</div>
|
|
|
|
|
|
|
| 471 |
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
<
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
<div className="px-3 py-2.5 text-[10px] leading-relaxed font-mono text-gray-400 max-h-48 overflow-y-auto">
|
| 491 |
-
{lines.map((line, i) => {
|
| 492 |
-
const isNew = gen > 0 && i >= PROMPTS[gen - 1].split('\n').length
|
| 493 |
-
return (
|
| 494 |
-
<div
|
| 495 |
-
key={i}
|
| 496 |
-
className={isNew ? 'text-green-400 bg-green-500/8 -mx-3 px-3' : ''}
|
| 497 |
-
>
|
| 498 |
-
{line || <br />}
|
| 499 |
-
</div>
|
| 500 |
-
)
|
| 501 |
-
})}
|
| 502 |
-
</div>
|
| 503 |
-
</motion.div>
|
| 504 |
-
)}
|
| 505 |
-
</AnimatePresence>
|
| 506 |
-
</div>
|
| 507 |
-
|
| 508 |
-
{/* Score history */}
|
| 509 |
-
<div className="flex flex-col gap-1">
|
| 510 |
-
<div className="text-[10px] text-gray-600 font-semibold uppercase tracking-wider mb-0.5">Evolution</div>
|
| 511 |
-
{SCORES.slice(0, gen + 1).map((s, i) => (
|
| 512 |
-
<div key={i} className="flex items-center gap-2">
|
| 513 |
-
<span className="text-[10px] text-gray-600 w-10 shrink-0">Gen {i}</span>
|
| 514 |
-
<div className="flex-1 h-1 bg-white/[0.05] rounded-full overflow-hidden">
|
| 515 |
-
<div
|
| 516 |
-
className="h-full rounded-full"
|
| 517 |
-
style={{
|
| 518 |
-
width: `${s * 100}%`,
|
| 519 |
-
background: i === gen ? 'linear-gradient(90deg,#8b5cf6,#22c55e)' : '#374151',
|
| 520 |
-
}}
|
| 521 |
-
/>
|
| 522 |
</div>
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
</div>
|
| 527 |
-
))}
|
| 528 |
-
</div>
|
| 529 |
</div>
|
| 530 |
)
|
| 531 |
-
}
|
| 532 |
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
interface DemoModeProps {
|
| 536 |
-
onClose: () => void
|
| 537 |
}
|
| 538 |
|
| 539 |
-
|
|
|
|
|
|
|
| 540 |
const [bubbles, setBubbles] = useState<BubbleData[]>([])
|
| 541 |
-
const [appState, setAppState] = useState<
|
| 542 |
const [gen, setGen] = useState(0)
|
| 543 |
const [score, setScore] = useState(SCORES[0])
|
|
|
|
|
|
|
|
|
|
| 544 |
const cancel = useRef(false)
|
| 545 |
const bottomRef = useRef<HTMLDivElement>(null)
|
| 546 |
|
| 547 |
-
const
|
| 548 |
-
|
| 549 |
}, [])
|
| 550 |
|
| 551 |
-
const
|
| 552 |
-
setBubbles((prev) => [...prev.slice(0, -1), b])
|
| 553 |
-
}, [])
|
| 554 |
|
| 555 |
-
const
|
| 556 |
-
|
|
|
|
| 557 |
}, [])
|
| 558 |
|
| 559 |
-
// Type text character by character
|
| 560 |
const typeUser = useCallback(async (text: string) => {
|
| 561 |
const id = uid()
|
| 562 |
push({ id, type: 'user', text: '' })
|
| 563 |
for (let i = 1; i <= text.length; i++) {
|
| 564 |
if (cancel.current) return
|
| 565 |
-
setBubbles((
|
| 566 |
-
await sleep(
|
| 567 |
}
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
}, [push, scrollDown])
|
| 571 |
|
| 572 |
-
// Stream SQL line by line
|
| 573 |
const streamSQL = useCallback(async (sql: string, attempt: number) => {
|
| 574 |
const id = uid()
|
| 575 |
push({ id, type: 'sql_stream', sql: '', attempt })
|
|
@@ -577,281 +565,168 @@ export function DemoMode({ onClose }: DemoModeProps) {
|
|
| 577 |
for (const line of sql.split('\n')) {
|
| 578 |
if (cancel.current) return
|
| 579 |
built += (built ? '\n' : '') + line
|
| 580 |
-
setBubbles((
|
| 581 |
-
await sleep(
|
| 582 |
}
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
setBubbles((prev) => prev.filter((b) => b.id !== id))
|
| 587 |
-
}, [push, scrollDown])
|
| 588 |
|
| 589 |
-
// Play one full query (possibly multiple attempts)
|
| 590 |
const playQuery = useCallback(async (def: QueryDef): Promise<BubbleData[]> => {
|
| 591 |
const children: BubbleData[] = []
|
| 592 |
-
|
| 593 |
await typeUser(def.question)
|
| 594 |
|
| 595 |
for (let i = 0; i < def.attempts.length; i++) {
|
| 596 |
if (cancel.current) return children
|
| 597 |
const att = def.attempts[i]
|
| 598 |
-
const attemptNum = i + 1
|
| 599 |
|
| 600 |
-
//
|
| 601 |
-
const
|
| 602 |
-
push({ id:
|
| 603 |
-
|
| 604 |
-
await sleep(800)
|
| 605 |
if (cancel.current) return children
|
| 606 |
-
setBubbles((
|
| 607 |
|
| 608 |
-
await streamSQL(att.sql,
|
| 609 |
if (cancel.current) return children
|
| 610 |
|
|
|
|
|
|
|
| 611 |
if (att.error) {
|
| 612 |
-
const
|
| 613 |
-
|
| 614 |
-
sql: att.sql, error: att.error,
|
| 615 |
-
errorClass: att.errorClass ?? 'OTHER',
|
| 616 |
-
rlAction: att.rlAction ?? 'REWRITE_FULL',
|
| 617 |
-
reward: att.reward,
|
| 618 |
-
attempt: attemptNum,
|
| 619 |
-
}
|
| 620 |
-
push(errBubble)
|
| 621 |
-
children.push(errBubble)
|
| 622 |
-
scrollDown()
|
| 623 |
-
await sleep(900)
|
| 624 |
} else {
|
| 625 |
-
const
|
| 626 |
-
|
| 627 |
-
sql: att.sql,
|
| 628 |
-
rows: att.rows ?? [],
|
| 629 |
-
reward: att.reward,
|
| 630 |
-
attempt: attemptNum,
|
| 631 |
-
badge: def.badge,
|
| 632 |
-
firstTry: attemptNum === 1,
|
| 633 |
-
}
|
| 634 |
-
push(okBubble)
|
| 635 |
-
children.push(okBubble)
|
| 636 |
-
scrollDown()
|
| 637 |
-
await sleep(1200)
|
| 638 |
}
|
| 639 |
}
|
| 640 |
-
|
| 641 |
return children
|
| 642 |
-
}, [push,
|
| 643 |
|
| 644 |
-
// Collapse query into a group bubble
|
| 645 |
const collapseQuery = useCallback((def: QueryDef, children: BubbleData[]) => {
|
| 646 |
-
const
|
| 647 |
-
const success = !lastAttempt.error
|
| 648 |
-
|
| 649 |
-
// Remove user bubble + children, replace with group
|
| 650 |
setBubbles((prev) => {
|
| 651 |
-
|
| 652 |
-
const userIdx = [...prev].reverse().findIndex(
|
| 653 |
-
(b) => b.type === 'user' && (b as UserBubble).text === def.question
|
| 654 |
-
)
|
| 655 |
if (userIdx < 0) return prev
|
| 656 |
const fromIdx = prev.length - 1 - userIdx
|
| 657 |
-
const
|
| 658 |
-
|
| 659 |
-
const group: GroupBubble = {
|
| 660 |
-
id: uid(),
|
| 661 |
-
type: 'group',
|
| 662 |
-
question: userBubble.text,
|
| 663 |
-
badge: def.badge,
|
| 664 |
-
difficulty: def.difficulty,
|
| 665 |
-
success,
|
| 666 |
-
attempts: def.attempts.length,
|
| 667 |
-
children,
|
| 668 |
-
}
|
| 669 |
return [...prev.slice(0, fromIdx), group]
|
| 670 |
})
|
| 671 |
}, [])
|
| 672 |
|
| 673 |
-
// Animate GEPA cycle
|
| 674 |
const playGepa = useCallback(async (fromGen: number, toGen: number) => {
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
const thinkId = uid()
|
| 678 |
-
const steps = [
|
| 679 |
-
'Analyzing failure patternsβ¦',
|
| 680 |
-
'Identifying missing rulesβ¦',
|
| 681 |
-
'Rewriting system promptβ¦',
|
| 682 |
-
'Benchmarking new promptβ¦',
|
| 683 |
-
]
|
| 684 |
-
for (const step of steps) {
|
| 685 |
if (cancel.current) return
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
if (last?.type === 'thinking') return [...prev.slice(0, -1), { id: thinkId, type: 'thinking', label: step }]
|
| 691 |
-
return [...prev, { id: thinkId, type: 'thinking', label: step }]
|
| 692 |
})
|
| 693 |
-
|
| 694 |
-
await sleep(1100)
|
| 695 |
}
|
| 696 |
-
|
| 697 |
-
setBubbles((prev) => prev.filter((b) => b.id !== thinkId))
|
| 698 |
|
| 699 |
// Animate score
|
| 700 |
-
const from = SCORES[fromGen]
|
| 701 |
-
|
| 702 |
-
for (let i = 0; i <= 40; i++) {
|
| 703 |
if (cancel.current) return
|
| 704 |
-
setScore(from + (to - from) * (i /
|
| 705 |
-
await sleep(
|
| 706 |
}
|
| 707 |
setGen(toGen)
|
|
|
|
| 708 |
|
| 709 |
-
// Push GEPA bubble
|
| 710 |
push({ id: uid(), type: 'gepa', fromGen, toGen, scoreFrom: from, scoreTo: to })
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
setAppState('running')
|
| 714 |
-
}, [push, replaceLast, scrollDown])
|
| 715 |
|
| 716 |
const autoPlay = useCallback(async () => {
|
| 717 |
cancel.current = false
|
| 718 |
-
setBubbles([])
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
setAppState('running')
|
| 722 |
-
await sleep(400)
|
| 723 |
|
| 724 |
-
// Round 1
|
| 725 |
for (const id of ROUND_1) {
|
| 726 |
if (cancel.current) break
|
| 727 |
-
const
|
| 728 |
-
const children = await playQuery(def)
|
| 729 |
if (cancel.current) break
|
| 730 |
-
await sleep(
|
| 731 |
-
collapseQuery(def, children)
|
| 732 |
-
await sleep(700)
|
| 733 |
}
|
|
|
|
| 734 |
|
| 735 |
-
if (!cancel.current) {
|
| 736 |
-
await playGepa(0, 1)
|
| 737 |
-
await sleep(500)
|
| 738 |
-
}
|
| 739 |
-
|
| 740 |
-
// Round 2
|
| 741 |
for (const id of ROUND_2) {
|
| 742 |
if (cancel.current) break
|
| 743 |
-
const
|
| 744 |
-
const children = await playQuery(def)
|
| 745 |
if (cancel.current) break
|
| 746 |
-
await sleep(
|
| 747 |
-
collapseQuery(def, children)
|
| 748 |
-
await sleep(700)
|
| 749 |
}
|
|
|
|
| 750 |
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
|
|
|
|
|
|
| 754 |
}
|
| 755 |
-
}, [playQuery, collapseQuery, playGepa])
|
| 756 |
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
const handleReplay = () => {
|
| 760 |
-
cancel.current = true
|
| 761 |
-
setTimeout(() => {
|
| 762 |
-
cancel.current = false
|
| 763 |
-
setBubbles([])
|
| 764 |
-
setGen(0)
|
| 765 |
-
setScore(SCORES[0])
|
| 766 |
-
setAppState('idle')
|
| 767 |
-
}, 200)
|
| 768 |
-
}
|
| 769 |
|
| 770 |
-
useEffect(() => {
|
| 771 |
-
return () => { cancel.current = true }
|
| 772 |
-
}, [])
|
| 773 |
|
| 774 |
return (
|
| 775 |
<motion.div
|
| 776 |
-
initial={{ opacity: 0 }}
|
| 777 |
-
animate={{ opacity: 1 }}
|
| 778 |
-
exit={{ opacity: 0 }}
|
| 779 |
className="fixed inset-0 z-[100] flex flex-col"
|
| 780 |
style={{ background: 'var(--bg-primary)' }}
|
| 781 |
>
|
| 782 |
-
{/* Header
|
| 783 |
-
<div
|
| 784 |
-
className="shrink-0 flex items-center justify-between px-4 py-3 border-b border-white/[0.06]"
|
| 785 |
-
style={{ background: 'var(--bg-secondary)' }}
|
| 786 |
-
>
|
| 787 |
<div className="flex items-center gap-3">
|
| 788 |
<div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-violet-500/15 border border-violet-500/25">
|
| 789 |
-
<Play size={
|
| 790 |
-
<span className="text-[11px] font-semibold text-violet-300">Demo
|
| 791 |
</div>
|
| 792 |
-
<span className="text-xs text-gray-
|
| 793 |
-
|
| 794 |
</span>
|
| 795 |
</div>
|
| 796 |
-
<
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
onClick={handleReplay}
|
| 800 |
-
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-white/[0.06] text-[11px] text-gray-400 hover:text-white hover:bg-white/5 transition-all"
|
| 801 |
-
>
|
| 802 |
-
<RotateCcw size={11} />
|
| 803 |
-
Replay
|
| 804 |
-
</button>
|
| 805 |
-
)}
|
| 806 |
-
<button
|
| 807 |
-
onClick={onClose}
|
| 808 |
-
className="p-1.5 rounded-lg hover:bg-white/5 transition-colors text-gray-500 hover:text-white"
|
| 809 |
-
>
|
| 810 |
-
<X size={16} />
|
| 811 |
-
</button>
|
| 812 |
-
</div>
|
| 813 |
</div>
|
| 814 |
|
| 815 |
{/* Body */}
|
| 816 |
<div className="flex flex-1 overflow-hidden">
|
| 817 |
-
{/* Chat
|
| 818 |
<div className="flex-1 flex flex-col overflow-hidden">
|
| 819 |
<div className="flex-1 overflow-y-auto px-4 py-4">
|
| 820 |
{appState === 'idle' ? (
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
className="w-16 h-16 rounded-2xl flex items-center justify-center"
|
| 825 |
-
style={{ background: 'linear-gradient(135deg,#3b0764,#1e3a5f)', boxShadow: '0 12px 40px rgba(139,92,246,0.3)' }}
|
| 826 |
-
>
|
| 827 |
-
<Play size={26} className="text-white ml-0.5" fill="currentColor" />
|
| 828 |
</div>
|
| 829 |
<div>
|
| 830 |
-
<h2 className="text-
|
| 831 |
-
<p className="text-sm text-gray-500 max-w-sm">
|
| 832 |
-
Watch the agent fail
|
| 833 |
-
and improve via GEPA prompt evolution β from 42% β 91% accuracy.
|
| 834 |
</p>
|
| 835 |
</div>
|
| 836 |
-
<div className="flex flex-col gap-
|
| 837 |
-
{[
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
'Round 2 β same queries succeed faster',
|
| 841 |
-
'GEPA cycle β Gen 1 β Gen 2 (91% accuracy)',
|
| 842 |
-
].map((s, i) => (
|
| 843 |
-
<div key={i} className="flex items-center gap-2">
|
| 844 |
-
<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">{i + 1}</div>
|
| 845 |
{s}
|
| 846 |
</div>
|
| 847 |
))}
|
| 848 |
</div>
|
| 849 |
<button
|
| 850 |
-
onClick={
|
| 851 |
-
className="flex items-center gap-2 px-6 py-3 rounded-2xl font-semibold text-sm text-white
|
| 852 |
style={{ background: 'linear-gradient(135deg,#7c3aed,#2563eb)', boxShadow: '0 8px 24px rgba(124,58,237,0.4)' }}
|
| 853 |
>
|
| 854 |
-
<Play size={
|
| 855 |
Start Demo
|
| 856 |
</button>
|
| 857 |
</div>
|
|
@@ -859,34 +734,19 @@ export function DemoMode({ onClose }: DemoModeProps) {
|
|
| 859 |
<div className="flex flex-col gap-4 max-w-2xl mx-auto">
|
| 860 |
<AnimatePresence initial={false}>
|
| 861 |
{bubbles.map((b) => (
|
| 862 |
-
<motion.div
|
| 863 |
-
key={b.id}
|
| 864 |
-
initial={{ opacity: 0, y: 8 }}
|
| 865 |
-
animate={{ opacity: 1, y: 0 }}
|
| 866 |
-
transition={{ duration: 0.2 }}
|
| 867 |
-
>
|
| 868 |
<Bubble b={b} />
|
| 869 |
</motion.div>
|
| 870 |
))}
|
| 871 |
</AnimatePresence>
|
|
|
|
| 872 |
{appState === 'done' && (
|
| 873 |
-
<motion.div
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
className="
|
| 877 |
-
|
| 878 |
-
<div className="text-2xl font-bold text-green-400 mb-1">91%</div>
|
| 879 |
-
<div className="text-sm text-white font-semibold mb-1">Demo complete</div>
|
| 880 |
-
<div className="text-xs text-gray-500">
|
| 881 |
-
Agent improved from 42% β 91% through RL repair + GEPA evolution
|
| 882 |
</div>
|
| 883 |
-
<button
|
| 884 |
-
onClick={handleReplay}
|
| 885 |
-
className="mt-3 flex items-center gap-1.5 px-4 py-2 rounded-xl border border-white/[0.06] text-xs text-gray-400 hover:text-white hover:bg-white/5 transition-all mx-auto"
|
| 886 |
-
>
|
| 887 |
-
<RotateCcw size={11} />
|
| 888 |
-
Watch again
|
| 889 |
-
</button>
|
| 890 |
</motion.div>
|
| 891 |
)}
|
| 892 |
<div ref={bottomRef} />
|
|
@@ -895,12 +755,9 @@ export function DemoMode({ onClose }: DemoModeProps) {
|
|
| 895 |
</div>
|
| 896 |
</div>
|
| 897 |
|
| 898 |
-
{/* Right panel
|
| 899 |
-
<aside
|
| 900 |
-
|
| 901 |
-
style={{ background: 'var(--bg-secondary)' }}
|
| 902 |
-
>
|
| 903 |
-
<PromptPanel gen={gen} score={score} />
|
| 904 |
</aside>
|
| 905 |
</div>
|
| 906 |
</motion.div>
|
|
|
|
| 1 |
/**
|
| 2 |
* DemoMode β scripted autoplay showcase for SQL Agent OpenEnv.
|
| 3 |
*
|
| 4 |
+
* Right panel: live reward chart + GitHub-style prompt diff.
|
| 5 |
+
* Chat: single-difficulty rounds separated by GEPA evolution.
|
| 6 |
+
* No looping β scrollable at the end.
|
| 7 |
*/
|
| 8 |
|
| 9 |
import { useState, useRef, useCallback, useEffect } from 'react'
|
| 10 |
import { motion, AnimatePresence } from 'framer-motion'
|
| 11 |
import {
|
| 12 |
+
LineChart, Line, XAxis, YAxis, CartesianGrid,
|
| 13 |
+
Tooltip, ResponsiveContainer, ReferenceLine,
|
| 14 |
+
} from 'recharts'
|
| 15 |
+
import {
|
| 16 |
+
Play, X, Zap, CheckCircle2, XCircle,
|
| 17 |
+
ChevronDown, ChevronUp, Sparkles, Loader2, GitCommitHorizontal,
|
| 18 |
} from 'lucide-react'
|
| 19 |
|
| 20 |
+
// βββ Prompts (3 generations) βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 21 |
|
| 22 |
const PROMPTS = [
|
| 23 |
+
`You are a SQL expert. Given a question and a SQLite schema, write correct SQL.
|
|
|
|
| 24 |
|
| 25 |
Rules:
|
| 26 |
- Output ONLY the SQL query
|
| 27 |
- Use SQLite syntax
|
| 28 |
- No markdown, no code fences`,
|
| 29 |
|
| 30 |
+
`You are a SQL expert. Given a question and a SQLite schema, write correct SQL.
|
|
|
|
| 31 |
|
| 32 |
Rules:
|
| 33 |
- Output ONLY the SQL query
|
| 34 |
- Use SQLite syntax
|
| 35 |
- No markdown, no code fences
|
| 36 |
- Always qualify column names with table aliases in JOINs
|
| 37 |
+
- Use t.column_name to avoid ambiguous column errors
|
| 38 |
+
- Verify every column name against the schema before use`,
|
| 39 |
|
| 40 |
+
`You are a SQL expert. Given a question and a SQLite schema, write correct SQL.
|
|
|
|
| 41 |
|
| 42 |
Rules:
|
| 43 |
- Output ONLY the SQL query
|
| 44 |
- Use SQLite syntax
|
| 45 |
- No markdown, no code fences
|
| 46 |
- Always qualify column names with table aliases in JOINs
|
| 47 |
+
- Use t.column_name to avoid ambiguous column errors
|
| 48 |
+
- Verify every column name against the schema before use
|
| 49 |
+
- For aggregations: include all non-aggregated columns in GROUP BY
|
| 50 |
+
- For revenue: use orders.total_price (not price or amount)
|
| 51 |
+
- For top-N: use ORDER BY β¦ DESC LIMIT N`,
|
| 52 |
]
|
| 53 |
|
| 54 |
const SCORES = [0.42, 0.74, 0.91]
|
| 55 |
|
| 56 |
+
// βββ Scripted query data βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 57 |
+
|
| 58 |
interface Attempt {
|
| 59 |
sql: string
|
| 60 |
error?: string
|
|
|
|
| 67 |
interface QueryDef {
|
| 68 |
id: string
|
| 69 |
question: string
|
|
|
|
|
|
|
| 70 |
attempts: Attempt[]
|
| 71 |
}
|
| 72 |
|
| 73 |
const QUERIES: Record<string, QueryDef> = {
|
| 74 |
+
// Round 1 β simple queries, some fail
|
| 75 |
+
r1q1: {
|
| 76 |
+
id: 'r1q1',
|
| 77 |
+
question: 'Show all products',
|
| 78 |
attempts: [
|
| 79 |
+
{ sql: 'SELECT * FROM product', error: 'no such table: product', errorClass: 'NO_SUCH_TABLE', rlAction: 'FIX_TABLE', reward: -0.15 },
|
| 80 |
+
{ sql: 'SELECT * FROM products LIMIT 10', reward: 0.90,
|
| 81 |
+
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 }] },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
],
|
| 83 |
},
|
| 84 |
+
r1q2: {
|
| 85 |
+
id: 'r1q2',
|
| 86 |
+
question: 'List all users from the USA',
|
| 87 |
attempts: [
|
| 88 |
+
{ sql: "SELECT * FROM users WHERE country = 'USA'", reward: 1.00,
|
| 89 |
+
rows: [{ id: 1, name: 'Alice Johnson', email: 'alice@example.com', country: 'USA' }, { id: 2, name: 'Bob Smith', email: 'bob@example.com', country: 'USA' }] },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
],
|
| 91 |
},
|
| 92 |
+
|
| 93 |
+
// Round 2 β join queries, ambiguous columns fixed by GEPA
|
| 94 |
+
r2q1: {
|
| 95 |
+
id: 'r2q1',
|
| 96 |
+
question: 'Top 5 sellers by total revenue',
|
| 97 |
attempts: [
|
| 98 |
+
{ 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 },
|
| 99 |
+
{ 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,
|
| 100 |
+
rows: [{ seller: 'TechStore Pro', revenue: 12840 }, { seller: 'StyleHub', revenue: 9320 }, { seller: 'GadgetWorld', revenue: 8750 }] },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
],
|
| 102 |
},
|
| 103 |
+
r2q2: {
|
| 104 |
+
id: 'r2q2',
|
| 105 |
+
question: 'Average order value by country',
|
|
|
|
| 106 |
attempts: [
|
| 107 |
+
{ 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 },
|
| 108 |
+
{ 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,
|
| 109 |
+
rows: [{ country: 'USA', avg_order: 142.50 }, { country: 'UK', avg_order: 138.20 }, { country: 'Germany', avg_order: 121.80 }] },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
],
|
| 111 |
},
|
| 112 |
+
|
| 113 |
+
// Round 3 β after GEPA 2, same joins succeed first try
|
| 114 |
+
r3q1: {
|
| 115 |
+
id: 'r3q1',
|
| 116 |
+
question: 'Top 5 sellers by total revenue',
|
| 117 |
attempts: [
|
| 118 |
+
{ 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,
|
| 119 |
+
rows: [{ seller: 'TechStore Pro', revenue: 12840 }, { seller: 'StyleHub', revenue: 9320 }, { seller: 'GadgetWorld', revenue: 8750 }] },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
],
|
| 121 |
},
|
| 122 |
+
r3q2: {
|
| 123 |
+
id: 'r3q2',
|
| 124 |
+
question: 'Monthly revenue for the last 6 months',
|
| 125 |
attempts: [
|
| 126 |
+
{ 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,
|
| 127 |
+
rows: [{ month: '2024-11', revenue: 24180 }, { month: '2024-10', revenue: 21340 }, { month: '2024-09', revenue: 19800 }] },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
],
|
| 129 |
},
|
| 130 |
}
|
| 131 |
|
| 132 |
+
const ROUND_1 = ['r1q1', 'r1q2']
|
| 133 |
+
const ROUND_2 = ['r2q1', 'r2q2']
|
| 134 |
+
const ROUND_3 = ['r3q1', 'r3q2']
|
| 135 |
+
|
| 136 |
+
// βββ GitHub-style diff βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 137 |
|
| 138 |
+
interface DiffLine { type: 'add' | 'remove' | 'same'; text: string }
|
| 139 |
|
| 140 |
+
function diffPrompts(fromIdx: number, toIdx: number): DiffLine[] {
|
| 141 |
+
const oldLines = PROMPTS[fromIdx].split('\n')
|
| 142 |
+
const newLines = PROMPTS[toIdx].split('\n')
|
| 143 |
+
const result: DiffLine[] = []
|
| 144 |
|
| 145 |
+
// Simple patience diff: lines in both = same, only in old = remove, only in new = add
|
| 146 |
+
const oldSet = new Set(oldLines)
|
| 147 |
+
const newSet = new Set(newLines)
|
| 148 |
+
|
| 149 |
+
// Walk new lines, carrying old-only lines before matching
|
| 150 |
+
let oi = 0
|
| 151 |
+
for (let ni = 0; ni < newLines.length; ni++) {
|
| 152 |
+
const line = newLines[ni]
|
| 153 |
+
if (oldSet.has(line)) {
|
| 154 |
+
// Flush any old-only lines before this match
|
| 155 |
+
while (oi < oldLines.length && oldLines[oi] !== line) {
|
| 156 |
+
if (!newSet.has(oldLines[oi])) result.push({ type: 'remove', text: oldLines[oi] })
|
| 157 |
+
oi++
|
| 158 |
+
}
|
| 159 |
+
result.push({ type: 'same', text: line })
|
| 160 |
+
oi++
|
| 161 |
+
} else {
|
| 162 |
+
result.push({ type: 'add', text: line })
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
// Remaining old-only
|
| 166 |
+
while (oi < oldLines.length) {
|
| 167 |
+
if (!newSet.has(oldLines[oi])) result.push({ type: 'remove', text: oldLines[oi] })
|
| 168 |
+
oi++
|
| 169 |
+
}
|
| 170 |
+
return result
|
| 171 |
}
|
| 172 |
|
| 173 |
+
function GithubDiff({ fromIdx, toIdx }: { fromIdx: number; toIdx: number }) {
|
| 174 |
+
const lines = diffPrompts(fromIdx, toIdx)
|
| 175 |
+
const added = lines.filter((l) => l.type === 'add').length
|
| 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]">
|
| 185 |
+
<span className="text-green-400 font-bold">+{added}</span>
|
| 186 |
+
<span className="text-red-400 font-bold">β{removed}</span>
|
| 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' ? '-' : ' '
|
| 194 |
+
const color = line.type === 'add' ? 'text-green-300' : line.type === 'remove' ? 'text-red-300' : 'text-gray-500'
|
| 195 |
+
return (
|
| 196 |
+
<div key={i} className={`flex items-start gap-2 px-3 py-0.5 leading-relaxed ${bg}`}>
|
| 197 |
+
<span className={`shrink-0 w-3 ${color} font-bold select-none`}>{prefix}</span>
|
| 198 |
+
<span className={color}>{line.text || <span className="opacity-0">β</span>}</span>
|
| 199 |
+
</div>
|
| 200 |
+
)
|
| 201 |
+
})}
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
)
|
| 205 |
+
}
|
| 206 |
|
| 207 |
+
// βββ Reward chart ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 208 |
+
|
| 209 |
+
interface RewardPoint { step: number; reward: number }
|
| 210 |
+
|
| 211 |
+
function RewardChart({ points }: { points: RewardPoint[] }) {
|
| 212 |
+
if (points.length === 0) {
|
| 213 |
+
return (
|
| 214 |
+
<div className="flex flex-col items-center justify-center h-28 gap-1.5 text-gray-700">
|
| 215 |
+
<span className="text-[11px]">Rewards appear as agent runs</span>
|
| 216 |
+
</div>
|
| 217 |
+
)
|
| 218 |
+
}
|
| 219 |
+
return (
|
| 220 |
+
<ResponsiveContainer width="100%" height={110}>
|
| 221 |
+
<LineChart data={points} margin={{ top: 4, right: 8, bottom: 0, left: -20 }}>
|
| 222 |
+
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" />
|
| 223 |
+
<XAxis dataKey="step" tick={{ fontSize: 9, fill: '#6b7280' }} />
|
| 224 |
+
<YAxis domain={[-0.5, 1.1]} tick={{ fontSize: 9, fill: '#6b7280' }} />
|
| 225 |
+
<Tooltip
|
| 226 |
+
contentStyle={{ background: '#1a1a2e', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, fontSize: 11 }}
|
| 227 |
+
labelStyle={{ color: '#9ca3af' }}
|
| 228 |
+
itemStyle={{ color: '#f97316' }}
|
| 229 |
+
formatter={(v: number) => [v.toFixed(2), 'reward']}
|
| 230 |
+
labelFormatter={(l) => `Step ${l}`}
|
| 231 |
+
/>
|
| 232 |
+
<ReferenceLine y={0} stroke="#ffffff18" strokeDasharray="3 3" />
|
| 233 |
+
<Line
|
| 234 |
+
type="monotone" dataKey="reward" name="Reward"
|
| 235 |
+
stroke="#f97316" strokeWidth={2}
|
| 236 |
+
dot={(props) => {
|
| 237 |
+
const { cx, cy, payload } = props
|
| 238 |
+
const color = (payload as RewardPoint).reward >= 0 ? '#22c55e' : '#ef4444'
|
| 239 |
+
return <circle key={`dot-${(payload as RewardPoint).step}`} cx={cx} cy={cy} r={3} fill={color} stroke="none" />
|
| 240 |
+
}}
|
| 241 |
+
activeDot={{ r: 4, fill: '#f97316' }}
|
| 242 |
+
isAnimationActive={false}
|
| 243 |
+
/>
|
| 244 |
+
</LineChart>
|
| 245 |
+
</ResponsiveContainer>
|
| 246 |
+
)
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
// βββ Right panel βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 250 |
+
|
| 251 |
+
interface RightPanelProps {
|
| 252 |
+
gen: number
|
| 253 |
+
score: number
|
| 254 |
+
rewardPoints: RewardPoint[]
|
| 255 |
+
latestDiff: { from: number; to: number } | null
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
function RightPanel({ gen, score, rewardPoints, latestDiff }: RightPanelProps) {
|
| 259 |
+
return (
|
| 260 |
+
<div className="flex flex-col gap-4 px-4 py-4 overflow-y-auto h-full">
|
| 261 |
+
{/* Gen badge + score */}
|
| 262 |
+
<div className="flex items-center justify-between">
|
| 263 |
+
<div className="flex items-center gap-1.5">
|
| 264 |
+
<Sparkles size={12} className="text-violet-400" />
|
| 265 |
+
<span className="text-[11px] font-semibold text-gray-400 uppercase tracking-wider">GEPA</span>
|
| 266 |
+
</div>
|
| 267 |
+
<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">
|
| 268 |
+
Gen {gen}
|
| 269 |
+
</span>
|
| 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)' }}
|
| 284 |
+
animate={{ width: `${score * 100}%` }}
|
| 285 |
+
transition={{ duration: 0.9, ease: 'easeOut' }}
|
| 286 |
+
/>
|
| 287 |
+
</div>
|
| 288 |
+
{/* Generation history */}
|
| 289 |
+
<div className="mt-2.5 flex flex-col gap-1">
|
| 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)}%
|
| 299 |
+
</span>
|
| 300 |
+
</div>
|
| 301 |
+
))}
|
| 302 |
+
</div>
|
| 303 |
+
</div>
|
| 304 |
|
| 305 |
+
{/* Reward chart */}
|
| 306 |
+
<div>
|
| 307 |
+
<div className="text-[10px] text-gray-600 font-semibold uppercase tracking-wider mb-2">
|
| 308 |
+
RL Reward per Step
|
| 309 |
+
</div>
|
| 310 |
+
<RewardChart points={rewardPoints} />
|
| 311 |
+
</div>
|
| 312 |
+
|
| 313 |
+
{/* GitHub diff β appears after each GEPA cycle */}
|
| 314 |
+
{latestDiff && (
|
| 315 |
+
<div>
|
| 316 |
+
<div className="flex items-center gap-1.5 mb-2">
|
| 317 |
+
<GitCommitHorizontal size={11} className="text-violet-400" />
|
| 318 |
+
<span className="text-[10px] text-gray-500 font-semibold uppercase tracking-wider">
|
| 319 |
+
Prompt diff β Gen {latestDiff.from} β {latestDiff.to}
|
| 320 |
+
</span>
|
| 321 |
+
</div>
|
| 322 |
+
<GithubDiff fromIdx={latestDiff.from} toIdx={latestDiff.to} />
|
| 323 |
+
</div>
|
| 324 |
+
)}
|
| 325 |
+
</div>
|
| 326 |
+
)
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
// βββ SQL highlighter βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 330 |
|
| 331 |
function HighlightSQL({ sql }: { sql: string }) {
|
| 332 |
+
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
|
| 333 |
const parts: React.ReactNode[] = []
|
| 334 |
+
let last = 0; let match: RegExpExecArray | null
|
| 335 |
+
const r = new RegExp(re.source, 'gi')
|
| 336 |
+
while ((match = r.exec(sql)) !== null) {
|
|
|
|
| 337 |
if (match.index > last) parts.push(<span key={`t${last}`}>{sql.slice(last, match.index)}</span>)
|
| 338 |
parts.push(<span key={`k${match.index}`} className="text-violet-300 font-semibold">{match[0]}</span>)
|
| 339 |
last = match.index + match[0].length
|
| 340 |
}
|
| 341 |
+
if (last < sql.length) parts.push(<span key="end">{sql.slice(last)}</span>)
|
| 342 |
return <>{parts}</>
|
| 343 |
}
|
| 344 |
|
| 345 |
+
// βββ Bubble types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 346 |
+
|
| 347 |
+
interface BaseBubble { id: string }
|
| 348 |
+
interface UserBubble extends BaseBubble { type: 'user'; text: string }
|
| 349 |
+
interface ThinkingBubble extends BaseBubble { type: 'thinking'; label: string }
|
| 350 |
+
interface SqlStreamBubble extends BaseBubble { type: 'sql_stream'; sql: string; attempt: number }
|
| 351 |
+
interface SqlErrBubble extends BaseBubble { type: 'sql_err'; sql: string; error: string; errorClass: string; rlAction: string; reward: number; attempt: number }
|
| 352 |
+
interface SqlOkBubble extends BaseBubble { type: 'sql_ok'; sql: string; rows: Record<string, string | number>[]; reward: number; attempt: number; firstTry: boolean }
|
| 353 |
+
interface GepaBubble extends BaseBubble { type: 'gepa'; fromGen: number; toGen: number; scoreFrom: number; scoreTo: number }
|
| 354 |
+
interface GroupBubble extends BaseBubble { type: 'group'; question: string; success: boolean; attempts: number; children: BubbleData[] }
|
| 355 |
+
type BubbleData = UserBubble | ThinkingBubble | SqlStreamBubble | SqlErrBubble | SqlOkBubble | GepaBubble | GroupBubble
|
| 356 |
+
|
| 357 |
+
let _id = 0
|
| 358 |
+
const uid = () => `b${++_id}`
|
| 359 |
+
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms))
|
| 360 |
+
|
| 361 |
+
// βββ Bubble renderer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 362 |
|
| 363 |
function Bubble({ b }: { b: BubbleData }) {
|
| 364 |
const [open, setOpen] = useState(false)
|
| 365 |
|
| 366 |
if (b.type === 'user') return (
|
| 367 |
<div className="flex justify-end">
|
| 368 |
+
<div className="max-w-[78%] bg-violet-600/20 border border-violet-500/25 rounded-2xl rounded-tr-sm px-4 py-2.5">
|
| 369 |
<p className="text-sm text-white">{b.text}</p>
|
| 370 |
</div>
|
| 371 |
</div>
|
| 372 |
)
|
| 373 |
|
| 374 |
if (b.type === 'thinking') return (
|
| 375 |
+
<div className="flex items-center gap-2 px-1 text-xs text-gray-500">
|
| 376 |
<Loader2 size={11} className="animate-spin text-violet-400 shrink-0" />
|
| 377 |
{b.label}
|
| 378 |
</div>
|
|
|
|
| 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>
|
|
|
|
| 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>
|
| 406 |
+
<div className="flex items-center gap-2 flex-wrap bg-red-500/8 border border-red-500/20 rounded-xl px-3 py-2">
|
| 407 |
+
<XCircle size={11} className="text-red-400 shrink-0" />
|
| 408 |
+
<span className="text-xs text-red-300 flex-1">{b.error}</span>
|
| 409 |
+
<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>
|
| 410 |
+
<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">
|
| 411 |
+
<Zap size={8} />{b.rlAction}
|
| 412 |
+
</span>
|
| 413 |
+
<span className="text-[11px] font-bold text-red-400">{b.reward.toFixed(2)}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
</div>
|
| 415 |
</div>
|
| 416 |
)
|
|
|
|
| 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>
|
| 432 |
+
<div className="flex items-center gap-1.5 text-[10px] px-0.5">
|
| 433 |
<CheckCircle2 size={11} className="text-green-400" />
|
| 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 |
+
))}
|
| 444 |
+
</tr>
|
| 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 |
+
))}
|
| 454 |
+
</tbody>
|
| 455 |
+
</table>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
</div>
|
| 457 |
</div>
|
| 458 |
)
|
|
|
|
| 460 |
if (b.type === 'gepa') return (
|
| 461 |
<div className="border border-violet-500/25 rounded-2xl overflow-hidden bg-violet-500/5">
|
| 462 |
<div className="px-4 py-3 flex items-center gap-2 border-b border-violet-500/15">
|
| 463 |
+
<Sparkles size={12} className="text-violet-400" />
|
| 464 |
+
<span className="text-xs font-semibold text-violet-300">GEPA β System Prompt Evolved</span>
|
| 465 |
+
<span className="ml-auto text-[10px] text-violet-400/60">Gen {b.fromGen} β {b.toGen}</span>
|
| 466 |
</div>
|
| 467 |
+
<div className="px-4 py-3 flex items-center gap-6">
|
| 468 |
+
<div className="text-center">
|
| 469 |
+
<div className="text-[10px] text-gray-600 mb-0.5">Before</div>
|
| 470 |
+
<div className="text-xl font-bold text-orange-400">{(b.scoreFrom * 100).toFixed(0)}%</div>
|
| 471 |
</div>
|
| 472 |
+
<div className="flex-1 flex flex-col items-center">
|
| 473 |
+
<div className="text-xs text-violet-400 mb-1">β improved</div>
|
| 474 |
+
<div className="w-full h-0.5 bg-gradient-to-r from-orange-400 to-green-400 rounded-full" />
|
|
|
|
| 475 |
</div>
|
| 476 |
+
<div className="text-center">
|
| 477 |
+
<div className="text-[10px] text-gray-600 mb-0.5">After</div>
|
| 478 |
+
<div className="text-xl font-bold text-green-400">{(b.scoreTo * 100).toFixed(0)}%</div>
|
| 479 |
</div>
|
| 480 |
</div>
|
| 481 |
+
<div className="px-4 pb-3">
|
| 482 |
+
<div className="text-[10px] text-gray-600 mb-1.5 flex items-center gap-1.5">
|
| 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>
|
| 490 |
+
<span className={line.type === 'add' ? 'text-green-300' : 'text-red-300'}>{line.text}</span>
|
| 491 |
+
</div>
|
| 492 |
+
))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
</div>
|
| 494 |
</div>
|
| 495 |
+
</div>
|
| 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>
|
| 518 |
+
)}
|
| 519 |
+
</AnimatePresence>
|
|
|
|
|
|
|
|
|
|
| 520 |
</div>
|
| 521 |
)
|
|
|
|
| 522 |
|
| 523 |
+
return null
|
|
|
|
|
|
|
|
|
|
| 524 |
}
|
| 525 |
|
| 526 |
+
// βββ DemoMode ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 527 |
+
|
| 528 |
+
export function DemoMode({ onClose }: { onClose: () => void }) {
|
| 529 |
const [bubbles, setBubbles] = useState<BubbleData[]>([])
|
| 530 |
+
const [appState, setAppState] = useState<'idle' | 'running' | 'done'>('idle')
|
| 531 |
const [gen, setGen] = useState(0)
|
| 532 |
const [score, setScore] = useState(SCORES[0])
|
| 533 |
+
const [rewardPoints, setRewardPoints] = useState<RewardPoint[]>([])
|
| 534 |
+
const [latestDiff, setLatestDiff] = useState<{ from: number; to: number } | null>(null)
|
| 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)
|
| 541 |
}, [])
|
| 542 |
|
| 543 |
+
const push = useCallback((b: BubbleData) => setBubbles((p) => [...p, b]), [])
|
|
|
|
|
|
|
| 544 |
|
| 545 |
+
const addReward = useCallback((reward: number) => {
|
| 546 |
+
stepRef.current += 1
|
| 547 |
+
setRewardPoints((p) => [...p, { step: stepRef.current, reward }])
|
| 548 |
}, [])
|
| 549 |
|
|
|
|
| 550 |
const typeUser = useCallback(async (text: string) => {
|
| 551 |
const id = uid()
|
| 552 |
push({ id, type: 'user', text: '' })
|
| 553 |
for (let i = 1; i <= text.length; i++) {
|
| 554 |
if (cancel.current) return
|
| 555 |
+
setBubbles((p) => p.map((b) => b.id === id ? { ...b, text: text.slice(0, i) } : b))
|
| 556 |
+
await sleep(32 + Math.random() * 22)
|
| 557 |
}
|
| 558 |
+
scroll(); await sleep(300)
|
| 559 |
+
}, [push, scroll])
|
|
|
|
| 560 |
|
|
|
|
| 561 |
const streamSQL = useCallback(async (sql: string, attempt: number) => {
|
| 562 |
const id = uid()
|
| 563 |
push({ id, type: 'sql_stream', sql: '', attempt })
|
|
|
|
| 565 |
for (const line of sql.split('\n')) {
|
| 566 |
if (cancel.current) return
|
| 567 |
built += (built ? '\n' : '') + line
|
| 568 |
+
setBubbles((p) => p.map((b) => b.id === id ? { ...b, sql: built } : b))
|
| 569 |
+
await sleep(85 + Math.random() * 70)
|
| 570 |
}
|
| 571 |
+
scroll(); await sleep(200)
|
| 572 |
+
setBubbles((p) => p.filter((b) => b.id !== id))
|
| 573 |
+
}, [push, scroll])
|
|
|
|
|
|
|
| 574 |
|
|
|
|
| 575 |
const playQuery = useCallback(async (def: QueryDef): Promise<BubbleData[]> => {
|
| 576 |
const children: BubbleData[] = []
|
|
|
|
| 577 |
await typeUser(def.question)
|
| 578 |
|
| 579 |
for (let i = 0; i < def.attempts.length; i++) {
|
| 580 |
if (cancel.current) return children
|
| 581 |
const att = def.attempts[i]
|
|
|
|
| 582 |
|
| 583 |
+
// Thinking
|
| 584 |
+
const thId = uid()
|
| 585 |
+
push({ id: thId, type: 'thinking', label: i === 0 ? 'Generating SQLβ¦' : 'Applying repair strategyβ¦' })
|
| 586 |
+
scroll(); await sleep(750)
|
|
|
|
| 587 |
if (cancel.current) return children
|
| 588 |
+
setBubbles((p) => p.filter((b) => b.id !== thId))
|
| 589 |
|
| 590 |
+
await streamSQL(att.sql, i + 1)
|
| 591 |
if (cancel.current) return children
|
| 592 |
|
| 593 |
+
addReward(att.reward)
|
| 594 |
+
|
| 595 |
if (att.error) {
|
| 596 |
+
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 }
|
| 597 |
+
push(eb); children.push(eb); scroll(); await sleep(900)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 598 |
} else {
|
| 599 |
+
const ob: SqlOkBubble = { id: uid(), type: 'sql_ok', sql: att.sql, rows: att.rows ?? [], reward: att.reward, attempt: i + 1, firstTry: i === 0 }
|
| 600 |
+
push(ob); children.push(ob); scroll(); await sleep(1100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 601 |
}
|
| 602 |
}
|
|
|
|
| 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) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 620 |
if (cancel.current) return
|
| 621 |
+
setBubbles((p) => {
|
| 622 |
+
const last = p[p.length - 1]
|
| 623 |
+
if (last?.type === 'thinking') return [...p.slice(0, -1), { id: last.id, type: 'thinking' as const, label }]
|
| 624 |
+
return [...p, { id: uid(), type: 'thinking' as const, label }]
|
|
|
|
|
|
|
| 625 |
})
|
| 626 |
+
scroll(); await sleep(1050)
|
|
|
|
| 627 |
}
|
| 628 |
+
setBubbles((p) => p.filter((b) => b.type !== 'thinking'))
|
|
|
|
| 629 |
|
| 630 |
// Animate score
|
| 631 |
+
const from = SCORES[fromGen], to = SCORES[toGen]
|
| 632 |
+
for (let i = 0; i <= 45; i++) {
|
|
|
|
| 633 |
if (cancel.current) return
|
| 634 |
+
setScore(from + (to - from) * (i / 45))
|
| 635 |
+
await sleep(18)
|
| 636 |
}
|
| 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)
|
|
|
|
|
|
|
| 649 |
|
|
|
|
| 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 |
|
| 678 |
return (
|
| 679 |
<motion.div
|
| 680 |
+
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
|
|
|
|
|
|
| 681 |
className="fixed inset-0 z-[100] flex flex-col"
|
| 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" />
|
| 689 |
+
<span className="text-[11px] font-semibold text-violet-300">Demo</span>
|
| 690 |
</div>
|
| 691 |
+
<span className="text-xs text-gray-600 hidden sm:block">
|
| 692 |
+
RL repair loop Β· GEPA prompt evolution Β· 42% β 91%
|
| 693 |
</span>
|
| 694 |
</div>
|
| 695 |
+
<button onClick={onClose} className="p-1.5 rounded-lg hover:bg-white/5 text-gray-500 hover:text-white transition-colors">
|
| 696 |
+
<X size={16} />
|
| 697 |
+
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 698 |
</div>
|
| 699 |
|
| 700 |
{/* Body */}
|
| 701 |
<div className="flex flex-1 overflow-hidden">
|
| 702 |
+
{/* Chat */}
|
| 703 |
<div className="flex-1 flex flex-col overflow-hidden">
|
| 704 |
<div className="flex-1 overflow-y-auto px-4 py-4">
|
| 705 |
{appState === 'idle' ? (
|
| 706 |
+
<div className="flex flex-col items-center justify-center h-full gap-5 text-center px-6">
|
| 707 |
+
<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)' }}>
|
| 708 |
+
<Play size={24} className="text-white ml-0.5" fill="currentColor" />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
</div>
|
| 710 |
<div>
|
| 711 |
+
<h2 className="text-base font-bold text-white mb-1.5">SQL Agent OpenEnv β Live Demo</h2>
|
| 712 |
+
<p className="text-sm text-gray-500 max-w-sm leading-relaxed">
|
| 713 |
+
Watch the agent fail, self-repair using LinUCB, then improve through two GEPA prompt evolution cycles.
|
|
|
|
| 714 |
</p>
|
| 715 |
</div>
|
| 716 |
+
<div className="flex flex-col gap-1.5 text-xs text-gray-600 max-w-xs text-left">
|
| 717 |
+
{['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) => (
|
| 718 |
+
<div key={i} className="flex items-start gap-2">
|
| 719 |
+
<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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
{s}
|
| 721 |
</div>
|
| 722 |
))}
|
| 723 |
</div>
|
| 724 |
<button
|
| 725 |
+
onClick={() => void autoPlay()}
|
| 726 |
+
className="flex items-center gap-2 px-6 py-3 rounded-2xl font-semibold text-sm text-white active:scale-95 transition-transform"
|
| 727 |
style={{ background: 'linear-gradient(135deg,#7c3aed,#2563eb)', boxShadow: '0 8px 24px rgba(124,58,237,0.4)' }}
|
| 728 |
>
|
| 729 |
+
<Play size={13} fill="currentColor" />
|
| 730 |
Start Demo
|
| 731 |
</button>
|
| 732 |
</div>
|
|
|
|
| 734 |
<div className="flex flex-col gap-4 max-w-2xl mx-auto">
|
| 735 |
<AnimatePresence initial={false}>
|
| 736 |
{bubbles.map((b) => (
|
| 737 |
+
<motion.div key={b.id} initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.18 }}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
<Bubble b={b} />
|
| 739 |
</motion.div>
|
| 740 |
))}
|
| 741 |
</AnimatePresence>
|
| 742 |
+
|
| 743 |
{appState === 'done' && (
|
| 744 |
+
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="border border-green-500/25 rounded-2xl p-5 bg-green-500/5 text-center mt-2">
|
| 745 |
+
<div className="text-3xl font-bold text-green-400 mb-1 tabular-nums">91%</div>
|
| 746 |
+
<div className="text-sm font-semibold text-white mb-1">Demo complete</div>
|
| 747 |
+
<div className="text-xs text-gray-500 max-w-xs mx-auto leading-relaxed">
|
| 748 |
+
Agent improved from 42% β 91% accuracy through RL-driven repair strategies and two GEPA prompt evolution cycles.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 749 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
</motion.div>
|
| 751 |
)}
|
| 752 |
<div ref={bottomRef} />
|
|
|
|
| 755 |
</div>
|
| 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>
|
| 763 |
</motion.div>
|