Spaces:
Running
Running
| import { useState, useEffect } from 'react' | |
| import { motion, AnimatePresence } from 'framer-motion' | |
| import { Brain, ChevronDown, ChevronUp, Zap, History } from 'lucide-react' | |
| import { useStore } from '../store/useStore' | |
| import { fetchPromptHistory } from '../lib/api' | |
| const SEED_PROMPT = `You are a SQL expert. Given a natural language question and a SQLite database schema, write a correct SQL query. | |
| Rules: | |
| - Output ONLY the SQL query, nothing else | |
| - No markdown, no code fences, no explanation | |
| - Use SQLite syntax | |
| - Always qualify column names with table aliases when using JOINs` | |
| export function PromptEvolution() { | |
| const { currentPrompt, promptGeneration, promptHistory, setPromptData } = useStore() | |
| const [expanded, setExpanded] = useState(false) | |
| const [historyExpanded, setHistoryExpanded] = useState(false) | |
| const [loading, setLoading] = useState(false) | |
| const prompt = currentPrompt || SEED_PROMPT | |
| const generation = promptGeneration | |
| const [queryCount, setQueryCount] = useState(0) | |
| const [optimizeEvery, setOptimizeEvery] = useState(4) | |
| const [cycleProgress, setCycleProgress] = useState(0) | |
| const loadHistory = async () => { | |
| setLoading(true) | |
| try { | |
| const data = await fetchPromptHistory() | |
| setPromptData(data) | |
| const d = data as Record<string, unknown> | |
| if (d.queryCount !== undefined) setQueryCount(d.queryCount as number) | |
| if (d.optimizeEvery !== undefined) setOptimizeEvery(d.optimizeEvery as number) | |
| if (d.cycleProgress !== undefined) setCycleProgress(d.cycleProgress as number) | |
| } catch { | |
| // noop | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| useEffect(() => { | |
| void loadHistory() | |
| // Poll for updates every 30s | |
| const interval = setInterval(() => void loadHistory(), 30000) | |
| return () => clearInterval(interval) | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, []) | |
| return ( | |
| <div className="flex flex-col gap-2"> | |
| {/* Header */} | |
| <button | |
| onClick={() => setExpanded((v) => !v)} | |
| className="flex items-center justify-between w-full group" | |
| > | |
| <div className="flex items-center gap-2"> | |
| <Brain size={14} className="text-violet-400" /> | |
| <span className="text-xs font-semibold text-white/70">System Prompt</span> | |
| {generation > 0 ? ( | |
| <span className="text-[10px] bg-violet-500/20 text-violet-300 border border-violet-500/30 rounded-full px-2 py-0.5"> | |
| Gen {generation} · Optimized | |
| </span> | |
| ) : ( | |
| <span className="text-[10px] bg-white/5 text-gray-500 rounded-full px-2 py-0.5"> | |
| Seed | |
| </span> | |
| )} | |
| </div> | |
| {expanded ? ( | |
| <ChevronUp size={13} className="text-gray-500" /> | |
| ) : ( | |
| <ChevronDown size={13} className="text-gray-500" /> | |
| )} | |
| </button> | |
| {/* Progress toward next optimization */} | |
| {queryCount > 0 && ( | |
| <div className="flex flex-col gap-1"> | |
| <div className="flex items-center justify-between text-[9px] text-gray-600"> | |
| <span>{queryCount} queries processed</span> | |
| <span className="text-gray-700"> | |
| {cycleProgress}/{optimizeEvery} · optimizes every {optimizeEvery} | |
| </span> | |
| </div> | |
| <div className="h-1 bg-white/5 rounded-full overflow-hidden"> | |
| <div | |
| className="h-full rounded-full bg-violet-500/50 transition-all duration-500" | |
| style={{ width: `${(cycleProgress / optimizeEvery) * 100}%` }} | |
| /> | |
| </div> | |
| </div> | |
| )} | |
| <AnimatePresence> | |
| {expanded && ( | |
| <motion.div | |
| initial={{ opacity: 0, height: 0 }} | |
| animate={{ opacity: 1, height: 'auto' }} | |
| exit={{ opacity: 0, height: 0 }} | |
| transition={{ duration: 0.2 }} | |
| className="overflow-hidden" | |
| > | |
| {/* Prompt preview */} | |
| <div className="max-h-40 overflow-y-auto"> | |
| <pre className="text-[11px] font-mono text-violet-200/70 bg-violet-950/30 rounded-xl p-3 border border-violet-500/20 whitespace-pre-wrap leading-relaxed"> | |
| {prompt} | |
| </pre> | |
| </div> | |
| {/* History button */} | |
| {promptHistory.length > 0 && ( | |
| <button | |
| onClick={() => setHistoryExpanded((v) => !v)} | |
| className="mt-2 w-full flex items-center justify-center gap-2 px-3 py-2 text-xs font-medium bg-violet-600/15 text-violet-300 border border-violet-500/25 rounded-xl hover:bg-violet-600/25 hover:border-violet-500/40 transition-all" | |
| > | |
| <History size={12} /> | |
| {historyExpanded ? 'Hide' : 'View'} Evolution History | |
| <span className="text-[10px] text-violet-400/60 ml-1"> | |
| ({promptHistory.length} gen{promptHistory.length !== 1 ? 's' : ''}) | |
| </span> | |
| </button> | |
| )} | |
| {/* Generation history */} | |
| <AnimatePresence> | |
| {historyExpanded && promptHistory.length > 0 && ( | |
| <motion.div | |
| initial={{ height: 0, opacity: 0 }} | |
| animate={{ height: 'auto', opacity: 1 }} | |
| exit={{ height: 0, opacity: 0 }} | |
| transition={{ duration: 0.15 }} | |
| className="overflow-hidden mt-2" | |
| > | |
| <div className="flex flex-col gap-1.5"> | |
| <div className="text-[10px] text-gray-500 font-medium flex items-center gap-1"> | |
| <Zap size={10} className="text-violet-400" /> | |
| Optimization History | |
| </div> | |
| {promptHistory.map((snap) => ( | |
| <div | |
| key={snap.generation} | |
| className="border border-white/5 rounded-xl p-2.5 hover:border-white/10 hover:bg-white/[0.02] transition-all" | |
| > | |
| <div className="flex items-center justify-between mb-1"> | |
| <span className="text-[10px] font-semibold text-violet-400"> | |
| Generation {snap.generation} | |
| </span> | |
| <span className="text-[10px] font-mono text-green-400"> | |
| {(snap.score * 100).toFixed(0)}% | |
| </span> | |
| </div> | |
| <p className="text-[10px] text-gray-400 leading-relaxed line-clamp-2"> | |
| {snap.summary} | |
| </p> | |
| <p className="text-[9px] text-gray-600 mt-1">{snap.timestamp}</p> | |
| </div> | |
| ))} | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| {loading && ( | |
| <div className="flex items-center gap-2 text-[10px] text-gray-500 mt-2 px-1"> | |
| <span className="w-3 h-3 border border-violet-500/40 border-t-violet-400 rounded-full animate-spin inline-block" /> | |
| Loading history... | |
| </div> | |
| )} | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| ) | |
| } | |