sql-agent-openenv / frontend /src /components /PromptEvolution.tsx
ar9avg's picture
fix
44ef33f
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>
)
}