krishpotanwar commited on
Commit
f86ef5b
·
1 Parent(s): 0376765

feat(frontend): add SQL command center UI

Browse files
.dockerignore CHANGED
@@ -7,6 +7,8 @@ __pycache__
7
  .pytest_cache
8
  .ruff_cache
9
  .mypy_cache
 
 
10
  tests/
11
  *.egg-info
12
  build/
 
7
  .pytest_cache
8
  .ruff_cache
9
  .mypy_cache
10
+ frontend/node_modules
11
+ frontend/dist
12
  tests/
13
  *.egg-info
14
  build/
Dockerfile CHANGED
@@ -2,9 +2,13 @@ FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
5
- # System deps (curl for healthchecks)
6
  RUN apt-get update && apt-get install -y --no-install-recommends \
7
  curl \
 
 
 
 
8
  && rm -rf /var/lib/apt/lists/*
9
 
10
  # Copy & install Python deps first for layer caching
@@ -14,6 +18,9 @@ RUN pip install --no-cache-dir -r requirements.txt
14
  # Copy application
15
  COPY . .
16
 
 
 
 
17
  # Install package so [project.scripts] is callable
18
  RUN pip install --no-cache-dir -e .
19
 
 
2
 
3
  WORKDIR /app
4
 
5
+ # System deps + Node.js for frontend build
6
  RUN apt-get update && apt-get install -y --no-install-recommends \
7
  curl \
8
+ gnupg \
9
+ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
10
+ && apt-get install -y --no-install-recommends \
11
+ nodejs \
12
  && rm -rf /var/lib/apt/lists/*
13
 
14
  # Copy & install Python deps first for layer caching
 
18
  # Copy application
19
  COPY . .
20
 
21
+ # Build the copied React frontend so FastAPI can serve it at /
22
+ RUN cd frontend && npm ci && npm run build
23
+
24
  # Install package so [project.scripts] is callable
25
  RUN pip install --no-cache-dir -e .
26
 
frontend/.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .vercel
5
+ .env*.local
frontend/README.md ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DisasterMan Frontend
2
+
3
+ React + TypeScript + Vite frontend for the DisasterMan disaster-relief simulator.
4
+
5
+ ## What It Does
6
+
7
+ - Loads task metadata from the FastAPI backend
8
+ - Replays full simulation runs step by step
9
+ - Compares `random`, `greedy`, and `ai_4stage` agents side by side
10
+ - Shows map state, resources, scores, and agent reasoning
11
+
12
+ ## Local Development
13
+
14
+ From this `frontend/` directory:
15
+
16
+ ```bash
17
+ npm install
18
+ npm run dev
19
+ ```
20
+
21
+ Default API behavior:
22
+
23
+ - Development: `http://localhost:7860`
24
+ - Production: `/api` proxy
25
+ - Override: set `VITE_API_URL`
26
+
27
+ ## Environment Variables
28
+
29
+ ```bash
30
+ VITE_API_URL=http://localhost:7860
31
+ ```
32
+
33
+ Use `VITE_API_URL` when you want the frontend to talk directly to a backend instead of using the production proxy rewrite.
34
+
35
+ ## Build
36
+
37
+ ```bash
38
+ npm run build
39
+ ```
40
+
41
+ ## Notes
42
+
43
+ - The compare view only includes the 4-stage AI agent when the backend has `GROQ_API_KEY` or `OPENAI_API_KEY` configured.
44
+ - Production rewrites for Vercel live in `vercel.json`.
frontend/eslint.config.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
frontend/index.html ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>DisasterMan</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ <script type="text/javascript">
13
+ (function(d, t) {
14
+ var v = d.createElement(t), s = d.getElementsByTagName(t)[0];
15
+ v.onload = function() {
16
+ window.voiceflow.chat.load({
17
+ verify: { projectID: 'YOUR_PROJECT_ID_HERE' },
18
+ url: 'https://general-runtime.voiceflow.com',
19
+ versionID: 'production'
20
+ });
21
+ }
22
+ v.src = "https://cdn.voiceflow.com/widget/bundle.mjs"; v.type = "text/javascript"; s.parentNode.insertBefore(v, s);
23
+ })(document, 'script');
24
+ </script>
25
+ </body>
26
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@tailwindcss/vite": "^4.2.2",
14
+ "clsx": "^2.1.1",
15
+ "framer-motion": "^12.38.0",
16
+ "leaflet": "^1.9.4",
17
+ "lucide-react": "^1.7.0",
18
+ "react": "^19.2.4",
19
+ "react-dom": "^19.2.4",
20
+ "react-leaflet": "^5.0.0",
21
+ "recharts": "^3.8.1",
22
+ "tailwindcss": "^4.2.2",
23
+ "zustand": "^5.0.8"
24
+ },
25
+ "devDependencies": {
26
+ "@eslint/js": "^9.39.4",
27
+ "@types/leaflet": "^1.9.20",
28
+ "@types/node": "^24.12.0",
29
+ "@types/react": "^19.2.14",
30
+ "@types/react-dom": "^19.2.3",
31
+ "@vitejs/plugin-react": "^6.0.1",
32
+ "eslint": "^9.39.4",
33
+ "eslint-plugin-react-hooks": "^7.0.1",
34
+ "eslint-plugin-react-refresh": "^0.5.2",
35
+ "globals": "^17.4.0",
36
+ "typescript": "~5.9.3",
37
+ "typescript-eslint": "^8.57.0",
38
+ "vite": "^8.0.1"
39
+ }
40
+ }
frontend/public/favicon.svg ADDED
frontend/public/icons.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .counter {
2
+ font-size: 16px;
3
+ padding: 5px 10px;
4
+ border-radius: 5px;
5
+ color: var(--accent);
6
+ background: var(--accent-bg);
7
+ border: 2px solid transparent;
8
+ transition: border-color 0.3s;
9
+ margin-bottom: 24px;
10
+
11
+ &:hover {
12
+ border-color: var(--accent-border);
13
+ }
14
+ &:focus-visible {
15
+ outline: 2px solid var(--accent);
16
+ outline-offset: 2px;
17
+ }
18
+ }
19
+
20
+ .hero {
21
+ position: relative;
22
+
23
+ .base,
24
+ .framework,
25
+ .vite {
26
+ inset-inline: 0;
27
+ margin: 0 auto;
28
+ }
29
+
30
+ .base {
31
+ width: 170px;
32
+ position: relative;
33
+ z-index: 0;
34
+ }
35
+
36
+ .framework,
37
+ .vite {
38
+ position: absolute;
39
+ }
40
+
41
+ .framework {
42
+ z-index: 1;
43
+ top: 34px;
44
+ height: 28px;
45
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
46
+ scale(1.4);
47
+ }
48
+
49
+ .vite {
50
+ z-index: 0;
51
+ top: 107px;
52
+ height: 26px;
53
+ width: auto;
54
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
55
+ scale(0.8);
56
+ }
57
+ }
58
+
59
+ #center {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 25px;
63
+ place-content: center;
64
+ place-items: center;
65
+ flex-grow: 1;
66
+
67
+ @media (max-width: 1024px) {
68
+ padding: 32px 20px 24px;
69
+ gap: 18px;
70
+ }
71
+ }
72
+
73
+ #next-steps {
74
+ display: flex;
75
+ border-top: 1px solid var(--border);
76
+ text-align: left;
77
+
78
+ & > div {
79
+ flex: 1 1 0;
80
+ padding: 32px;
81
+ @media (max-width: 1024px) {
82
+ padding: 24px 20px;
83
+ }
84
+ }
85
+
86
+ .icon {
87
+ margin-bottom: 16px;
88
+ width: 22px;
89
+ height: 22px;
90
+ }
91
+
92
+ @media (max-width: 1024px) {
93
+ flex-direction: column;
94
+ text-align: center;
95
+ }
96
+ }
97
+
98
+ #docs {
99
+ border-right: 1px solid var(--border);
100
+
101
+ @media (max-width: 1024px) {
102
+ border-right: none;
103
+ border-bottom: 1px solid var(--border);
104
+ }
105
+ }
106
+
107
+ #next-steps ul {
108
+ list-style: none;
109
+ padding: 0;
110
+ display: flex;
111
+ gap: 8px;
112
+ margin: 32px 0 0;
113
+
114
+ .logo {
115
+ height: 18px;
116
+ }
117
+
118
+ a {
119
+ color: var(--text-h);
120
+ font-size: 16px;
121
+ border-radius: 6px;
122
+ background: var(--social-bg);
123
+ display: flex;
124
+ padding: 6px 12px;
125
+ align-items: center;
126
+ gap: 8px;
127
+ text-decoration: none;
128
+ transition: box-shadow 0.3s;
129
+
130
+ &:hover {
131
+ box-shadow: var(--shadow);
132
+ }
133
+ .button-icon {
134
+ height: 18px;
135
+ width: 18px;
136
+ }
137
+ }
138
+
139
+ @media (max-width: 1024px) {
140
+ margin-top: 20px;
141
+ flex-wrap: wrap;
142
+ justify-content: center;
143
+
144
+ li {
145
+ flex: 1 1 calc(50% - 8px);
146
+ }
147
+
148
+ a {
149
+ width: 100%;
150
+ justify-content: center;
151
+ box-sizing: border-box;
152
+ }
153
+ }
154
+ }
155
+
156
+ #spacer {
157
+ height: 88px;
158
+ border-top: 1px solid var(--border);
159
+ @media (max-width: 1024px) {
160
+ height: 48px;
161
+ }
162
+ }
163
+
164
+ .ticks {
165
+ position: relative;
166
+ width: 100%;
167
+
168
+ &::before,
169
+ &::after {
170
+ content: '';
171
+ position: absolute;
172
+ top: -4.5px;
173
+ border: 5px solid transparent;
174
+ }
175
+
176
+ &::before {
177
+ left: 0;
178
+ border-left-color: var(--border);
179
+ }
180
+ &::after {
181
+ right: 0;
182
+ border-right-color: var(--border);
183
+ }
184
+ }
frontend/src/App.tsx ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { startTransition, useEffect, useMemo, useState } from 'react'
2
+ import { ApiError, fetchHealth, fetchTasks, getApiInfo } from './api/client'
3
+ import { ApiOpsTab } from './components/ApiOpsTab'
4
+ import { BaselineArenaTab } from './components/BaselineArenaTab'
5
+ import { ProtocolTab } from './components/ProtocolTab'
6
+ import { QueryWorkbenchTab } from './components/QueryWorkbenchTab'
7
+ import { SqlLandingPage } from './components/SqlLandingPage'
8
+ import { TaskAtlasTab } from './components/TaskAtlasTab'
9
+ import type { HealthStatus, TaskInfo } from './types'
10
+
11
+ type Tab = 'lab' | 'atlas' | 'baseline' | 'protocol' | 'ops'
12
+
13
+ const TAB_LABELS: Record<Tab, string> = {
14
+ lab: 'QUERY LAB',
15
+ atlas: 'TASK ATLAS',
16
+ baseline: 'BASELINE ARENA',
17
+ protocol: 'AGENT PROTOCOL',
18
+ ops: 'API OPS',
19
+ }
20
+
21
+ const TAB_ICONS: Record<Tab, string> = {
22
+ lab: '⌘',
23
+ atlas: '◫',
24
+ baseline: '▣',
25
+ protocol: '⟟',
26
+ ops: '◌',
27
+ }
28
+
29
+ export default function App() {
30
+ const [showLanding, setShowLanding] = useState(true)
31
+ const [tab, setTab] = useState<Tab>('lab')
32
+ const [tasks, setTasks] = useState<TaskInfo[]>([])
33
+ const [selectedTask, setSelectedTask] = useState('task_1')
34
+ const [tasksError, setTasksError] = useState<string | null>(null)
35
+ const [health, setHealth] = useState<HealthStatus>('loading')
36
+ const [healthPayload, setHealthPayload] = useState<string>('Loading…')
37
+ const apiInfo = getApiInfo()
38
+
39
+ useEffect(() => {
40
+ void fetchHealth()
41
+ .then((payload) => {
42
+ setHealth(payload.status === 'ok' ? 'ok' : 'error')
43
+ setHealthPayload(JSON.stringify(payload))
44
+ })
45
+ .catch((error: unknown) => {
46
+ setHealth('error')
47
+ setHealthPayload(error instanceof Error ? error.message : String(error))
48
+ })
49
+ }, [])
50
+
51
+ useEffect(() => {
52
+ void fetchTasks()
53
+ .then((nextTasks) => {
54
+ setTasks(nextTasks)
55
+ setTasksError(null)
56
+ if (nextTasks.length > 0) {
57
+ startTransition(() => {
58
+ setSelectedTask((current) =>
59
+ nextTasks.some((task) => task.id === current) ? current : nextTasks[0].id,
60
+ )
61
+ })
62
+ }
63
+ })
64
+ .catch((error: unknown) => {
65
+ if (error instanceof ApiError) {
66
+ setTasksError(`${error.message} (url: ${error.url})`)
67
+ return
68
+ }
69
+ setTasksError(String(error))
70
+ })
71
+ }, [])
72
+
73
+ const selectedTaskInfo = useMemo(
74
+ () => tasks.find((task) => task.id === selectedTask),
75
+ [selectedTask, tasks],
76
+ )
77
+
78
+ const tabClass = (value: Tab) =>
79
+ `px-5 py-2.5 text-sm font-medium rounded-lg transition-colors whitespace-nowrap ${
80
+ tab === value
81
+ ? 'bg-zinc-800 text-white'
82
+ : 'text-zinc-500 hover:text-zinc-300 hover:bg-zinc-900/50'
83
+ }`
84
+
85
+ if (showLanding) {
86
+ return <SqlLandingPage onLaunch={() => setShowLanding(false)} />
87
+ }
88
+
89
+ return (
90
+ <div className="min-h-screen text-white flex flex-col relative z-0">
91
+ <header className="border-b border-zinc-800/50 backdrop-blur-3xl bg-black/40 sticky top-0 z-50">
92
+ <div className="max-w-7xl mx-auto px-4 py-4 flex flex-wrap items-center justify-between gap-4">
93
+ <div className="flex items-center gap-3">
94
+ <div className="w-10 h-10 rounded-full bg-emerald-500/15 border border-emerald-400/30 flex items-center justify-center text-xl shadow-[0_0_15px_rgba(16,185,129,0.35)]">
95
+
96
+ </div>
97
+ <div>
98
+ <h1 className="text-xl font-bold leading-tight tracking-wider bg-gradient-to-r from-emerald-300 via-cyan-300 to-amber-300 bg-clip-text text-transparent drop-shadow-sm">
99
+ SQL REPAIR COMMAND
100
+ </h1>
101
+ <p className="text-[11px] text-zinc-400 uppercase tracking-widest mt-0.5">
102
+ OpenEnv Query Recovery Console
103
+ </p>
104
+ </div>
105
+ </div>
106
+
107
+ <div className="flex items-center gap-3 flex-wrap">
108
+ <div
109
+ className={`px-3 py-1.5 rounded-lg border text-xs font-semibold tracking-wide ${
110
+ health === 'ok'
111
+ ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-300'
112
+ : health === 'loading'
113
+ ? 'border-zinc-700 bg-zinc-900 text-zinc-300'
114
+ : 'border-rose-500/30 bg-rose-500/10 text-rose-300'
115
+ }`}
116
+ >
117
+ {health === 'ok' ? 'SPACE HEALTHY' : health === 'loading' ? 'CHECKING HEALTH' : 'HEALTH WARNING'}
118
+ </div>
119
+ <a
120
+ href="https://github.com/Krishpotanwar/sql-repair-env"
121
+ target="_blank"
122
+ rel="noopener noreferrer"
123
+ className="text-xs text-zinc-500 hover:text-white transition-colors px-3 py-1.5 border border-zinc-800 rounded-lg"
124
+ >
125
+ GitHub ↗
126
+ </a>
127
+ <a
128
+ href="/docs"
129
+ target="_blank"
130
+ rel="noopener noreferrer"
131
+ className="text-xs font-semibold bg-gradient-to-r from-emerald-600 to-cyan-600 hover:from-emerald-500 hover:to-cyan-500 text-white transition-all px-4 py-1.5 rounded-lg shadow-[0_0_15px_rgba(16,185,129,0.35)]"
132
+ >
133
+ API Docs ↗
134
+ </a>
135
+ </div>
136
+ </div>
137
+
138
+ <div className="max-w-7xl mx-auto px-4 pb-3 flex gap-2 overflow-x-auto">
139
+ {(Object.keys(TAB_LABELS) as Tab[]).map((value) => (
140
+ <button key={value} className={tabClass(value)} onClick={() => setTab(value)}>
141
+ <span className="mr-1.5">{TAB_ICONS[value]}</span>
142
+ {TAB_LABELS[value]}
143
+ </button>
144
+ ))}
145
+ </div>
146
+ </header>
147
+
148
+ <main className="max-w-7xl mx-auto px-4 py-6 w-full">
149
+ {tasksError && (
150
+ <div className="bg-red-950 border border-red-800 rounded-xl p-4 text-red-300 text-sm mb-6">
151
+ Could not connect to backend: {tasksError}
152
+ <br />
153
+ <span className="text-xs text-red-500">
154
+ API mode: {apiInfo.mode} | base: {apiInfo.base} | VITE_API_URL: {apiInfo.env}
155
+ </span>
156
+ </div>
157
+ )}
158
+
159
+ {!tasksError && tasks.length === 0 && (
160
+ <div className="glass-panel rounded-2xl px-6 py-10 text-zinc-300 text-sm flex items-center justify-center gap-3">
161
+ <span className="animate-spin text-lg">⚙</span>
162
+ Connecting to the SQL task grid…
163
+ </div>
164
+ )}
165
+
166
+ {tasks.length > 0 && (
167
+ <>
168
+ <div className="glass-panel rounded-2xl px-5 py-4 mb-6 flex flex-wrap items-center justify-between gap-3">
169
+ <div>
170
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500">Loaded Mission</p>
171
+ <p className="text-lg font-semibold text-zinc-100">
172
+ {selectedTaskInfo?.name ?? selectedTask}
173
+ </p>
174
+ </div>
175
+ <div className="flex items-center gap-3 flex-wrap text-sm">
176
+ <label className="text-zinc-400">Current task</label>
177
+ <select
178
+ value={selectedTask}
179
+ onChange={(event) => {
180
+ const nextValue = event.target.value
181
+ startTransition(() => setSelectedTask(nextValue))
182
+ }}
183
+ className="bg-zinc-950/70 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 focus:outline-none focus:border-emerald-500/50"
184
+ >
185
+ {tasks.map((task) => (
186
+ <option key={task.id} value={task.id}>
187
+ {task.name} ({task.difficulty})
188
+ </option>
189
+ ))}
190
+ </select>
191
+ <div className="text-xs text-zinc-500">
192
+ Live health payload: <span className="mono">{healthPayload}</span>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ {tab === 'lab' && (
198
+ <QueryWorkbenchTab
199
+ apiInfo={apiInfo}
200
+ selectedTask={selectedTask}
201
+ taskInfo={selectedTaskInfo}
202
+ />
203
+ )}
204
+ {tab === 'atlas' && (
205
+ <TaskAtlasTab
206
+ onSelectTask={(taskId) => {
207
+ startTransition(() => {
208
+ setSelectedTask(taskId)
209
+ setTab('lab')
210
+ })
211
+ }}
212
+ selectedTask={selectedTask}
213
+ tasks={tasks}
214
+ />
215
+ )}
216
+ {tab === 'baseline' && <BaselineArenaTab />}
217
+ {tab === 'protocol' && <ProtocolTab />}
218
+ {tab === 'ops' && (
219
+ <ApiOpsTab
220
+ apiInfo={apiInfo}
221
+ health={health}
222
+ selectedTask={selectedTask}
223
+ taskInfo={selectedTaskInfo}
224
+ />
225
+ )}
226
+ </>
227
+ )}
228
+ </main>
229
+ </div>
230
+ )
231
+ }
frontend/src/api/client.ts ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type {
2
+ ApiInfo,
3
+ BaselineResponse,
4
+ GraderResponse,
5
+ HealthResponse,
6
+ StepResponse,
7
+ TaskInfo,
8
+ TaskObservation,
9
+ } from '../types'
10
+
11
+ const PROD = import.meta.env.PROD
12
+ const devEnvBase = import.meta.env.VITE_API_URL?.trim()
13
+ const normalizedDevEnvBase = devEnvBase ? devEnvBase.replace(/\/+$/, '') : ''
14
+ const BASE = PROD ? '/api' : normalizedDevEnvBase || 'http://localhost:8000'
15
+
16
+ export class ApiError extends Error {
17
+ status: number
18
+ body: string
19
+ url: string
20
+
21
+ constructor(status: number, body: string, url: string, message: string) {
22
+ super(message)
23
+ this.name = 'ApiError'
24
+ this.status = status
25
+ this.body = body
26
+ this.url = url
27
+ }
28
+ }
29
+
30
+ function buildUrl(path: string): string {
31
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`
32
+ return `${BASE}${normalizedPath}`
33
+ }
34
+
35
+ async function request<T>(path: string, options?: RequestInit): Promise<T> {
36
+ const url = buildUrl(path)
37
+
38
+ try {
39
+ const res = await fetch(url, options)
40
+ if (!res.ok) {
41
+ const err = await res.text()
42
+ throw new ApiError(res.status, err, url, `API error ${res.status}: ${err}`)
43
+ }
44
+ return res.json() as Promise<T>
45
+ } catch (error) {
46
+ if (error instanceof ApiError) {
47
+ throw error
48
+ }
49
+ const message = error instanceof Error ? error.message : String(error)
50
+ throw new ApiError(0, message, url, `Network error: ${message}`)
51
+ }
52
+ }
53
+
54
+ export function getApiInfo(): ApiInfo {
55
+ return {
56
+ base: BASE,
57
+ mode: PROD ? 'proxy' : normalizedDevEnvBase ? 'direct' : 'local',
58
+ env: PROD ? '(disabled in production)' : normalizedDevEnvBase || '(not set)',
59
+ }
60
+ }
61
+
62
+ export async function fetchHealth(): Promise<HealthResponse> {
63
+ return request<HealthResponse>('/health')
64
+ }
65
+
66
+ export async function fetchTasks(): Promise<TaskInfo[]> {
67
+ const data = await request<{
68
+ tasks: string[]
69
+ details: Array<{ id: string; name: string; difficulty: string }>
70
+ }>('/tasks')
71
+ return data.details.map((task) => ({
72
+ id: task.id,
73
+ name: task.name,
74
+ difficulty: task.difficulty,
75
+ }))
76
+ }
77
+
78
+ export async function resetTask(taskId?: string): Promise<TaskObservation> {
79
+ return request<TaskObservation>('/reset', {
80
+ method: 'POST',
81
+ headers: { 'Content-Type': 'application/json' },
82
+ body: JSON.stringify(taskId ? { task_id: taskId } : {}),
83
+ })
84
+ }
85
+
86
+ export async function submitQuery(query: string): Promise<StepResponse> {
87
+ return request<StepResponse>('/step', {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/json' },
90
+ body: JSON.stringify({
91
+ action: {
92
+ action_type: 'submit_query',
93
+ query,
94
+ },
95
+ }),
96
+ })
97
+ }
98
+
99
+ export async function gradeTask(taskId?: string): Promise<GraderResponse> {
100
+ return request<GraderResponse>('/grader', {
101
+ method: 'POST',
102
+ headers: { 'Content-Type': 'application/json' },
103
+ body: JSON.stringify(taskId ? { task_id: taskId } : {}),
104
+ })
105
+ }
106
+
107
+ export async function fetchBaseline(taskIds?: string[]): Promise<BaselineResponse> {
108
+ return request<BaselineResponse>('/baseline', {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify(taskIds?.length ? { tasks: taskIds } : {}),
112
+ })
113
+ }
frontend/src/assets/hero.png ADDED
frontend/src/assets/react.svg ADDED
frontend/src/assets/vite.svg ADDED
frontend/src/components/ApiOpsTab.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TASK_CATALOG } from '../data/taskCatalog'
2
+ import type { ApiInfo, HealthStatus, TaskInfo } from '../types'
3
+ import { SectionCard } from './SectionCard'
4
+
5
+ interface ApiOpsTabProps {
6
+ health: HealthStatus
7
+ apiInfo: ApiInfo
8
+ selectedTask: string
9
+ taskInfo?: TaskInfo
10
+ }
11
+
12
+ export function ApiOpsTab({
13
+ health,
14
+ apiInfo,
15
+ selectedTask,
16
+ taskInfo,
17
+ }: ApiOpsTabProps) {
18
+ const canonicalQuery = TASK_CATALOG[selectedTask]?.canonicalQuery ?? 'SELECT 1'
19
+
20
+ return (
21
+ <div className="space-y-6">
22
+ <SectionCard
23
+ eyebrow="Operations"
24
+ title="API ops + live endpoints"
25
+ subtitle="The frontend talks to Winner through `/api/*` in production, while the backend still exposes the original canonical OpenEnv routes."
26
+ >
27
+ <div className="grid xl:grid-cols-[0.95fr_1.05fr] gap-6">
28
+ <div className="space-y-4">
29
+ <div className="rounded-2xl border border-zinc-800 bg-zinc-950/60 p-5">
30
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-3">Live status</p>
31
+ <div className="space-y-2 text-sm">
32
+ <p className="text-zinc-200">Health state: <span className="mono">{health}</span></p>
33
+ <p className="text-zinc-200">API mode: <span className="mono">{apiInfo.mode}</span></p>
34
+ <p className="text-zinc-200">Base path: <span className="mono">{apiInfo.base}</span></p>
35
+ <p className="text-zinc-200">Focused task: <span className="mono">{taskInfo?.name ?? selectedTask}</span></p>
36
+ </div>
37
+ </div>
38
+
39
+ <div className="rounded-2xl border border-zinc-800 bg-zinc-950/60 p-5">
40
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-3">Endpoints</p>
41
+ <div className="space-y-2 text-sm text-zinc-200 mono">
42
+ <div>GET /health</div>
43
+ <div>GET /tasks</div>
44
+ <div>POST /reset</div>
45
+ <div>POST /step</div>
46
+ <div>POST /grader</div>
47
+ <div>POST /baseline</div>
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <div className="space-y-4">
53
+ <div className="rounded-2xl border border-cyan-500/15 bg-cyan-500/8 p-5">
54
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-3">Canonical smoke test</p>
55
+ <pre className="mono text-xs leading-6 text-cyan-100 whitespace-pre-wrap">
56
+ {`curl -sS https://krishpotanwar-sql-repair-env.hf.space/health
57
+ curl -sS -X POST https://krishpotanwar-sql-repair-env.hf.space/reset \\
58
+ -H "Content-Type: application/json" \\
59
+ -d '{"task_id":"${selectedTask}"}'
60
+ curl -sS -X POST https://krishpotanwar-sql-repair-env.hf.space/step \\
61
+ -H "Content-Type: application/json" \\
62
+ -d '{"action":{"type":"submit_query","query":"${canonicalQuery}"}}'`}
63
+ </pre>
64
+ </div>
65
+
66
+ <div className="rounded-2xl border border-amber-500/15 bg-amber-500/8 p-5">
67
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-3">Docs</p>
68
+ <p className="text-sm text-zinc-200 leading-7">
69
+ FastAPI still exposes Swagger docs at <span className="mono">/docs</span>. The
70
+ HF App tab now serves a proper root UI at <span className="mono">/</span>, while
71
+ preserving the canonical OpenEnv API for the validator.
72
+ </p>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </SectionCard>
77
+ </div>
78
+ )
79
+ }
frontend/src/components/BaselineArenaTab.tsx ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import { fetchBaseline } from '../api/client'
3
+ import { TASK_CATALOG } from '../data/taskCatalog'
4
+ import type { BaselineResponse } from '../types'
5
+ import { SectionCard } from './SectionCard'
6
+
7
+ const IDEAL_SCORE = 0.99
8
+
9
+ export function BaselineArenaTab() {
10
+ const [baseline, setBaseline] = useState<BaselineResponse | null>(null)
11
+ const [loading, setLoading] = useState(true)
12
+ const [error, setError] = useState<string | null>(null)
13
+
14
+ const loadBaseline = async () => {
15
+ setLoading(true)
16
+ setError(null)
17
+ try {
18
+ const result = await fetchBaseline()
19
+ setBaseline(result)
20
+ } catch (loadError) {
21
+ setError(loadError instanceof Error ? loadError.message : String(loadError))
22
+ } finally {
23
+ setLoading(false)
24
+ }
25
+ }
26
+
27
+ useEffect(() => {
28
+ void loadBaseline()
29
+ }, [])
30
+
31
+ const entries = useMemo(
32
+ () =>
33
+ Object.entries(baseline?.scores ?? {}).map(([taskId, score]) => ({
34
+ taskId,
35
+ score,
36
+ delta: Number((IDEAL_SCORE - score).toFixed(4)),
37
+ })),
38
+ [baseline],
39
+ )
40
+
41
+ return (
42
+ <div className="space-y-6">
43
+ <SectionCard
44
+ eyebrow="Broken-query benchmark"
45
+ title="Baseline arena"
46
+ subtitle="The baseline endpoint runs the intentionally broken SQL for every task so we can inspect score separation."
47
+ actions={
48
+ <button
49
+ onClick={() => void loadBaseline()}
50
+ className="px-3 py-2 rounded-lg text-sm border border-zinc-800 bg-zinc-950/60 hover:bg-zinc-900/60"
51
+ >
52
+ Refresh Baseline
53
+ </button>
54
+ }
55
+ >
56
+ {error && (
57
+ <div className="rounded-xl border border-rose-500/20 bg-rose-500/10 px-4 py-4 text-sm text-rose-200 mb-4">
58
+ {error}
59
+ </div>
60
+ )}
61
+
62
+ {loading && (
63
+ <div className="text-sm text-zinc-500">Collecting live baseline scores…</div>
64
+ )}
65
+
66
+ {!loading && baseline && (
67
+ <div className="grid xl:grid-cols-[1.15fr_0.85fr] gap-6">
68
+ <div className="space-y-4">
69
+ {entries.map(({ taskId, score, delta }) => (
70
+ <div key={taskId} className="rounded-2xl border border-zinc-800 bg-zinc-950/60 p-4">
71
+ <div className="flex items-start justify-between gap-4 mb-3">
72
+ <div>
73
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500">{taskId}</p>
74
+ <h3 className="text-lg font-semibold text-zinc-100 mt-2">
75
+ {TASK_CATALOG[taskId]?.story}
76
+ </h3>
77
+ </div>
78
+ <div className="text-right">
79
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500">Score</p>
80
+ <p className="text-2xl font-semibold text-zinc-100 mt-2">{score.toFixed(4)}</p>
81
+ </div>
82
+ </div>
83
+
84
+ <div className="h-3 rounded-full bg-zinc-900 overflow-hidden">
85
+ <div
86
+ className="h-full rounded-full bg-gradient-to-r from-amber-400 to-emerald-400"
87
+ style={{ width: `${Math.max(6, score * 100)}%` }}
88
+ />
89
+ </div>
90
+
91
+ <p className="text-xs text-zinc-500 mt-3">
92
+ Gap to a near-perfect solve: <span className="mono">{delta.toFixed(4)}</span>
93
+ </p>
94
+ </div>
95
+ ))}
96
+ </div>
97
+
98
+ <div className="space-y-4">
99
+ <div className="rounded-2xl border border-zinc-800 bg-zinc-950/60 p-5">
100
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">Grader formula</p>
101
+ <ul className="space-y-2 text-sm text-zinc-200 leading-7">
102
+ <li>0.05 for submitting any query</li>
103
+ <li>0.25 when the last query executes without error</li>
104
+ <li>0.60 when the result matches the expected rows</li>
105
+ <li>0.09 efficiency bonus for faster perfect solves</li>
106
+ </ul>
107
+ </div>
108
+
109
+ <div className="rounded-2xl border border-cyan-500/15 bg-cyan-500/8 p-5">
110
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">Why this matters</p>
111
+ <p className="text-sm text-cyan-100 leading-7">
112
+ The scores stay strictly inside the open interval (0, 1), while still leaving
113
+ enough spread between broken-query baselines and reference solves for the
114
+ validator to tell them apart.
115
+ </p>
116
+ </div>
117
+
118
+ <div className="rounded-2xl border border-amber-500/15 bg-amber-500/8 p-5">
119
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">Max steps</p>
120
+ <p className="text-3xl font-semibold text-zinc-100">{baseline.max_steps}</p>
121
+ <p className="text-sm text-zinc-300 leading-7 mt-2">
122
+ Every task shares the same step ceiling, which keeps grading and agent runtime
123
+ predictable for the OpenEnv portal.
124
+ </p>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ )}
129
+ </SectionCard>
130
+ </div>
131
+ )
132
+ }
frontend/src/components/ProtocolTab.tsx ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SectionCard } from './SectionCard'
2
+
3
+ const stdoutContract = `[START] task_1
4
+ [END] task_1 | score=0.5000 | status=fatal_no_llm
5
+ [START] task_2
6
+ [END] task_2 | score=0.5000 | status=fatal_no_llm
7
+ [START] task_3
8
+ [END] task_3 | score=0.5000 | status=fatal_no_llm`
9
+
10
+ export function ProtocolTab() {
11
+ return (
12
+ <div className="space-y-6">
13
+ <SectionCard
14
+ eyebrow="Inference contract"
15
+ title="Agent protocol"
16
+ subtitle="Winner’s extra protocol pages document the exact validator assumptions that came out of the hackathon debugging cycle."
17
+ >
18
+ <div className="grid xl:grid-cols-2 gap-6">
19
+ <div className="space-y-4">
20
+ <div className="rounded-2xl border border-zinc-800 bg-zinc-950/60 p-5">
21
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-3">Environment variables</p>
22
+ <ul className="space-y-2 text-sm text-zinc-200 mono">
23
+ <li>API_BASE_URL</li>
24
+ <li>MODEL_NAME</li>
25
+ <li>HF_TOKEN</li>
26
+ <li>LOCAL_IMAGE_NAME (optional)</li>
27
+ <li>ENV_URL (optional for local replay)</li>
28
+ </ul>
29
+ </div>
30
+
31
+ <div className="rounded-2xl border border-zinc-800 bg-zinc-950/60 p-5">
32
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-3">Scoring guardrails</p>
33
+ <ul className="space-y-2 text-sm text-zinc-200 leading-7">
34
+ <li>Scores are clamped after rounding to avoid `0.0000` and `1.0000` leaks.</li>
35
+ <li>NaN, inf, and non-numeric paths collapse to `0.5`.</li>
36
+ <li>Every task emits exactly one `[START]` and one `[END]` line.</li>
37
+ <li>No stray stdout floats are allowed outside the score field.</li>
38
+ </ul>
39
+ </div>
40
+ </div>
41
+
42
+ <div className="rounded-2xl border border-emerald-500/15 bg-emerald-500/8 p-5">
43
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-3">No-key stdout example</p>
44
+ <pre className="mono text-xs leading-6 text-emerald-100 whitespace-pre-wrap">
45
+ {stdoutContract}
46
+ </pre>
47
+ </div>
48
+ </div>
49
+ </SectionCard>
50
+
51
+ <SectionCard
52
+ eyebrow="Validator checklist"
53
+ title="What the portal expects"
54
+ subtitle="This page mirrors the checks we had to satisfy before the Winner Space became portal-ready."
55
+ >
56
+ <div className="grid md:grid-cols-2 xl:grid-cols-4 gap-4">
57
+ {[
58
+ 'GET /health returns 200',
59
+ 'POST /reset accepts an empty body',
60
+ 'POST /step returns reward and done state',
61
+ 'POST /grader emits a score strictly inside (0, 1)',
62
+ ].map((item) => (
63
+ <div key={item} className="rounded-2xl border border-zinc-800 bg-zinc-950/60 p-4 text-sm text-zinc-200">
64
+ {item}
65
+ </div>
66
+ ))}
67
+ </div>
68
+ </SectionCard>
69
+ </div>
70
+ )
71
+ }
frontend/src/components/QueryWorkbenchTab.tsx ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useEffect, useMemo, useState } from 'react'
2
+ import { gradeTask, resetTask, submitQuery } from '../api/client'
3
+ import { TASK_CATALOG } from '../data/taskCatalog'
4
+ import type { ApiInfo, GraderResponse, StepResponse, TaskInfo, TaskObservation } from '../types'
5
+ import { SectionCard } from './SectionCard'
6
+ import { SqlPreviewTable } from './SqlPreviewTable'
7
+
8
+ interface QueryWorkbenchTabProps {
9
+ selectedTask: string
10
+ taskInfo?: TaskInfo
11
+ apiInfo: ApiInfo
12
+ }
13
+
14
+ function scoreTone(score: number | null) {
15
+ if (score === null) return 'text-zinc-400'
16
+ if (score >= 0.9) return 'text-emerald-300'
17
+ if (score >= 0.3) return 'text-amber-300'
18
+ return 'text-rose-300'
19
+ }
20
+
21
+ export function QueryWorkbenchTab({
22
+ selectedTask,
23
+ taskInfo,
24
+ apiInfo,
25
+ }: QueryWorkbenchTabProps) {
26
+ const [task, setTask] = useState<TaskObservation | null>(null)
27
+ const [query, setQuery] = useState('')
28
+ const [grader, setGrader] = useState<GraderResponse | null>(null)
29
+ const [stepResult, setStepResult] = useState<StepResponse | null>(null)
30
+ const [loading, setLoading] = useState(false)
31
+ const [error, setError] = useState<string | null>(null)
32
+ const catalogEntry = TASK_CATALOG[selectedTask]
33
+
34
+ const loadTask = useCallback(async () => {
35
+ setLoading(true)
36
+ setError(null)
37
+ try {
38
+ const nextTask = await resetTask(selectedTask)
39
+ setTask(nextTask)
40
+ setQuery(nextTask.broken_query)
41
+ setStepResult(null)
42
+ setGrader(null)
43
+ } catch (loadError) {
44
+ setError(loadError instanceof Error ? loadError.message : String(loadError))
45
+ } finally {
46
+ setLoading(false)
47
+ }
48
+ }, [selectedTask])
49
+
50
+ useEffect(() => {
51
+ void loadTask()
52
+ }, [loadTask])
53
+
54
+ const mergedObservation = useMemo(() => {
55
+ if (!task) return null
56
+ if (!stepResult) return task
57
+ return { ...task, ...stepResult.observation }
58
+ }, [stepResult, task])
59
+
60
+ const runQuery = async (nextQuery: string) => {
61
+ setLoading(true)
62
+ setError(null)
63
+ try {
64
+ const response = await submitQuery(nextQuery)
65
+ setStepResult(response)
66
+ setTask((current) => (current ? { ...current, ...response.observation } : response.observation))
67
+ const nextScore = await gradeTask(selectedTask)
68
+ setGrader(nextScore)
69
+ } catch (runError) {
70
+ setError(runError instanceof Error ? runError.message : String(runError))
71
+ } finally {
72
+ setLoading(false)
73
+ }
74
+ }
75
+
76
+ return (
77
+ <div className="space-y-6">
78
+ <div className="grid xl:grid-cols-[0.95fr_1.05fr] gap-6">
79
+ <SectionCard
80
+ eyebrow="Case File"
81
+ title={taskInfo?.name ?? selectedTask}
82
+ subtitle="The environment resets on every task change and tracks a single active SQL repair session."
83
+ actions={
84
+ <button
85
+ onClick={() => void loadTask()}
86
+ className="px-3 py-2 rounded-lg text-sm border border-zinc-800 bg-zinc-950/60 hover:bg-zinc-900/60"
87
+ >
88
+ Reset Task
89
+ </button>
90
+ }
91
+ >
92
+ {mergedObservation ? (
93
+ <div className="space-y-5">
94
+ <div className="grid sm:grid-cols-3 gap-3">
95
+ <div className="rounded-xl bg-zinc-950/60 border border-zinc-800 p-3">
96
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500">Difficulty</p>
97
+ <p className="text-lg font-semibold mt-2 text-zinc-100">{mergedObservation.difficulty}</p>
98
+ </div>
99
+ <div className="rounded-xl bg-zinc-950/60 border border-zinc-800 p-3">
100
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500">Expected Shape</p>
101
+ <p className="text-lg font-semibold mt-2 text-zinc-100">
102
+ {mergedObservation.expected_row_count} × {mergedObservation.expected_column_count}
103
+ </p>
104
+ </div>
105
+ <div className="rounded-xl bg-zinc-950/60 border border-zinc-800 p-3">
106
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500">Current Score</p>
107
+ <p className={`text-lg font-semibold mt-2 ${scoreTone(grader?.score ?? null)}`}>
108
+ {grader ? grader.score.toFixed(4) : '—'}
109
+ </p>
110
+ </div>
111
+ </div>
112
+
113
+ <div>
114
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">Hint</p>
115
+ <p className="text-sm leading-7 text-zinc-200">{mergedObservation.hint}</p>
116
+ </div>
117
+
118
+ <div>
119
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">Broken Query</p>
120
+ <pre className="mono text-sm rounded-xl border border-zinc-800 bg-zinc-950/70 px-4 py-4 overflow-auto text-rose-200 whitespace-pre-wrap">
121
+ {mergedObservation.broken_query}
122
+ </pre>
123
+ </div>
124
+
125
+ <div>
126
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">Schema</p>
127
+ <pre className="mono text-sm rounded-xl border border-zinc-800 bg-zinc-950/70 px-4 py-4 overflow-auto text-zinc-200 whitespace-pre-wrap">
128
+ {mergedObservation.schema_sql}
129
+ </pre>
130
+ </div>
131
+
132
+ {mergedObservation.broken_query_error && (
133
+ <div className="rounded-xl border border-amber-500/20 bg-amber-500/8 px-4 py-4 text-sm text-amber-100">
134
+ Baseline error: <span className="mono">{mergedObservation.broken_query_error}</span>
135
+ </div>
136
+ )}
137
+ </div>
138
+ ) : (
139
+ <div className="text-sm text-zinc-500">Loading task snapshot…</div>
140
+ )}
141
+ </SectionCard>
142
+
143
+ <SectionCard
144
+ eyebrow="Repair Workbench"
145
+ title="Submit and score candidate fixes"
146
+ subtitle="Use the same reset/step/grader flow that the validator exercises on the live Space."
147
+ actions={
148
+ <div className="flex flex-wrap gap-2">
149
+ <button
150
+ onClick={() => setQuery(task?.broken_query ?? '')}
151
+ className="px-3 py-2 rounded-lg text-sm border border-zinc-800 bg-zinc-950/60 hover:bg-zinc-900/60"
152
+ >
153
+ Load Broken Query
154
+ </button>
155
+ <button
156
+ onClick={() => setQuery(catalogEntry?.canonicalQuery ?? '')}
157
+ className="px-3 py-2 rounded-lg text-sm border border-emerald-500/20 bg-emerald-500/10 text-emerald-200 hover:bg-emerald-500/15"
158
+ >
159
+ Load Reference Fix
160
+ </button>
161
+ </div>
162
+ }
163
+ >
164
+ <div className="space-y-4">
165
+ <textarea
166
+ value={query}
167
+ onChange={(event) => setQuery(event.target.value)}
168
+ spellCheck={false}
169
+ className="w-full min-h-[220px] rounded-2xl border border-zinc-800 bg-zinc-950/80 px-4 py-4 text-sm text-zinc-100 mono focus:outline-none focus:border-cyan-500/40"
170
+ />
171
+
172
+ <div className="flex flex-wrap gap-3">
173
+ <button
174
+ onClick={() => void runQuery(query)}
175
+ disabled={loading}
176
+ className="px-5 py-3 rounded-xl text-sm font-semibold bg-gradient-to-r from-emerald-500 to-cyan-500 text-zinc-950 disabled:opacity-60"
177
+ >
178
+ {loading ? 'Executing…' : 'Submit Current Query'}
179
+ </button>
180
+ <button
181
+ onClick={() => void loadTask()}
182
+ disabled={loading}
183
+ className="px-4 py-3 rounded-xl text-sm border border-zinc-800 bg-zinc-950/60 hover:bg-zinc-900/60 disabled:opacity-60"
184
+ >
185
+ Reset Session
186
+ </button>
187
+ </div>
188
+
189
+ {error && (
190
+ <div className="rounded-xl border border-rose-500/20 bg-rose-500/10 px-4 py-4 text-sm text-rose-200">
191
+ {error}
192
+ </div>
193
+ )}
194
+
195
+ <div className="grid lg:grid-cols-3 gap-4">
196
+ <div className="rounded-2xl border border-zinc-800 bg-zinc-950/60 p-4">
197
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">Reward</p>
198
+ <p className="text-2xl font-semibold text-zinc-100">
199
+ {stepResult ? stepResult.reward.toFixed(2) : '—'}
200
+ </p>
201
+ </div>
202
+ <div className="rounded-2xl border border-zinc-800 bg-zinc-950/60 p-4">
203
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">Solved</p>
204
+ <p className="text-2xl font-semibold text-zinc-100">
205
+ {stepResult ? (stepResult.info.solved ? 'YES' : 'NO') : '—'}
206
+ </p>
207
+ </div>
208
+ <div className="rounded-2xl border border-zinc-800 bg-zinc-950/60 p-4">
209
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">API Mode</p>
210
+ <p className="text-sm font-semibold text-zinc-100">{apiInfo.mode}</p>
211
+ <p className="text-xs text-zinc-500 mt-1 mono">{apiInfo.base}</p>
212
+ </div>
213
+ </div>
214
+
215
+ <div className="grid lg:grid-cols-2 gap-4">
216
+ <div>
217
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">Result Preview</p>
218
+ <SqlPreviewTable
219
+ emptyLabel="Run a query to inspect the first rows returned by SQLite."
220
+ rows={mergedObservation?.result_preview}
221
+ />
222
+ </div>
223
+ <div>
224
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">Expected Preview</p>
225
+ <SqlPreviewTable
226
+ emptyLabel="Expected rows will appear after the task loads."
227
+ rows={mergedObservation?.expected_preview}
228
+ />
229
+ </div>
230
+ </div>
231
+
232
+ {catalogEntry && (
233
+ <div className="rounded-2xl border border-cyan-500/15 bg-cyan-500/8 px-4 py-4 text-sm text-cyan-100">
234
+ <p className="font-semibold mb-2">Reference validation signal</p>
235
+ <p className="leading-7">{catalogEntry.validationSignal}</p>
236
+ </div>
237
+ )}
238
+ </div>
239
+ </SectionCard>
240
+ </div>
241
+ </div>
242
+ )
243
+ }
frontend/src/components/SectionCard.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import clsx from 'clsx'
2
+ import type { ReactNode } from 'react'
3
+
4
+ interface SectionCardProps {
5
+ title: string
6
+ eyebrow?: string
7
+ subtitle?: string
8
+ actions?: ReactNode
9
+ children: ReactNode
10
+ className?: string
11
+ }
12
+
13
+ export function SectionCard({
14
+ title,
15
+ eyebrow,
16
+ subtitle,
17
+ actions,
18
+ children,
19
+ className,
20
+ }: SectionCardProps) {
21
+ return (
22
+ <section className={clsx('glass-panel rounded-2xl overflow-hidden', className)}>
23
+ <div className="px-5 py-4 border-b border-zinc-800/80 flex items-start justify-between gap-4">
24
+ <div>
25
+ {eyebrow && (
26
+ <p className="text-[11px] uppercase tracking-[0.26em] text-zinc-500 mb-2">
27
+ {eyebrow}
28
+ </p>
29
+ )}
30
+ <h2 className="text-lg font-semibold text-zinc-100">{title}</h2>
31
+ {subtitle && <p className="text-sm text-zinc-400 mt-1">{subtitle}</p>}
32
+ </div>
33
+ {actions && <div className="flex items-center gap-2 flex-wrap">{actions}</div>}
34
+ </div>
35
+ <div className="p-5">{children}</div>
36
+ </section>
37
+ )
38
+ }
frontend/src/components/SqlLandingPage.tsx ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion } from 'framer-motion'
2
+ import {
3
+ Activity,
4
+ Braces,
5
+ Database,
6
+ ShieldCheck,
7
+ Sparkles,
8
+ TerminalSquare,
9
+ } from 'lucide-react'
10
+
11
+ interface SqlLandingPageProps {
12
+ onLaunch: () => void
13
+ }
14
+
15
+ const featureCards = [
16
+ {
17
+ icon: Database,
18
+ title: 'SQLite-backed task arena',
19
+ copy: 'Each mission spins up a fresh in-memory database so every repair attempt is deterministic and reproducible.',
20
+ },
21
+ {
22
+ icon: ShieldCheck,
23
+ title: 'Strict validator guardrails',
24
+ copy: 'Scores are clamped to the open interval (0, 1) even after rounding, matching the hackathon validator contract.',
25
+ },
26
+ {
27
+ icon: TerminalSquare,
28
+ title: 'Inference protocol ready',
29
+ copy: 'The console mirrors the same reset/step/grader lifecycle that powers the baseline agent and portal checks.',
30
+ },
31
+ ]
32
+
33
+ const statusPills = [
34
+ { label: '3 SQL tasks', value: 'easy → hard' },
35
+ { label: '6-step budget', value: 'per task' },
36
+ { label: 'HF Space', value: 'live + healthy' },
37
+ ]
38
+
39
+ export function SqlLandingPage({ onLaunch }: SqlLandingPageProps) {
40
+ return (
41
+ <div className="min-h-screen relative overflow-hidden bg-[#050505] text-white">
42
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(16,185,129,0.18),transparent_32%),radial-gradient(circle_at_top_right,rgba(34,211,238,0.14),transparent_32%),radial-gradient(circle_at_bottom,rgba(245,158,11,0.12),transparent_36%)]" />
43
+ <div className="absolute inset-0 opacity-30 bg-[linear-gradient(rgba(255,255,255,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.03)_1px,transparent_1px)] bg-[size:28px_28px]" />
44
+
45
+ <div className="relative z-10 max-w-7xl mx-auto px-6 py-10 md:py-16">
46
+ <motion.div
47
+ initial={{ opacity: 0, y: 24 }}
48
+ animate={{ opacity: 1, y: 0 }}
49
+ transition={{ duration: 0.5 }}
50
+ className="flex flex-wrap items-center justify-between gap-4 mb-16"
51
+ >
52
+ <div className="flex items-center gap-3">
53
+ <div className="w-12 h-12 rounded-full border border-emerald-400/30 bg-emerald-500/10 flex items-center justify-center shadow-[0_0_18px_rgba(16,185,129,0.35)]">
54
+ <Sparkles className="w-6 h-6 text-emerald-300" />
55
+ </div>
56
+ <div>
57
+ <p className="text-[11px] uppercase tracking-[0.3em] text-zinc-500">Winner Project</p>
58
+ <h1 className="text-xl font-semibold">SQL Repair Env</h1>
59
+ </div>
60
+ </div>
61
+
62
+ <div className="flex flex-wrap gap-3">
63
+ <a
64
+ href="/docs"
65
+ className="px-4 py-2 text-sm rounded-lg border border-zinc-800 bg-black/30 hover:bg-zinc-900/60 transition-colors"
66
+ >
67
+ Inspect API Docs
68
+ </a>
69
+ <button
70
+ onClick={onLaunch}
71
+ className="px-5 py-2 text-sm font-semibold rounded-lg bg-gradient-to-r from-emerald-500 to-cyan-500 text-zinc-950 shadow-[0_0_22px_rgba(34,211,238,0.28)]"
72
+ >
73
+ Launch Repair Console
74
+ </button>
75
+ </div>
76
+ </motion.div>
77
+
78
+ <div className="grid lg:grid-cols-[1.2fr_0.8fr] gap-8 items-start">
79
+ <motion.div
80
+ initial={{ opacity: 0, x: -24 }}
81
+ animate={{ opacity: 1, x: 0 }}
82
+ transition={{ duration: 0.55, delay: 0.1 }}
83
+ className="glass-panel rounded-[28px] p-8 md:p-10"
84
+ >
85
+ <div className="inline-flex items-center gap-2 rounded-full border border-emerald-400/20 bg-emerald-500/8 px-4 py-2 text-[11px] uppercase tracking-[0.26em] text-emerald-200 mb-6">
86
+ <Activity className="w-4 h-4" />
87
+ Live Validation Console
88
+ </div>
89
+
90
+ <h2 className="text-4xl md:text-6xl font-semibold leading-[1.02] tracking-tight max-w-3xl">
91
+ Copy the canary’s
92
+ <span className="block bg-gradient-to-r from-emerald-300 via-cyan-300 to-amber-300 bg-clip-text text-transparent">
93
+ command-center energy
94
+ </span>
95
+ for a SQL repair environment.
96
+ </h2>
97
+
98
+ <p className="text-zinc-300 text-base md:text-lg leading-8 mt-6 max-w-2xl">
99
+ Winner now ships a proper root UI for Hugging Face Spaces: task explorer, live
100
+ repair workbench, baseline scoring, and the exact agent protocol used by the
101
+ OpenEnv validator.
102
+ </p>
103
+
104
+ <div className="grid sm:grid-cols-3 gap-3 mt-8">
105
+ {statusPills.map((pill) => (
106
+ <div
107
+ key={pill.label}
108
+ className="rounded-2xl border border-zinc-800/80 bg-zinc-950/65 px-4 py-4"
109
+ >
110
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500">{pill.label}</p>
111
+ <p className="text-lg font-semibold mt-2 text-zinc-100">{pill.value}</p>
112
+ </div>
113
+ ))}
114
+ </div>
115
+ </motion.div>
116
+
117
+ <motion.div
118
+ initial={{ opacity: 0, x: 24 }}
119
+ animate={{ opacity: 1, x: 0 }}
120
+ transition={{ duration: 0.55, delay: 0.18 }}
121
+ className="space-y-4"
122
+ >
123
+ {featureCards.map(({ icon: Icon, title, copy }) => (
124
+ <div
125
+ key={title}
126
+ className="glass-panel rounded-2xl p-5 border border-zinc-800/80"
127
+ >
128
+ <div className="flex items-center gap-3 mb-3">
129
+ <div className="w-11 h-11 rounded-xl bg-zinc-950/70 border border-zinc-800 flex items-center justify-center">
130
+ <Icon className="w-5 h-5 text-cyan-300" />
131
+ </div>
132
+ <h3 className="text-lg font-semibold">{title}</h3>
133
+ </div>
134
+ <p className="text-sm leading-7 text-zinc-300">{copy}</p>
135
+ </div>
136
+ ))}
137
+
138
+ <div className="glass-panel rounded-2xl p-5 border border-amber-500/20 bg-[linear-gradient(180deg,rgba(245,158,11,0.08),rgba(9,9,11,0.6))]">
139
+ <div className="flex items-center gap-3 mb-3">
140
+ <div className="w-11 h-11 rounded-xl bg-zinc-950/60 border border-amber-400/20 flex items-center justify-center">
141
+ <Braces className="w-5 h-5 text-amber-200" />
142
+ </div>
143
+ <h3 className="text-lg font-semibold">Validator-safe stdout</h3>
144
+ </div>
145
+ <pre className="mono text-xs leading-6 text-amber-100 whitespace-pre-wrap">
146
+ {`[START] task_1
147
+ [END] task_1 | score=0.5000 | status=fatal_no_llm
148
+ [START] task_2
149
+ [END] task_2 | score=0.5000 | status=fatal_no_llm
150
+ [START] task_3
151
+ [END] task_3 | score=0.5000 | status=fatal_no_llm`}
152
+ </pre>
153
+ </div>
154
+ </motion.div>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ )
159
+ }
frontend/src/components/SqlPreviewTable.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { SqlRow } from '../types'
2
+
3
+ interface SqlPreviewTableProps {
4
+ rows?: SqlRow[] | null
5
+ emptyLabel: string
6
+ }
7
+
8
+ function formatCell(cell: SqlRow[number]) {
9
+ if (cell === null) return 'NULL'
10
+ if (typeof cell === 'number') return Number.isInteger(cell) ? cell.toString() : cell.toFixed(2)
11
+ return String(cell)
12
+ }
13
+
14
+ export function SqlPreviewTable({ rows, emptyLabel }: SqlPreviewTableProps) {
15
+ if (!rows || rows.length === 0) {
16
+ return (
17
+ <div className="rounded-xl border border-dashed border-zinc-800 bg-zinc-950/60 px-4 py-6 text-sm text-zinc-500">
18
+ {emptyLabel}
19
+ </div>
20
+ )
21
+ }
22
+
23
+ return (
24
+ <div className="overflow-auto rounded-xl border border-zinc-800">
25
+ <table className="w-full text-sm">
26
+ <tbody>
27
+ {rows.map((row, rowIndex) => (
28
+ <tr key={`row-${rowIndex}`} className="odd:bg-zinc-950/60 even:bg-zinc-900/60">
29
+ <td className="px-3 py-2 text-zinc-500 border-r border-zinc-800 mono w-12">{rowIndex}</td>
30
+ {row.map((cell, cellIndex) => (
31
+ <td
32
+ key={`cell-${rowIndex}-${cellIndex}`}
33
+ className="px-3 py-2 border-r border-zinc-800/80 last:border-r-0 text-zinc-100 mono"
34
+ >
35
+ {formatCell(cell)}
36
+ </td>
37
+ ))}
38
+ </tr>
39
+ ))}
40
+ </tbody>
41
+ </table>
42
+ </div>
43
+ )
44
+ }
frontend/src/components/TaskAtlasTab.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TASK_CATALOG } from '../data/taskCatalog'
2
+ import type { TaskInfo } from '../types'
3
+ import { SectionCard } from './SectionCard'
4
+
5
+ interface TaskAtlasTabProps {
6
+ tasks: TaskInfo[]
7
+ selectedTask: string
8
+ onSelectTask: (taskId: string) => void
9
+ }
10
+
11
+ export function TaskAtlasTab({ tasks, selectedTask, onSelectTask }: TaskAtlasTabProps) {
12
+ return (
13
+ <div className="space-y-6">
14
+ <SectionCard
15
+ eyebrow="Mission Catalog"
16
+ title="All SQL repair tasks"
17
+ subtitle="Winner-specific task cards built on top of the copied DisasterMan console shell."
18
+ >
19
+ <div className="grid xl:grid-cols-3 gap-5">
20
+ {tasks.map((task) => {
21
+ const catalog = TASK_CATALOG[task.id]
22
+ const isSelected = task.id === selectedTask
23
+
24
+ return (
25
+ <article
26
+ key={task.id}
27
+ className={`rounded-2xl border p-5 transition-colors ${
28
+ isSelected
29
+ ? 'border-emerald-400/40 bg-emerald-500/10'
30
+ : 'border-zinc-800 bg-zinc-950/60'
31
+ }`}
32
+ >
33
+ <div className="flex items-start justify-between gap-3 mb-4">
34
+ <div>
35
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500">{task.id}</p>
36
+ <h3 className="text-lg font-semibold mt-2 text-zinc-100">{task.name}</h3>
37
+ </div>
38
+ <span className="rounded-full px-3 py-1 text-xs uppercase tracking-[0.2em] border border-zinc-700 text-zinc-300">
39
+ {task.difficulty}
40
+ </span>
41
+ </div>
42
+
43
+ <p className="text-sm leading-7 text-zinc-300 mb-4">{catalog?.story}</p>
44
+
45
+ <div className="space-y-4 text-sm">
46
+ <div>
47
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">Why it fails</p>
48
+ <p className="leading-7 text-zinc-200">{catalog?.whyItFails}</p>
49
+ </div>
50
+
51
+ <div>
52
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">Schema snapshot</p>
53
+ <pre className="mono text-xs rounded-xl border border-zinc-800 bg-black/40 px-3 py-3 whitespace-pre-wrap overflow-auto text-zinc-300">
54
+ {catalog?.schemaStatements.slice(0, 4).join('\n')}
55
+ </pre>
56
+ </div>
57
+
58
+ <div>
59
+ <p className="text-[11px] uppercase tracking-[0.24em] text-zinc-500 mb-2">Reference fix</p>
60
+ <pre className="mono text-xs rounded-xl border border-emerald-500/15 bg-emerald-500/8 px-3 py-3 whitespace-pre-wrap overflow-auto text-emerald-100">
61
+ {catalog?.canonicalQuery}
62
+ </pre>
63
+ </div>
64
+ </div>
65
+
66
+ <button
67
+ onClick={() => onSelectTask(task.id)}
68
+ className="mt-5 w-full px-4 py-3 rounded-xl bg-gradient-to-r from-emerald-500 to-cyan-500 text-zinc-950 font-semibold text-sm"
69
+ >
70
+ Open in Query Lab
71
+ </button>
72
+ </article>
73
+ )
74
+ })}
75
+ </div>
76
+ </SectionCard>
77
+ </div>
78
+ )
79
+ }
frontend/src/data/taskCatalog.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { TaskCatalogEntry } from '../types'
2
+
3
+ export const TASK_CATALOG: Record<string, TaskCatalogEntry> = {
4
+ task_1: {
5
+ id: 'task_1',
6
+ story:
7
+ 'A receipt parser captured the right columns but dropped commas in the SELECT list.',
8
+ whyItFails:
9
+ 'SQLite interprets `id name price` as malformed syntax because the projection list never separates the column identifiers.',
10
+ schemaStatements: [
11
+ 'CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL);',
12
+ "INSERT INTO products VALUES (1, 'Apple', 0.50);",
13
+ "INSERT INTO products VALUES (2, 'Bread', 2.50);",
14
+ "INSERT INTO products VALUES (3, 'Cheese', 5.00);",
15
+ "INSERT INTO products VALUES (4, 'Milk', 1.50);",
16
+ "INSERT INTO products VALUES (5, 'Eggs', 3.00);",
17
+ ],
18
+ canonicalQuery: 'SELECT id, name, price FROM products ORDER BY id',
19
+ validationSignal: 'Solved when the result matches 5 rows × 3 columns in the expected order.',
20
+ },
21
+ task_2: {
22
+ id: 'task_2',
23
+ story:
24
+ 'An analyst copied a JOIN from another schema and kept the wrong column names.',
25
+ whyItFails:
26
+ 'Both `username` and `user` do not exist in the current schema, so the join never resolves against the users and orders tables.',
27
+ schemaStatements: [
28
+ 'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, country TEXT);',
29
+ 'CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, total REAL NOT NULL);',
30
+ "INSERT INTO users VALUES (1, 'Aarav', 'IN');",
31
+ "INSERT INTO users VALUES (2, 'Bea', 'US');",
32
+ "INSERT INTO users VALUES (3, 'Chen', 'CN');",
33
+ 'INSERT INTO orders VALUES (10, 1, 99.00);',
34
+ 'INSERT INTO orders VALUES (11, 1, 49.50);',
35
+ 'INSERT INTO orders VALUES (12, 2, 200.00);',
36
+ 'INSERT INTO orders VALUES (13, 3, 25.00);',
37
+ ],
38
+ canonicalQuery:
39
+ 'SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.id',
40
+ validationSignal: 'Solved when the name/total pairs align with all 4 order rows.',
41
+ },
42
+ task_3: {
43
+ id: 'task_3',
44
+ story:
45
+ 'A reporting query totals revenue by region but omits the grouping step.',
46
+ whyItFails:
47
+ 'The query mixes a non-aggregate column with `SUM(amount)` and never groups by region, so the repair must add the missing aggregation boundary.',
48
+ schemaStatements: [
49
+ 'CREATE TABLE sales (id INTEGER PRIMARY KEY, region TEXT NOT NULL, amount REAL NOT NULL);',
50
+ "INSERT INTO sales VALUES (1, 'north', 100.00);",
51
+ "INSERT INTO sales VALUES (2, 'north', 50.00);",
52
+ "INSERT INTO sales VALUES (3, 'south', 200.00);",
53
+ "INSERT INTO sales VALUES (4, 'south', 75.00);",
54
+ "INSERT INTO sales VALUES (5, 'east', 150.00);",
55
+ "INSERT INTO sales VALUES (6, 'east', 25.00);",
56
+ ],
57
+ canonicalQuery:
58
+ 'SELECT region, SUM(amount) AS total FROM sales GROUP BY region ORDER BY region',
59
+ validationSignal: 'Solved when the grouped totals produce exactly 3 ordered regional aggregates.',
60
+ },
61
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;600&family=Geist:wght@400;500;600;700&display=swap');
2
+
3
+ @import 'leaflet/dist/leaflet.css';
4
+
5
+ @import "tailwindcss";
6
+
7
+ :root {
8
+ font-family: 'Geist', system-ui, sans-serif;
9
+ --glass-bg: rgba(9, 9, 11, 0.6);
10
+ --glass-border: rgba(255, 255, 255, 0.05);
11
+ }
12
+
13
+ body {
14
+ background-color: #050505;
15
+ background-image:
16
+ linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
17
+ linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px),
18
+ radial-gradient(circle at 15% 50%, rgba(59, 130, 246, 0.15), transparent 40%),
19
+ radial-gradient(circle at 85% 30%, rgba(220, 38, 38, 0.12), transparent 40%);
20
+ background-size: 32px 32px, 32px 32px, 100% 100%, 100% 100%;
21
+ background-position: center center;
22
+ background-attachment: fixed;
23
+ color: #fafafa;
24
+ margin: 0;
25
+ min-height: 100vh;
26
+ }
27
+
28
+ #root {
29
+ min-height: 100vh;
30
+ }
31
+
32
+ .mono {
33
+ font-family: 'Geist Mono', monospace;
34
+ }
35
+
36
+ .zone-pulse {
37
+ animation: pulse-ring 0.8s ease-out infinite;
38
+ }
39
+
40
+ @keyframes pulse-ring {
41
+ 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.8); }
42
+ 100% { box-shadow: 0 0 0 16px rgba(239, 68, 68, 0); }
43
+ }
44
+
45
+ .zone-pulse-green {
46
+ animation: pulse-ring-green 1s ease-out infinite;
47
+ }
48
+
49
+ @keyframes pulse-ring-green {
50
+ 0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.6); }
51
+ 100% { box-shadow: 0 0 0 14px rgba(34, 197, 94, 0); }
52
+ }
53
+
54
+ .step-fade-in {
55
+ animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
56
+ }
57
+
58
+ @keyframes fadeIn {
59
+ from { opacity: 0; transform: translateY(6px) scale(0.98); }
60
+ to { opacity: 1; transform: translateY(0) scale(1); }
61
+ }
62
+
63
+ .score-bar-fill {
64
+ transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
65
+ }
66
+
67
+ .glass-panel {
68
+ background: rgba(9, 9, 11, 0.4);
69
+ backdrop-filter: blur(24px);
70
+ -webkit-backdrop-filter: blur(24px);
71
+ border: 1px solid rgba(255, 255, 255, 0.08);
72
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5), inset 0 1px 0 0 rgba(255, 255, 255, 0.05);
73
+ }
74
+
75
+ .neon-border-red {
76
+ box-shadow: 0 0 10px rgba(220, 38, 38, 0.4), inset 0 0 10px rgba(220, 38, 38, 0.4);
77
+ border-color: rgba(239, 68, 68, 0.6);
78
+ }
79
+
80
+ .neon-border-green {
81
+ box-shadow: 0 0 10px rgba(34, 197, 94, 0.2), inset 0 0 10px rgba(34, 197, 94, 0.1);
82
+ border-color: rgba(34, 197, 94, 0.4);
83
+ }
84
+
85
+ .neon-border-purple {
86
+ box-shadow: 0 0 15px rgba(168, 85, 247, 0.5), inset 0 0 15px rgba(168, 85, 247, 0.3);
87
+ border-color: rgba(192, 132, 252, 0.8);
88
+ }
frontend/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/src/types.ts ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type HealthStatus = 'loading' | 'ok' | 'error'
2
+
3
+ export interface ApiInfo {
4
+ base: string
5
+ mode: 'proxy' | 'direct' | 'local'
6
+ env: string
7
+ }
8
+
9
+ export type SqlCell = string | number | boolean | null
10
+ export type SqlRow = SqlCell[]
11
+
12
+ export interface HealthResponse {
13
+ status: string
14
+ [key: string]: unknown
15
+ }
16
+
17
+ export interface TaskInfo {
18
+ id: string
19
+ name: string
20
+ difficulty: string
21
+ }
22
+
23
+ export interface TaskObservation {
24
+ task_id: string
25
+ name: string
26
+ difficulty: string
27
+ schema_sql: string
28
+ broken_query: string
29
+ broken_query_error: string | null
30
+ broken_query_executes: boolean
31
+ hint: string
32
+ expected_row_count: number
33
+ expected_column_count: number
34
+ step_count: number
35
+ max_steps: number
36
+ remaining_steps: number
37
+ submitted_query?: string
38
+ error?: string | null
39
+ executed?: boolean
40
+ matches_expected?: boolean
41
+ result_row_count?: number
42
+ result_preview?: SqlRow[] | null
43
+ expected_preview?: SqlRow[] | null
44
+ }
45
+
46
+ export interface StepResponse {
47
+ observation: TaskObservation
48
+ reward: number
49
+ done: boolean
50
+ info: {
51
+ solved: boolean
52
+ [key: string]: unknown
53
+ }
54
+ }
55
+
56
+ export interface GraderResponse {
57
+ task_id: string
58
+ score: number
59
+ }
60
+
61
+ export interface BaselineResponse {
62
+ scores: Record<string, number>
63
+ max_steps: number
64
+ }
65
+
66
+ export interface TaskCatalogEntry {
67
+ id: string
68
+ story: string
69
+ whyItFails: string
70
+ schemaStatements: string[]
71
+ canonicalQuery: string
72
+ validationSignal: string
73
+ }
frontend/tsconfig.app.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": false,
22
+ "noUnusedParameters": false,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true
26
+ },
27
+ "include": ["src"]
28
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
frontend/vercel.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "rewrites": [
3
+ {
4
+ "source": "/api/(.*)",
5
+ "destination": "https://krishpotanwar-disaster-relief-env.hf.space/$1"
6
+ },
7
+ {
8
+ "source": "/(.*)",
9
+ "destination": "/index.html"
10
+ }
11
+ ]
12
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react(), tailwindcss()],
8
+ })
server/app.py CHANGED
@@ -18,7 +18,9 @@ from __future__ import annotations
18
  import os
19
  from typing import Any, Dict, List, Optional
20
 
21
- from fastapi import Body, FastAPI
 
 
22
  from pydantic import BaseModel, Field
23
 
24
  from sql_env.env_core import EnvState, MAX_STEPS
@@ -36,6 +38,8 @@ app = FastAPI(
36
 
37
  # Single mutable env state instance — the validator runs one session.
38
  _state = EnvState()
 
 
39
 
40
 
41
  # ---------------------------------------------------------------------------
@@ -70,6 +74,11 @@ def health() -> Dict[str, str]:
70
  return {"status": "ok"}
71
 
72
 
 
 
 
 
 
73
  @app.get("/tasks")
74
  def list_tasks() -> Dict[str, Any]:
75
  return {
@@ -85,6 +94,11 @@ def list_tasks() -> Dict[str, Any]:
85
  }
86
 
87
 
 
 
 
 
 
88
  @app.post("/reset")
89
  def reset(req: Optional[ResetRequest] = Body(default=None)) -> Dict[str, Any]:
90
  """Reset the environment. Body is optional — defaults to task_1."""
@@ -93,6 +107,11 @@ def reset(req: Optional[ResetRequest] = Body(default=None)) -> Dict[str, Any]:
93
  return obs
94
 
95
 
 
 
 
 
 
96
  @app.post("/step")
97
  def step(req: Optional[StepRequest] = Body(default=None)) -> Dict[str, Any]:
98
  """Apply one action to the environment."""
@@ -100,6 +119,11 @@ def step(req: Optional[StepRequest] = Body(default=None)) -> Dict[str, Any]:
100
  return _state.step(action)
101
 
102
 
 
 
 
 
 
103
  @app.post("/grader")
104
  def grader(req: Optional[GraderRequest] = Body(default=None)) -> Dict[str, Any]:
105
  """Return the strict-(0,1) score for the given task."""
@@ -108,6 +132,11 @@ def grader(req: Optional[GraderRequest] = Body(default=None)) -> Dict[str, Any]:
108
  return {"task_id": task_id, "score": float(score)}
109
 
110
 
 
 
 
 
 
111
  @app.post("/baseline")
112
  def baseline(
113
  req: Optional[BaselineRequest] = Body(default=None),
@@ -126,6 +155,48 @@ def baseline(
126
  return {"scores": out, "max_steps": MAX_STEPS}
127
 
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  # ---------------------------------------------------------------------------
130
  # Entry point — referenced by [project.scripts] server = "server.app:main"
131
  # ---------------------------------------------------------------------------
 
18
  import os
19
  from typing import Any, Dict, List, Optional
20
 
21
+ from fastapi import Body, FastAPI, HTTPException
22
+ from fastapi.responses import FileResponse
23
+ from fastapi.staticfiles import StaticFiles
24
  from pydantic import BaseModel, Field
25
 
26
  from sql_env.env_core import EnvState, MAX_STEPS
 
38
 
39
  # Single mutable env state instance — the validator runs one session.
40
  _state = EnvState()
41
+ _server_dir = os.path.dirname(os.path.abspath(__file__))
42
+ _frontend_dist = os.path.abspath(os.path.join(_server_dir, "..", "frontend", "dist"))
43
 
44
 
45
  # ---------------------------------------------------------------------------
 
74
  return {"status": "ok"}
75
 
76
 
77
+ @app.get("/api/health")
78
+ def health_api() -> Dict[str, str]:
79
+ return health()
80
+
81
+
82
  @app.get("/tasks")
83
  def list_tasks() -> Dict[str, Any]:
84
  return {
 
94
  }
95
 
96
 
97
+ @app.get("/api/tasks")
98
+ def list_tasks_api() -> Dict[str, Any]:
99
+ return list_tasks()
100
+
101
+
102
  @app.post("/reset")
103
  def reset(req: Optional[ResetRequest] = Body(default=None)) -> Dict[str, Any]:
104
  """Reset the environment. Body is optional — defaults to task_1."""
 
107
  return obs
108
 
109
 
110
+ @app.post("/api/reset")
111
+ def reset_api(req: Optional[ResetRequest] = Body(default=None)) -> Dict[str, Any]:
112
+ return reset(req)
113
+
114
+
115
  @app.post("/step")
116
  def step(req: Optional[StepRequest] = Body(default=None)) -> Dict[str, Any]:
117
  """Apply one action to the environment."""
 
119
  return _state.step(action)
120
 
121
 
122
+ @app.post("/api/step")
123
+ def step_api(req: Optional[StepRequest] = Body(default=None)) -> Dict[str, Any]:
124
+ return step(req)
125
+
126
+
127
  @app.post("/grader")
128
  def grader(req: Optional[GraderRequest] = Body(default=None)) -> Dict[str, Any]:
129
  """Return the strict-(0,1) score for the given task."""
 
132
  return {"task_id": task_id, "score": float(score)}
133
 
134
 
135
+ @app.post("/api/grader")
136
+ def grader_api(req: Optional[GraderRequest] = Body(default=None)) -> Dict[str, Any]:
137
+ return grader(req)
138
+
139
+
140
  @app.post("/baseline")
141
  def baseline(
142
  req: Optional[BaselineRequest] = Body(default=None),
 
155
  return {"scores": out, "max_steps": MAX_STEPS}
156
 
157
 
158
+ @app.post("/api/baseline")
159
+ def baseline_api(
160
+ req: Optional[BaselineRequest] = Body(default=None),
161
+ ) -> Dict[str, Any]:
162
+ return baseline(req)
163
+
164
+
165
+ @app.get("/", include_in_schema=False, response_model=None)
166
+ def root() -> Any:
167
+ index_file = os.path.join(_frontend_dist, "index.html")
168
+ if os.path.exists(index_file):
169
+ return FileResponse(index_file)
170
+ return {
171
+ "name": "SQL Repair OpenEnv",
172
+ "status": "ok",
173
+ "docs": "/docs",
174
+ "health": "/health",
175
+ "tasks": "/tasks",
176
+ }
177
+
178
+
179
+ if os.path.isdir(_frontend_dist):
180
+ assets_dir = os.path.join(_frontend_dist, "assets")
181
+ if os.path.isdir(assets_dir):
182
+ app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
183
+
184
+ @app.get("/{full_path:path}", include_in_schema=False)
185
+ def spa_fallback(full_path: str) -> FileResponse:
186
+ if full_path.startswith("api/"):
187
+ raise HTTPException(status_code=404, detail="Not Found")
188
+
189
+ requested_file = os.path.join(_frontend_dist, full_path)
190
+ if os.path.isfile(requested_file):
191
+ return FileResponse(requested_file)
192
+
193
+ index_file = os.path.join(_frontend_dist, "index.html")
194
+ if os.path.isfile(index_file):
195
+ return FileResponse(index_file)
196
+
197
+ raise HTTPException(status_code=404, detail="Not Found")
198
+
199
+
200
  # ---------------------------------------------------------------------------
201
  # Entry point — referenced by [project.scripts] server = "server.app:main"
202
  # ---------------------------------------------------------------------------