Spaces:
Sleeping
Sleeping
| 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> | |
| ) | |
| } | |