Spaces:
Sleeping
Sleeping
Commit ·
f86ef5b
1
Parent(s): 0376765
feat(frontend): add SQL command center UI
Browse files- .dockerignore +2 -0
- Dockerfile +8 -1
- frontend/.gitignore +5 -0
- frontend/README.md +44 -0
- frontend/eslint.config.js +23 -0
- frontend/index.html +26 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +40 -0
- frontend/public/favicon.svg +1 -0
- frontend/public/icons.svg +24 -0
- frontend/src/App.css +184 -0
- frontend/src/App.tsx +231 -0
- frontend/src/api/client.ts +113 -0
- frontend/src/assets/hero.png +0 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/assets/vite.svg +1 -0
- frontend/src/components/ApiOpsTab.tsx +79 -0
- frontend/src/components/BaselineArenaTab.tsx +132 -0
- frontend/src/components/ProtocolTab.tsx +71 -0
- frontend/src/components/QueryWorkbenchTab.tsx +243 -0
- frontend/src/components/SectionCard.tsx +38 -0
- frontend/src/components/SqlLandingPage.tsx +159 -0
- frontend/src/components/SqlPreviewTable.tsx +44 -0
- frontend/src/components/TaskAtlasTab.tsx +79 -0
- frontend/src/data/taskCatalog.ts +61 -0
- frontend/src/index.css +88 -0
- frontend/src/main.tsx +10 -0
- frontend/src/types.ts +73 -0
- frontend/tsconfig.app.json +28 -0
- frontend/tsconfig.json +7 -0
- frontend/tsconfig.node.json +26 -0
- frontend/vercel.json +12 -0
- frontend/vite.config.ts +8 -0
- server/app.py +72 -1
.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
|
| 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 |
# ---------------------------------------------------------------------------
|