ar9avg's picture
fix
17e7bd7
import { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { MessageSquare, Target, GitFork, X } from 'lucide-react'
import { Header } from './components/Header'
import { LeftSidebar } from './components/LeftSidebar'
import { ChatPanel } from './components/ChatPanel'
import { BenchmarkPanel } from './components/BenchmarkPanel'
import { ERDiagram } from './components/ERDiagram'
import { RightSidebar } from './components/RightSidebar'
import { DemoMode } from './components/DemoMode'
import { ConnectDB } from './components/ConnectDB'
import { useStore } from './store/useStore'
import { fetchInit, fetchBenchmarkQuestions } from './lib/api'
type Tab = 'chat' | 'benchmark' | 'er'
const TABS: { id: Tab; label: string; icon: React.ReactNode }[] = [
{ id: 'chat', label: 'Chat', icon: <MessageSquare size={12} /> },
{ id: 'benchmark', label: 'Benchmark', icon: <Target size={12} /> },
{ id: 'er', label: 'ER Diagram', icon: <GitFork size={12} /> },
]
export default function App() {
const [activeTab, setActiveTab] = useState<Tab>('chat')
const [leftOpen, setLeftOpen] = useState(false)
const [rightOpen, setRightOpen] = useState(false)
const [demoOpen, setDemoOpen] = useState(false)
const [connectDbOpen, setConnectDbOpen] = useState(false)
const { theme, setDbSeeded, setTables, setSchemaGraph, setDbLabel, taskDifficulty, isCustomDb } = useStore()
// Apply theme on mount / change
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme)
}, [theme])
// Restore theme from storage on mount
useEffect(() => {
try {
const saved = localStorage.getItem('theme') as 'dark' | 'light' | null
if (saved) {
document.documentElement.setAttribute('data-theme', saved)
useStore.setState({ theme: saved })
}
} catch { /* noop */ }
}, [])
// Fetch init data
useEffect(() => {
fetchInit()
.then((d) => {
setDbSeeded(true)
setTables(d.tables)
if (d.dbLabel) setDbLabel(d.dbLabel)
// Lazy-load schema graph
fetch('/api/schema-graph')
.then((r) => r.json())
.then((g) => setSchemaGraph(g))
.catch(() => { /* noop */ })
})
.catch(() => { /* noop */ })
}, [setDbSeeded, setTables, setSchemaGraph])
// Load benchmark questions from API on mount
useEffect(() => {
const { setBenchmarkResults } = useStore.getState()
fetchBenchmarkQuestions(taskDifficulty)
.then(({ questions }) => {
setBenchmarkResults(
questions.map((q) => ({
id: q.id,
question: q.question,
difficulty: q.difficulty as 'easy' | 'medium' | 'hard',
status: 'pending' as const,
score: null,
sql: null,
reason: null,
attempts: null,
refRowCount: null,
agentRowCount: null,
}))
)
})
.catch(() => { /* noop */ })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Close mobile sidebars on tab change
useEffect(() => {
setLeftOpen(false)
setRightOpen(false)
}, [activeTab])
// If custom DB is enabled while benchmark tab is active, switch to chat
useEffect(() => {
if (isCustomDb && activeTab === 'benchmark') setActiveTab('chat')
}, [isCustomDb, activeTab])
return (
<div
className="h-screen flex flex-col overflow-hidden theme-bg-primary theme-text-primary"
style={{ fontFamily: 'ui-monospace,"SF Mono",Consolas,"Liberation Mono",monospace' }}
>
<Header
onToggleLeft={() => { setLeftOpen((v) => !v); setRightOpen(false) }}
onToggleRight={() => { setRightOpen((v) => !v); setLeftOpen(false) }}
onDemo={() => setDemoOpen(true)}
onConnectDb={() => setConnectDbOpen(true)}
/>
<AnimatePresence>
{demoOpen && <DemoMode onClose={() => setDemoOpen(false)} />}
</AnimatePresence>
<AnimatePresence>
{connectDbOpen && <ConnectDB onClose={() => setConnectDbOpen(false)} />}
</AnimatePresence>
<div className="flex flex-1 overflow-hidden relative">
{/* Overlay backdrop (mobile) */}
{(leftOpen || rightOpen) && (
<div
className="fixed inset-0 bg-black/50 z-30 lg:hidden"
onClick={() => { setLeftOpen(false); setRightOpen(false) }}
/>
)}
{/* LEFT SIDEBAR */}
<aside
className={`
fixed top-[53px] bottom-0 left-0 z-40 w-60 border-r theme-border flex flex-col overflow-y-auto
transition-transform duration-200 ease-out
lg:static lg:w-60 lg:shrink-0 lg:translate-x-0 lg:z-auto
${leftOpen ? 'translate-x-0' : '-translate-x-full'}
`}
style={{ background: 'var(--bg-secondary)' }}
>
<div className="flex items-center justify-between px-4 pt-3 pb-1 lg:hidden">
<span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">
Dataset & Tasks
</span>
<button
onClick={() => setLeftOpen(false)}
className="p-1 rounded hover:bg-white/5 text-gray-500"
>
<X size={14} />
</button>
</div>
<div className="flex-1 px-4 py-3">
<LeftSidebar />
</div>
</aside>
{/* CENTER: Tabbed panel */}
<main className="flex-1 flex flex-col overflow-hidden min-w-0">
{/* Tab bar */}
<div
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"
style={{ background: 'var(--bg-secondary)' }}
>
{TABS.filter((tab) => !(isCustomDb && tab.id === 'benchmark')).map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-1.5 px-2.5 sm:px-3 py-1.5 rounded-lg text-xs font-medium transition-all whitespace-nowrap shrink-0 ${
activeTab === tab.id
? 'bg-violet-600/20 text-violet-300 border border-violet-500/30'
: 'text-gray-500 hover:text-gray-300 hover:bg-white/5 border border-transparent'
}`}
>
{tab.icon}
<span>{tab.label}</span>
</button>
))}
</div>
{/* Tab content */}
<div className="flex-1 overflow-hidden relative">
<AnimatePresence mode="wait">
<motion.div
key={activeTab}
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className="absolute inset-0 flex flex-col overflow-hidden"
>
{activeTab === 'chat' && <ChatPanel />}
{activeTab === 'benchmark' && <BenchmarkPanel />}
{activeTab === 'er' && <ERDiagram />}
</motion.div>
</AnimatePresence>
</div>
</main>
{/* RIGHT SIDEBAR */}
<aside
className={`
fixed top-[53px] bottom-0 right-0 z-40 w-72 border-l theme-border flex flex-col overflow-hidden
transition-transform duration-200 ease-out
lg:static lg:w-72 lg:shrink-0 lg:translate-x-0 lg:z-auto
${rightOpen ? 'translate-x-0' : 'translate-x-full'}
`}
style={{ background: 'var(--bg-secondary)' }}
>
<div className="flex items-center justify-between px-4 pt-3 pb-1 lg:hidden">
<span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">
GEPA & RL
</span>
<button
onClick={() => setRightOpen(false)}
className="p-1 rounded hover:bg-white/5 text-gray-500"
>
<X size={14} />
</button>
</div>
<RightSidebar />
</aside>
</div>
</div>
)
}