ar9avg's picture
fix
17e7bd7
import { useState } from 'react'
import { motion } from 'framer-motion'
import { X, PlugZap, Database, CheckCircle2, XCircle, Loader2, RotateCcw } from 'lucide-react'
import { useStore } from '../store/useStore'
import { connectExternalDb, fetchSuggestQuestions } from '../lib/api'
interface ConnectDBProps {
onClose: () => void
}
type DbType = 'sqlite' | 'postgres'
const SQLITE_EXAMPLES = [
{ label: 'In-memory (blank)', value: ':memory:' },
{ label: 'Custom path', value: '/path/to/your/database.db' },
]
const POSTGRES_EXAMPLES = [
{ label: 'Local default', value: 'postgresql://postgres:password@localhost:5432/mydb' },
{ label: 'With SSL', value: 'postgresql://user:pass@host:5432/dbname?sslmode=require' },
]
export function ConnectDB({ onClose }: ConnectDBProps) {
const { dbLabel, setDbLabel, setTables, setDbSeeded, setIsCustomDb, setCustomDbSuggestions, setSuggestionsLoading } = useStore()
const [dbType, setDbType] = useState<DbType>('sqlite')
const [value, setValue] = useState('')
const [status, setStatus] = useState<'idle' | 'connecting' | 'success' | 'error'>('idle')
const [message, setMessage] = useState('')
const placeholder = dbType === 'postgres'
? 'postgresql://user:password@host:5432/dbname'
: '/path/to/database.db'
const examples = dbType === 'postgres' ? POSTGRES_EXAMPLES : SQLITE_EXAMPLES
const getDsn = () => {
if (dbType === 'postgres' && value.trim() && !value.trim().startsWith('postgresql://') && !value.trim().startsWith('postgres://')) {
return `postgresql://${value.trim()}`
}
return value.trim()
}
const handleConnect = async () => {
const dsn = getDsn()
if (!dsn) return
setStatus('connecting')
setMessage('')
try {
const res = await connectExternalDb(dsn)
if (res.success) {
setDbLabel(res.dbLabel)
setTables(res.tables)
setDbSeeded(true)
setIsCustomDb(true)
setStatus('success')
setMessage(res.message)
// Start generating suggestions in the background (cached by DSN)
setCustomDbSuggestions([])
setSuggestionsLoading(true)
fetchSuggestQuestions(dsn)
.then((qs) => { setCustomDbSuggestions(qs); setSuggestionsLoading(false) })
.catch(() => setSuggestionsLoading(false))
} else {
setStatus('error')
setMessage(res.message)
}
} catch (e) {
setStatus('error')
setMessage(e instanceof Error ? e.message : 'Connection failed')
}
}
const handleReset = async () => {
setStatus('connecting')
try {
const res = await connectExternalDb('/app/backend/data/benchmark.db')
if (res.success) {
setDbLabel(res.dbLabel)
setTables(res.tables)
setDbSeeded(true)
setIsCustomDb(false)
setCustomDbSuggestions([])
setSuggestionsLoading(false)
setStatus('success')
setMessage('Reset to built-in benchmark database')
} else {
setStatus('error')
setMessage(res.message)
}
} catch (e) {
setStatus('error')
setMessage(e instanceof Error ? e.message : 'Reset failed')
}
}
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[200] flex items-center justify-center p-4"
style={{ background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(4px)' }}
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
>
<motion.div
initial={{ scale: 0.95, opacity: 0, y: 8 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.95, opacity: 0 }}
transition={{ duration: 0.15 }}
className="w-full max-w-md rounded-2xl border shadow-2xl overflow-hidden"
style={{ background: 'var(--bg-secondary)', borderColor: 'var(--border-color)' }}
>
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b" style={{ borderColor: 'var(--border-color)' }}>
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: 'linear-gradient(135deg,#1e3a5f,#2d1b69)' }}>
<PlugZap size={13} className="text-white" />
</div>
<div>
<h2 className="text-sm font-semibold theme-text-primary">Connect Database</h2>
<p className="text-[10px] text-gray-500">SQLite file or PostgreSQL connection string</p>
</div>
</div>
<button onClick={onClose} className="p-1.5 rounded-lg hover:bg-white/5 text-gray-500 hover:text-gray-300 transition-colors">
<X size={15} />
</button>
</div>
{/* Current DB */}
<div className="px-5 py-3 border-b flex items-center gap-2" style={{ borderColor: 'var(--border-color)', background: 'var(--bg-tertiary)' }}>
<Database size={11} className="text-violet-400 shrink-0" />
<span className="text-[11px] text-gray-500">Active:</span>
<span className="text-[11px] font-semibold text-violet-400 truncate">{dbLabel}</span>
<button
onClick={handleReset}
className="ml-auto flex items-center gap-1 text-[10px] text-gray-600 hover:text-gray-400 transition-colors"
title="Reset to built-in demo database"
>
<RotateCcw size={9} />
Reset to demo
</button>
</div>
{/* Body */}
<div className="px-5 py-4 flex flex-col gap-4">
{/* DB Type toggle */}
<div>
<label className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider block mb-1.5">
Database Type
</label>
<div className="flex rounded-xl overflow-hidden border" style={{ borderColor: 'var(--border-color)' }}>
{(['sqlite', 'postgres'] as DbType[]).map((type) => (
<button
key={type}
onClick={() => { setDbType(type); setValue(''); setStatus('idle') }}
className="flex-1 py-2 text-xs font-medium transition-all"
style={{
background: dbType === type ? 'linear-gradient(135deg,#7c3aed,#2563eb)' : 'var(--bg-tertiary)',
color: dbType === type ? '#fff' : 'var(--text-secondary)',
}}
>
{type === 'sqlite' ? 'SQLite' : 'PostgreSQL'}
</button>
))}
</div>
</div>
<div>
<label className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider block mb-1.5">
{dbType === 'postgres' ? 'Connection String' : 'File Path'}
</label>
<input
type={dbType === 'postgres' ? 'text' : 'text'}
value={value}
onChange={(e) => { setValue(e.target.value); setStatus('idle') }}
onKeyDown={(e) => e.key === 'Enter' && void handleConnect()}
placeholder={placeholder}
className="w-full px-3 py-2.5 text-sm rounded-xl border focus:outline-none transition-all font-mono"
style={{
background: 'var(--bg-tertiary)',
borderColor: 'var(--border-color)',
color: 'var(--text-primary)',
}}
autoFocus
/>
{dbType === 'postgres' && (
<p className="text-[10px] text-gray-600 mt-1">
Format: <span className="font-mono text-gray-500">postgresql://user:password@host:port/dbname</span>
</p>
)}
</div>
{/* Quick examples */}
<div className="flex flex-col gap-1.5">
<span className="text-[10px] text-gray-600 uppercase tracking-wider">Quick select</span>
<div className="flex flex-wrap gap-1.5">
{examples.map((ex) => (
<button
key={ex.value}
onClick={() => { setValue(ex.value); setStatus('idle') }}
className="text-[10px] px-2.5 py-1 rounded-full border transition-all text-gray-500 hover:text-gray-300"
style={{ borderColor: 'var(--border-color)', background: 'var(--bg-tertiary)' }}
>
{ex.label}
</button>
))}
</div>
</div>
{/* Status message */}
{status !== 'idle' && (
<div className={`flex items-start gap-2 rounded-xl px-3 py-2.5 text-xs ${
status === 'success' ? 'bg-green-500/10 border border-green-500/20 text-green-400' :
status === 'error' ? 'bg-red-500/10 border border-red-500/20 text-red-400' :
'bg-violet-500/10 border border-violet-500/20 text-violet-400'
}`}>
{status === 'connecting' && <Loader2 size={12} className="animate-spin shrink-0 mt-0.5" />}
{status === 'success' && <CheckCircle2 size={12} className="shrink-0 mt-0.5" />}
{status === 'error' && <XCircle size={12} className="shrink-0 mt-0.5" />}
<span>{status === 'connecting' ? 'Connecting…' : message}</span>
</div>
)}
</div>
{/* Footer */}
<div className="px-5 pb-5 flex items-center justify-end gap-2">
<button
onClick={onClose}
className="px-4 py-2 rounded-xl text-xs font-medium text-gray-500 hover:text-gray-300 transition-colors"
>
{status === 'success' ? 'Close' : 'Cancel'}
</button>
<button
onClick={() => void handleConnect()}
disabled={!value.trim() || status === 'connecting'}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold text-white transition-all active:scale-95 disabled:opacity-40 disabled:cursor-not-allowed"
style={{ background: 'linear-gradient(135deg,#7c3aed,#2563eb)' }}
>
{status === 'connecting' ? (
<><Loader2 size={11} className="animate-spin" /> Connecting…</>
) : (
<><PlugZap size={11} /> Connect</>
)}
</button>
</div>
</motion.div>
</motion.div>
)
}