Spaces:
Sleeping
Sleeping
fix
Browse files- backend/api/demo.py +49 -0
- frontend/src/App.tsx +7 -2
- frontend/src/components/ChatPanel.tsx +41 -15
- frontend/src/components/ConnectDB.tsx +12 -2
- frontend/src/components/Header.tsx +3 -3
- frontend/src/components/LeftSidebar.tsx +77 -64
- frontend/src/lib/api.ts +11 -0
- frontend/src/store/useStore.ts +12 -0
backend/api/demo.py
CHANGED
|
@@ -442,6 +442,55 @@ async def execute_query_stream(req: ExecuteQueryRequest):
|
|
| 442 |
return EventSourceResponse(event_generator())
|
| 443 |
|
| 444 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
# ─── /api/benchmark-questions ────────────────────────────────────
|
| 446 |
|
| 447 |
@router.get("/benchmark-questions")
|
|
|
|
| 442 |
return EventSourceResponse(event_generator())
|
| 443 |
|
| 444 |
|
| 445 |
+
# ─── /api/suggest-questions ──────────────────────────────────────
|
| 446 |
+
|
| 447 |
+
@router.get("/suggest-questions")
|
| 448 |
+
async def suggest_questions():
|
| 449 |
+
"""
|
| 450 |
+
Generate example questions based on the currently active database schema.
|
| 451 |
+
Returns up to 5 short natural-language questions the user might want to ask.
|
| 452 |
+
"""
|
| 453 |
+
from env.sql_env import _make_client, _MODEL
|
| 454 |
+
from env.database import get_schema_info as _get_schema
|
| 455 |
+
|
| 456 |
+
schema = _get_schema()
|
| 457 |
+
client = _make_client()
|
| 458 |
+
try:
|
| 459 |
+
resp = await client.chat.completions.create(
|
| 460 |
+
model=_MODEL,
|
| 461 |
+
messages=[
|
| 462 |
+
{
|
| 463 |
+
"role": "system",
|
| 464 |
+
"content": (
|
| 465 |
+
"You are a helpful data analyst. Given a database schema, "
|
| 466 |
+
"return ONLY a JSON array of 5 short natural-language questions "
|
| 467 |
+
"(5-10 words each) a user might want to ask about the data. "
|
| 468 |
+
"No markdown, no explanation — just the JSON array."
|
| 469 |
+
),
|
| 470 |
+
},
|
| 471 |
+
{
|
| 472 |
+
"role": "user",
|
| 473 |
+
"content": f"Schema:\n{schema}\n\nGenerate 5 example questions.",
|
| 474 |
+
},
|
| 475 |
+
],
|
| 476 |
+
temperature=0.7,
|
| 477 |
+
max_tokens=250,
|
| 478 |
+
)
|
| 479 |
+
raw = (resp.choices[0].message.content or "").strip()
|
| 480 |
+
# Strip markdown fences if present
|
| 481 |
+
if raw.startswith("```"):
|
| 482 |
+
raw = raw.split("```")[1]
|
| 483 |
+
if raw.startswith("json"):
|
| 484 |
+
raw = raw[4:]
|
| 485 |
+
questions = json.loads(raw)
|
| 486 |
+
if not isinstance(questions, list):
|
| 487 |
+
questions = []
|
| 488 |
+
return {"questions": [str(q) for q in questions[:5]]}
|
| 489 |
+
except Exception as e:
|
| 490 |
+
logger.error("suggest-questions failed: %s", e)
|
| 491 |
+
return {"questions": []}
|
| 492 |
+
|
| 493 |
+
|
| 494 |
# ─── /api/benchmark-questions ────────────────────────────────────
|
| 495 |
|
| 496 |
@router.get("/benchmark-questions")
|
frontend/src/App.tsx
CHANGED
|
@@ -28,7 +28,7 @@ export default function App() {
|
|
| 28 |
const [demoOpen, setDemoOpen] = useState(false)
|
| 29 |
const [connectDbOpen, setConnectDbOpen] = useState(false)
|
| 30 |
|
| 31 |
-
const { theme, setDbSeeded, setTables, setSchemaGraph, setDbLabel, taskDifficulty } = useStore()
|
| 32 |
|
| 33 |
// Apply theme on mount / change
|
| 34 |
useEffect(() => {
|
|
@@ -92,6 +92,11 @@ export default function App() {
|
|
| 92 |
setRightOpen(false)
|
| 93 |
}, [activeTab])
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
return (
|
| 96 |
<div
|
| 97 |
className="h-screen flex flex-col overflow-hidden theme-bg-primary theme-text-primary"
|
|
@@ -153,7 +158,7 @@ export default function App() {
|
|
| 153 |
className="flex items-center gap-1 px-2 sm:px-4 py-2.5 border-b theme-border shrink-0 overflow-x-auto scrollbar-none"
|
| 154 |
style={{ background: 'var(--bg-secondary)' }}
|
| 155 |
>
|
| 156 |
-
{TABS.map((tab) => (
|
| 157 |
<button
|
| 158 |
key={tab.id}
|
| 159 |
onClick={() => setActiveTab(tab.id)}
|
|
|
|
| 28 |
const [demoOpen, setDemoOpen] = useState(false)
|
| 29 |
const [connectDbOpen, setConnectDbOpen] = useState(false)
|
| 30 |
|
| 31 |
+
const { theme, setDbSeeded, setTables, setSchemaGraph, setDbLabel, taskDifficulty, isCustomDb } = useStore()
|
| 32 |
|
| 33 |
// Apply theme on mount / change
|
| 34 |
useEffect(() => {
|
|
|
|
| 92 |
setRightOpen(false)
|
| 93 |
}, [activeTab])
|
| 94 |
|
| 95 |
+
// If custom DB is enabled while benchmark tab is active, switch to chat
|
| 96 |
+
useEffect(() => {
|
| 97 |
+
if (isCustomDb && activeTab === 'benchmark') setActiveTab('chat')
|
| 98 |
+
}, [isCustomDb, activeTab])
|
| 99 |
+
|
| 100 |
return (
|
| 101 |
<div
|
| 102 |
className="h-screen flex flex-col overflow-hidden theme-bg-primary theme-text-primary"
|
|
|
|
| 158 |
className="flex items-center gap-1 px-2 sm:px-4 py-2.5 border-b theme-border shrink-0 overflow-x-auto scrollbar-none"
|
| 159 |
style={{ background: 'var(--bg-secondary)' }}
|
| 160 |
>
|
| 161 |
+
{TABS.filter((tab) => !(isCustomDb && tab.id === 'benchmark')).map((tab) => (
|
| 162 |
<button
|
| 163 |
key={tab.id}
|
| 164 |
onClick={() => setActiveTab(tab.id)}
|
frontend/src/components/ChatPanel.tsx
CHANGED
|
@@ -154,9 +154,28 @@ const SUGGESTED: Record<string, string[]> = {
|
|
| 154 |
hard: ['Rolling 7-day revenue', 'Seller ranking with rank change', 'Cohort retention analysis'],
|
| 155 |
}
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
function EmptyState({ onSelect }: { onSelect: (q: string) => void }) {
|
| 158 |
-
const { taskDifficulty } = useStore()
|
| 159 |
-
const suggestions = SUGGESTED[taskDifficulty] ?? SUGGESTED.easy
|
| 160 |
|
| 161 |
return (
|
| 162 |
<div className="flex flex-col items-center justify-center h-full gap-6 px-8 text-center">
|
|
@@ -176,18 +195,22 @@ function EmptyState({ onSelect }: { onSelect: (q: string) => void }) {
|
|
| 176 |
|
| 177 |
<div className="flex flex-col gap-2 w-full max-w-sm">
|
| 178 |
<div className="text-[10px] text-gray-600 uppercase tracking-wider mb-0.5">
|
| 179 |
-
Try these queries
|
| 180 |
</div>
|
| 181 |
-
{
|
| 182 |
-
<
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
</div>
|
| 192 |
</div>
|
| 193 |
)
|
|
@@ -413,6 +436,7 @@ export function ChatPanel() {
|
|
| 413 |
taskId, taskDifficulty,
|
| 414 |
optimizingBanner, setOptimizingBanner,
|
| 415 |
promptGeneration,
|
|
|
|
| 416 |
} = useStore()
|
| 417 |
|
| 418 |
const [input, setInput] = useState('')
|
|
@@ -546,7 +570,9 @@ export function ChatPanel() {
|
|
| 546 |
}
|
| 547 |
}
|
| 548 |
|
| 549 |
-
const suggestions =
|
|
|
|
|
|
|
| 550 |
|
| 551 |
return (
|
| 552 |
<div className="flex flex-col h-full">
|
|
@@ -615,7 +641,7 @@ export function ChatPanel() {
|
|
| 615 |
value={input}
|
| 616 |
onChange={(e) => setInput(e.target.value)}
|
| 617 |
onKeyDown={handleKeyDown}
|
| 618 |
-
placeholder=
|
| 619 |
disabled={isExecuting}
|
| 620 |
rows={1}
|
| 621 |
className="w-full px-3 py-2.5 pr-10 text-sm text-white rounded-xl border border-white/[0.06] bg-white/[0.03] placeholder-gray-600 resize-none focus:outline-none focus:border-violet-500/40 focus:bg-white/[0.05] transition-all disabled:opacity-50"
|
|
|
|
| 154 |
hard: ['Rolling 7-day revenue', 'Seller ranking with rank change', 'Cohort retention analysis'],
|
| 155 |
}
|
| 156 |
|
| 157 |
+
function SuggestionSkeleton() {
|
| 158 |
+
return (
|
| 159 |
+
<div className="flex flex-col gap-2 w-full max-w-sm">
|
| 160 |
+
{[90, 110, 80].map((w, i) => (
|
| 161 |
+
<div
|
| 162 |
+
key={i}
|
| 163 |
+
className="flex items-center gap-2 px-3 py-2.5 rounded-xl border border-white/[0.06] bg-white/[0.02]"
|
| 164 |
+
>
|
| 165 |
+
<div className="w-1 h-3 rounded bg-violet-500/20 animate-pulse shrink-0" />
|
| 166 |
+
<div
|
| 167 |
+
className="h-3 rounded bg-white/8 animate-pulse"
|
| 168 |
+
style={{ width: w }}
|
| 169 |
+
/>
|
| 170 |
+
</div>
|
| 171 |
+
))}
|
| 172 |
+
</div>
|
| 173 |
+
)
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
function EmptyState({ onSelect }: { onSelect: (q: string) => void }) {
|
| 177 |
+
const { taskDifficulty, isCustomDb, customDbSuggestions, suggestionsLoading } = useStore()
|
| 178 |
+
const suggestions = isCustomDb ? customDbSuggestions : (SUGGESTED[taskDifficulty] ?? SUGGESTED.easy)
|
| 179 |
|
| 180 |
return (
|
| 181 |
<div className="flex flex-col items-center justify-center h-full gap-6 px-8 text-center">
|
|
|
|
| 195 |
|
| 196 |
<div className="flex flex-col gap-2 w-full max-w-sm">
|
| 197 |
<div className="text-[10px] text-gray-600 uppercase tracking-wider mb-0.5">
|
| 198 |
+
{isCustomDb && suggestionsLoading ? 'Generating suggestions…' : 'Try these queries'}
|
| 199 |
</div>
|
| 200 |
+
{isCustomDb && suggestionsLoading ? (
|
| 201 |
+
<SuggestionSkeleton />
|
| 202 |
+
) : suggestions.length > 0 ? (
|
| 203 |
+
suggestions.map((q) => (
|
| 204 |
+
<button
|
| 205 |
+
key={q}
|
| 206 |
+
onClick={() => onSelect(q)}
|
| 207 |
+
className="flex items-center gap-2 px-3 py-2.5 rounded-xl border border-white/[0.06] bg-white/[0.02] hover:bg-white/[0.05] hover:border-violet-500/30 transition-all text-left group"
|
| 208 |
+
>
|
| 209 |
+
<span className="text-violet-500 shrink-0 group-hover:text-violet-400">›</span>
|
| 210 |
+
<span className="text-xs text-gray-300">{q}</span>
|
| 211 |
+
</button>
|
| 212 |
+
))
|
| 213 |
+
) : null}
|
| 214 |
</div>
|
| 215 |
</div>
|
| 216 |
)
|
|
|
|
| 436 |
taskId, taskDifficulty,
|
| 437 |
optimizingBanner, setOptimizingBanner,
|
| 438 |
promptGeneration,
|
| 439 |
+
isCustomDb, customDbSuggestions,
|
| 440 |
} = useStore()
|
| 441 |
|
| 442 |
const [input, setInput] = useState('')
|
|
|
|
| 570 |
}
|
| 571 |
}
|
| 572 |
|
| 573 |
+
const suggestions = isCustomDb
|
| 574 |
+
? customDbSuggestions
|
| 575 |
+
: (SUGGESTED[taskDifficulty] ?? SUGGESTED.easy)
|
| 576 |
|
| 577 |
return (
|
| 578 |
<div className="flex flex-col h-full">
|
|
|
|
| 641 |
value={input}
|
| 642 |
onChange={(e) => setInput(e.target.value)}
|
| 643 |
onKeyDown={handleKeyDown}
|
| 644 |
+
placeholder={isCustomDb ? 'Ask anything about your data…' : 'Ask about products, orders, sellers...'}
|
| 645 |
disabled={isExecuting}
|
| 646 |
rows={1}
|
| 647 |
className="w-full px-3 py-2.5 pr-10 text-sm text-white rounded-xl border border-white/[0.06] bg-white/[0.03] placeholder-gray-600 resize-none focus:outline-none focus:border-violet-500/40 focus:bg-white/[0.05] transition-all disabled:opacity-50"
|
frontend/src/components/ConnectDB.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { useState } from 'react'
|
|
| 2 |
import { motion } from 'framer-motion'
|
| 3 |
import { X, PlugZap, Database, CheckCircle2, XCircle, Loader2, RotateCcw } from 'lucide-react'
|
| 4 |
import { useStore } from '../store/useStore'
|
| 5 |
-
import { connectExternalDb } from '../lib/api'
|
| 6 |
|
| 7 |
interface ConnectDBProps {
|
| 8 |
onClose: () => void
|
|
@@ -21,7 +21,7 @@ const POSTGRES_EXAMPLES = [
|
|
| 21 |
]
|
| 22 |
|
| 23 |
export function ConnectDB({ onClose }: ConnectDBProps) {
|
| 24 |
-
const { dbLabel, setDbLabel, setTables, setDbSeeded } = useStore()
|
| 25 |
const [dbType, setDbType] = useState<DbType>('sqlite')
|
| 26 |
const [value, setValue] = useState('')
|
| 27 |
const [status, setStatus] = useState<'idle' | 'connecting' | 'success' | 'error'>('idle')
|
|
@@ -51,8 +51,15 @@ export function ConnectDB({ onClose }: ConnectDBProps) {
|
|
| 51 |
setDbLabel(res.dbLabel)
|
| 52 |
setTables(res.tables)
|
| 53 |
setDbSeeded(true)
|
|
|
|
| 54 |
setStatus('success')
|
| 55 |
setMessage(res.message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
} else {
|
| 57 |
setStatus('error')
|
| 58 |
setMessage(res.message)
|
|
@@ -71,6 +78,9 @@ export function ConnectDB({ onClose }: ConnectDBProps) {
|
|
| 71 |
setDbLabel(res.dbLabel)
|
| 72 |
setTables(res.tables)
|
| 73 |
setDbSeeded(true)
|
|
|
|
|
|
|
|
|
|
| 74 |
setStatus('success')
|
| 75 |
setMessage('Reset to built-in benchmark database')
|
| 76 |
} else {
|
|
|
|
| 2 |
import { motion } from 'framer-motion'
|
| 3 |
import { X, PlugZap, Database, CheckCircle2, XCircle, Loader2, RotateCcw } from 'lucide-react'
|
| 4 |
import { useStore } from '../store/useStore'
|
| 5 |
+
import { connectExternalDb, fetchSuggestQuestions } from '../lib/api'
|
| 6 |
|
| 7 |
interface ConnectDBProps {
|
| 8 |
onClose: () => void
|
|
|
|
| 21 |
]
|
| 22 |
|
| 23 |
export function ConnectDB({ onClose }: ConnectDBProps) {
|
| 24 |
+
const { dbLabel, setDbLabel, setTables, setDbSeeded, setIsCustomDb, setCustomDbSuggestions, setSuggestionsLoading } = useStore()
|
| 25 |
const [dbType, setDbType] = useState<DbType>('sqlite')
|
| 26 |
const [value, setValue] = useState('')
|
| 27 |
const [status, setStatus] = useState<'idle' | 'connecting' | 'success' | 'error'>('idle')
|
|
|
|
| 51 |
setDbLabel(res.dbLabel)
|
| 52 |
setTables(res.tables)
|
| 53 |
setDbSeeded(true)
|
| 54 |
+
setIsCustomDb(true)
|
| 55 |
setStatus('success')
|
| 56 |
setMessage(res.message)
|
| 57 |
+
// Start generating suggestions in the background (cached by DSN)
|
| 58 |
+
setCustomDbSuggestions([])
|
| 59 |
+
setSuggestionsLoading(true)
|
| 60 |
+
fetchSuggestQuestions(dsn)
|
| 61 |
+
.then((qs) => { setCustomDbSuggestions(qs); setSuggestionsLoading(false) })
|
| 62 |
+
.catch(() => setSuggestionsLoading(false))
|
| 63 |
} else {
|
| 64 |
setStatus('error')
|
| 65 |
setMessage(res.message)
|
|
|
|
| 78 |
setDbLabel(res.dbLabel)
|
| 79 |
setTables(res.tables)
|
| 80 |
setDbSeeded(true)
|
| 81 |
+
setIsCustomDb(false)
|
| 82 |
+
setCustomDbSuggestions([])
|
| 83 |
+
setSuggestionsLoading(false)
|
| 84 |
setStatus('success')
|
| 85 |
setMessage('Reset to built-in benchmark database')
|
| 86 |
} else {
|
frontend/src/components/Header.tsx
CHANGED
|
@@ -48,9 +48,9 @@ export function Header({ onToggleLeft, onToggleRight, onDemo, onConnectDb }: Hea
|
|
| 48 |
<div className="flex items-center gap-2 sm:gap-3">
|
| 49 |
{/* Connection status */}
|
| 50 |
{dbSeeded ? (
|
| 51 |
-
<div className="hidden sm:flex items-center gap-1.5 text-[10px] text-green-400">
|
| 52 |
-
<span className="w-1.5 h-1.5 rounded-full bg-green-400 inline-block" />
|
| 53 |
-
|
| 54 |
</div>
|
| 55 |
) : (
|
| 56 |
<div className="hidden sm:flex items-center gap-1.5 text-[10px] text-amber-400">
|
|
|
|
| 48 |
<div className="flex items-center gap-2 sm:gap-3">
|
| 49 |
{/* Connection status */}
|
| 50 |
{dbSeeded ? (
|
| 51 |
+
<div className="hidden sm:flex items-center gap-1.5 text-[10px] text-green-400 max-w-[140px] truncate">
|
| 52 |
+
<span className="w-1.5 h-1.5 rounded-full bg-green-400 inline-block shrink-0" />
|
| 53 |
+
<span className="truncate">{dbLabel}</span>
|
| 54 |
</div>
|
| 55 |
) : (
|
| 56 |
<div className="hidden sm:flex items-center gap-1.5 text-[10px] text-amber-400">
|
frontend/src/components/LeftSidebar.tsx
CHANGED
|
@@ -11,42 +11,44 @@ const DIFFICULTY_CONFIG: Record<Difficulty, { label: string; bg: string; text: s
|
|
| 11 |
}
|
| 12 |
|
| 13 |
export function LeftSidebar() {
|
| 14 |
-
const { tables, taskDifficulty, setTaskDifficulty, dbSeeded } = useStore()
|
| 15 |
const [tablesExpanded, setTablesExpanded] = useState(true)
|
| 16 |
|
| 17 |
const cfg = DIFFICULTY_CONFIG[taskDifficulty]
|
| 18 |
|
| 19 |
return (
|
| 20 |
<div className="flex flex-col gap-4 py-1">
|
| 21 |
-
{/* Task Difficulty */}
|
| 22 |
-
|
| 23 |
-
<
|
| 24 |
-
<
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
| 50 |
|
| 51 |
{/* Schema Tables */}
|
| 52 |
<section>
|
|
@@ -114,44 +116,55 @@ export function LeftSidebar() {
|
|
| 114 |
className="rounded-xl border border-white/[0.05] p-3 text-[11px] text-gray-500 leading-relaxed"
|
| 115 |
style={{ background: 'var(--bg-card)' }}
|
| 116 |
>
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
<
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
<
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
</div>
|
| 133 |
</section>
|
| 134 |
|
| 135 |
-
{/* Current task badge */}
|
| 136 |
-
|
| 137 |
-
<
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
<
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
</div>
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
? 'Simple SELECT queries, basic filtering and aggregation'
|
| 149 |
-
: taskDifficulty === 'medium'
|
| 150 |
-
? 'Multi-table JOINs, GROUP BY, subqueries, window functions'
|
| 151 |
-
: 'Complex CTEs, rolling aggregations, cohort analysis, ranking'}
|
| 152 |
-
</p>
|
| 153 |
-
</div>
|
| 154 |
-
</section>
|
| 155 |
</div>
|
| 156 |
)
|
| 157 |
}
|
|
|
|
| 11 |
}
|
| 12 |
|
| 13 |
export function LeftSidebar() {
|
| 14 |
+
const { tables, taskDifficulty, setTaskDifficulty, dbSeeded, isCustomDb, dbLabel } = useStore()
|
| 15 |
const [tablesExpanded, setTablesExpanded] = useState(true)
|
| 16 |
|
| 17 |
const cfg = DIFFICULTY_CONFIG[taskDifficulty]
|
| 18 |
|
| 19 |
return (
|
| 20 |
<div className="flex flex-col gap-4 py-1">
|
| 21 |
+
{/* Task Difficulty — hidden for custom databases */}
|
| 22 |
+
{!isCustomDb && (
|
| 23 |
+
<section>
|
| 24 |
+
<div className="text-[10px] font-semibold text-gray-500 uppercase tracking-widest mb-2 flex items-center gap-1.5">
|
| 25 |
+
<GitFork size={10} className="text-violet-400" />
|
| 26 |
+
Task Difficulty
|
| 27 |
+
</div>
|
| 28 |
+
<div className="flex flex-col gap-1">
|
| 29 |
+
{(Object.keys(DIFFICULTY_CONFIG) as Difficulty[]).map((d) => {
|
| 30 |
+
const c = DIFFICULTY_CONFIG[d]
|
| 31 |
+
const active = d === taskDifficulty
|
| 32 |
+
return (
|
| 33 |
+
<button
|
| 34 |
+
key={d}
|
| 35 |
+
onClick={() => setTaskDifficulty(d)}
|
| 36 |
+
className={`flex items-center justify-between px-3 py-2 rounded-lg border text-xs font-medium transition-all ${
|
| 37 |
+
active
|
| 38 |
+
? `${c.bg} ${c.text} ${c.border}`
|
| 39 |
+
: 'border-transparent text-gray-500 hover:text-gray-300 hover:bg-white/5'
|
| 40 |
+
}`}
|
| 41 |
+
>
|
| 42 |
+
<span>{c.label}</span>
|
| 43 |
+
{active && (
|
| 44 |
+
<span className={`text-[9px] font-mono ${c.text} opacity-70`}>selected</span>
|
| 45 |
+
)}
|
| 46 |
+
</button>
|
| 47 |
+
)
|
| 48 |
+
})}
|
| 49 |
+
</div>
|
| 50 |
+
</section>
|
| 51 |
+
)}
|
| 52 |
|
| 53 |
{/* Schema Tables */}
|
| 54 |
<section>
|
|
|
|
| 116 |
className="rounded-xl border border-white/[0.05] p-3 text-[11px] text-gray-500 leading-relaxed"
|
| 117 |
style={{ background: 'var(--bg-card)' }}
|
| 118 |
>
|
| 119 |
+
{isCustomDb ? (
|
| 120 |
+
<p className="text-gray-600 italic">
|
| 121 |
+
Connected to <span className="text-violet-400 not-italic font-medium">{dbLabel}</span>.
|
| 122 |
+
Ask questions about your data in natural language.
|
| 123 |
+
</p>
|
| 124 |
+
) : (
|
| 125 |
+
<>
|
| 126 |
+
<p className="mb-2 text-gray-400 font-medium">E-Commerce Marketplace</p>
|
| 127 |
+
<p>
|
| 128 |
+
Multi-vendor marketplace with products, orders, sellers, users, and reviews.
|
| 129 |
+
Supports complex analytical queries across sales, inventory, and user behavior.
|
| 130 |
+
</p>
|
| 131 |
+
<div className="mt-2 flex flex-wrap gap-1">
|
| 132 |
+
{['Products', 'Orders', 'Sellers', 'Users', 'Reviews', 'Categories'].map((t) => (
|
| 133 |
+
<span
|
| 134 |
+
key={t}
|
| 135 |
+
className="text-[9px] px-1.5 py-0.5 rounded border border-white/[0.06] text-gray-600"
|
| 136 |
+
>
|
| 137 |
+
{t}
|
| 138 |
+
</span>
|
| 139 |
+
))}
|
| 140 |
+
</div>
|
| 141 |
+
</>
|
| 142 |
+
)}
|
| 143 |
</div>
|
| 144 |
</section>
|
| 145 |
|
| 146 |
+
{/* Current task badge — hidden for custom databases */}
|
| 147 |
+
{!isCustomDb && (
|
| 148 |
+
<section>
|
| 149 |
+
<div
|
| 150 |
+
className={`rounded-xl border ${cfg.border} ${cfg.bg} p-3 flex flex-col gap-1.5`}
|
| 151 |
+
>
|
| 152 |
+
<div className="flex items-center justify-between">
|
| 153 |
+
<span className={`text-[10px] font-semibold uppercase tracking-wider ${cfg.text}`}>
|
| 154 |
+
Current Task
|
| 155 |
+
</span>
|
| 156 |
+
<span className={`text-[10px] font-mono ${cfg.text}`}>{cfg.label}</span>
|
| 157 |
+
</div>
|
| 158 |
+
<p className="text-[11px] text-gray-400 leading-relaxed">
|
| 159 |
+
{taskDifficulty === 'easy'
|
| 160 |
+
? 'Simple SELECT queries, basic filtering and aggregation'
|
| 161 |
+
: taskDifficulty === 'medium'
|
| 162 |
+
? 'Multi-table JOINs, GROUP BY, subqueries, window functions'
|
| 163 |
+
: 'Complex CTEs, rolling aggregations, cohort analysis, ranking'}
|
| 164 |
+
</p>
|
| 165 |
</div>
|
| 166 |
+
</section>
|
| 167 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
</div>
|
| 169 |
)
|
| 170 |
}
|
frontend/src/lib/api.ts
CHANGED
|
@@ -108,6 +108,17 @@ export async function fetchBenchmarkQuestions(
|
|
| 108 |
return res.json()
|
| 109 |
}
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
export async function connectExternalDb(path: string): Promise<{ success: boolean; message: string; tables: { name: string; rows: number }[]; dbLabel: string }> {
|
| 112 |
const res = await fetch(`${BASE_URL}/api/connect-db`, {
|
| 113 |
method: 'POST',
|
|
|
|
| 108 |
return res.json()
|
| 109 |
}
|
| 110 |
|
| 111 |
+
const _suggestionsCache = new Map<string, string[]>()
|
| 112 |
+
|
| 113 |
+
export async function fetchSuggestQuestions(cacheKey: string): Promise<string[]> {
|
| 114 |
+
if (_suggestionsCache.has(cacheKey)) return _suggestionsCache.get(cacheKey)!
|
| 115 |
+
const res = await fetch(`${BASE_URL}/api/suggest-questions`)
|
| 116 |
+
if (!res.ok) return []
|
| 117 |
+
const data = await res.json() as { questions: string[] }
|
| 118 |
+
_suggestionsCache.set(cacheKey, data.questions ?? [])
|
| 119 |
+
return data.questions ?? []
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
export async function connectExternalDb(path: string): Promise<{ success: boolean; message: string; tables: { name: string; rows: number }[]; dbLabel: string }> {
|
| 123 |
const res = await fetch(`${BASE_URL}/api/connect-db`, {
|
| 124 |
method: 'POST',
|
frontend/src/store/useStore.ts
CHANGED
|
@@ -23,6 +23,12 @@ interface Store {
|
|
| 23 |
// DB
|
| 24 |
dbLabel: string
|
| 25 |
setDbLabel: (label: string) => void
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
// Init / DB
|
| 28 |
dbSeeded: boolean
|
|
@@ -104,6 +110,12 @@ export const useStore = create<Store>((set) => ({
|
|
| 104 |
// DB
|
| 105 |
dbLabel: 'benchmark (built-in)',
|
| 106 |
setDbLabel: (label) => set({ dbLabel: label }),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
// Init
|
| 109 |
dbSeeded: false,
|
|
|
|
| 23 |
// DB
|
| 24 |
dbLabel: string
|
| 25 |
setDbLabel: (label: string) => void
|
| 26 |
+
isCustomDb: boolean
|
| 27 |
+
setIsCustomDb: (v: boolean) => void
|
| 28 |
+
customDbSuggestions: string[]
|
| 29 |
+
setCustomDbSuggestions: (qs: string[]) => void
|
| 30 |
+
suggestionsLoading: boolean
|
| 31 |
+
setSuggestionsLoading: (v: boolean) => void
|
| 32 |
|
| 33 |
// Init / DB
|
| 34 |
dbSeeded: boolean
|
|
|
|
| 110 |
// DB
|
| 111 |
dbLabel: 'benchmark (built-in)',
|
| 112 |
setDbLabel: (label) => set({ dbLabel: label }),
|
| 113 |
+
isCustomDb: false,
|
| 114 |
+
setIsCustomDb: (v) => set({ isCustomDb: v }),
|
| 115 |
+
customDbSuggestions: [],
|
| 116 |
+
setCustomDbSuggestions: (qs) => set({ customDbSuggestions: qs }),
|
| 117 |
+
suggestionsLoading: false,
|
| 118 |
+
setSuggestionsLoading: (v) => set({ suggestionsLoading: v }),
|
| 119 |
|
| 120 |
// Init
|
| 121 |
dbSeeded: false,
|