Spaces:
Sleeping
Sleeping
import { useState, useEffect } from 'react'; | |
import { motion, AnimatePresence } from 'framer-motion'; | |
import { IconDatabase, IconBrandMysql, IconArrowRight, IconLoader2, IconAlertCircle, IconRefresh } from '@tabler/icons-react'; | |
import AIVisualization from '../components/AIVisualization'; | |
interface QueryResults { | |
results: any; | |
query: string; | |
visualization?: { | |
type: 'bar' | 'pie' | 'line'; | |
config: any; | |
}; | |
} | |
interface ApiError { | |
message: string; | |
details?: string | Record<string, string | null>; | |
} | |
export default function Home() { | |
const [dbConfig, setDbConfig] = useState({ | |
host: '', | |
port: '3306', | |
database: '', | |
username: '', | |
password: '' | |
}); | |
const [tables, setTables] = useState<string[]>([]); | |
const [selectedTable, setSelectedTable] = useState(''); | |
const [prompt, setPrompt] = useState(''); | |
const [results, setResults] = useState<QueryResults | null>(null); | |
const [loading, setLoading] = useState(false); | |
const [error, setError] = useState<ApiError | null>(null); | |
const [dbUri, setDbUri] = useState(''); | |
const [showDbConfig, setShowDbConfig] = useState(false); | |
useEffect(() => { | |
const uri = `mysql://${dbConfig.username}:${dbConfig.password}@${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`; | |
setDbUri(uri); | |
}, [dbConfig]); | |
const fetchTables = async () => { | |
if (!dbUri) return; | |
setLoading(true); | |
setError(null); | |
try { | |
const response = await fetch('/api/tables', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ dbUri }), | |
}); | |
const data = await response.json(); | |
if (!response.ok) { | |
throw new Error(data.message); | |
} | |
setTables(data.tables); | |
} catch (err: any) { | |
setError({ | |
message: 'Failed to fetch tables', | |
details: err.message | |
}); | |
} finally { | |
setLoading(false); | |
} | |
}; | |
const handleSubmit = async (e: React.FormEvent) => { | |
e.preventDefault(); | |
if (!selectedTable) { | |
setError({ | |
message: "Table selection required", | |
details: "Please select a table before submitting the query" | |
}); | |
return; | |
} | |
setLoading(true); | |
setError(null); | |
setResults(null); | |
try { | |
const response = await fetch('/api/sql', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ | |
dbUri, | |
userPrompt: `${prompt} from table ${selectedTable}` | |
}), | |
}); | |
const data = await response.json(); | |
if (!response.ok) { | |
throw { | |
message: data.message || 'An error occurred', | |
details: data.details | |
}; | |
} | |
setResults(data); | |
} catch (err: any) { | |
setError({ | |
message: err.message || 'Failed to process request', | |
details: err.details | |
}); | |
} finally { | |
setLoading(false); | |
} | |
}; | |
return ( | |
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800"> | |
<div className="container mx-auto px-4 py-12 max-w-4xl"> | |
<motion.div | |
initial={{ opacity: 0, y: 20 }} | |
animate={{ opacity: 1, y: 0 }} | |
className="text-center mb-12" | |
> | |
<div className="flex items-center justify-center gap-3 mb-4"> | |
<IconBrandMysql className="w-10 h-10 text-blue-500" /> | |
<IconArrowRight className="w-6 h-6 text-gray-400" /> | |
<IconDatabase className="w-10 h-10 text-blue-500" /> | |
</div> | |
<h1 className="text-4xl font-bold text-gray-800 dark:text-white mb-4"> | |
Text to SQL Converter | |
</h1> | |
<p className="text-gray-600 dark:text-gray-300"> | |
Transform natural language into SQL queries using AI | |
</p> | |
</motion.div> | |
<motion.div | |
initial={{ opacity: 0, y: 20 }} | |
animate={{ opacity: 1, y: 0 }} | |
transition={{ delay: 0.2 }} | |
className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-6 mb-8" | |
> | |
<form onSubmit={handleSubmit} className="space-y-6"> | |
{error && ( | |
<motion.div | |
initial={{ opacity: 0, height: 0 }} | |
animate={{ opacity: 1, height: 'auto' }} | |
exit={{ opacity: 0, height: 0 }} | |
className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4" | |
> | |
<div className="flex items-center gap-2 text-red-800 dark:text-red-200"> | |
<IconAlertCircle className="w-5 h-5" /> | |
<p className="font-medium">{error.message}</p> | |
</div> | |
{error.details && ( | |
<p className="mt-2 text-sm text-red-600 dark:text-red-300"> | |
{typeof error.details === 'string' | |
? error.details | |
: Object.entries(error.details) | |
.filter(([_, value]) => value) | |
.map(([_, value]) => value) | |
.join(', ')} | |
</p> | |
)} | |
</motion.div> | |
)} | |
<div className="space-y-4"> | |
<div className="flex justify-between items-center"> | |
<label className="block text-sm font-medium text-gray-700 dark:text-gray-200"> | |
Database Connection | |
</label> | |
<button | |
type="button" | |
onClick={() => setShowDbConfig(!showDbConfig)} | |
className="flex items-center gap-2 text-sm text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300" | |
> | |
{showDbConfig ? ( | |
<>Hide Connection Details <IconArrowRight className="w-4 h-4 rotate-90" /></> | |
) : ( | |
<>Show Connection Details <IconArrowRight className="w-4 h-4 -rotate-90" /></> | |
)} | |
</button> | |
</div> | |
<AnimatePresence> | |
{showDbConfig && ( | |
<motion.div | |
initial={{ opacity: 0, height: 0 }} | |
animate={{ opacity: 1, height: 'auto' }} | |
exit={{ opacity: 0, height: 0 }} | |
transition={{ duration: 0.2 }} | |
className="overflow-hidden" | |
> | |
<div className="grid grid-cols-2 gap-4"> | |
<div> | |
<input | |
type="text" | |
value={dbConfig.host} | |
onChange={(e) => setDbConfig(prev => ({ ...prev, host: e.target.value }))} | |
className="w-full px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent transition-all" | |
placeholder="Server Host" | |
/> | |
</div> | |
<div> | |
<input | |
type="text" | |
value={dbConfig.port} | |
onChange={(e) => setDbConfig(prev => ({ ...prev, port: e.target.value }))} | |
className="w-full px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent transition-all" | |
placeholder="Port" | |
/> | |
</div> | |
<div> | |
<input | |
type="text" | |
value={dbConfig.database} | |
onChange={(e) => setDbConfig(prev => ({ ...prev, database: e.target.value }))} | |
className="w-full px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent transition-all" | |
placeholder="Database Name" | |
/> | |
</div> | |
<div> | |
<input | |
type="text" | |
value={dbConfig.username} | |
onChange={(e) => setDbConfig(prev => ({ ...prev, username: e.target.value }))} | |
className="w-full px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent transition-all" | |
placeholder="Username" | |
/> | |
</div> | |
<div className="col-span-2"> | |
<input | |
type="password" | |
value={dbConfig.password} | |
onChange={(e) => setDbConfig(prev => ({ ...prev, password: e.target.value }))} | |
className="w-full px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent transition-all" | |
placeholder="Password" | |
/> | |
</div> | |
</div> | |
</motion.div> | |
)} | |
</AnimatePresence> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2"> | |
Select Table | |
</label> | |
<div className="flex gap-4"> | |
<select | |
value={selectedTable} | |
onChange={(e) => setSelectedTable(e.target.value)} | |
className="flex-1 px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent transition-all" | |
disabled={tables.length === 0} | |
> | |
<option value="">Select a table</option> | |
{tables.map((table) => ( | |
<option key={table} value={table}> | |
{table} | |
</option> | |
))} | |
</select> | |
<button | |
type="button" | |
onClick={fetchTables} | |
disabled={!dbUri || loading} | |
className="px-4 py-3 rounded-lg bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-all disabled:opacity-50" | |
> | |
{loading ? ( | |
<IconLoader2 className="w-5 h-5 animate-spin" /> | |
) : ( | |
<IconRefresh className="w-5 h-5" /> | |
)} | |
</button> | |
</div> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2"> | |
Your Query | |
</label> | |
<textarea | |
value={prompt} | |
onChange={(e) => setPrompt(e.target.value)} | |
rows={4} | |
className="w-full px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent transition-all" | |
placeholder="Example: Show me the first 5 users from the users table" | |
/> | |
</div> | |
<button | |
type="submit" | |
disabled={loading} | |
className="w-full bg-blue-500 hover:bg-blue-600 text-white font-medium py-3 px-6 rounded-lg transition-all transform hover:scale-[1.02] active:scale-[0.98] disabled:opacity-70 disabled:cursor-not-allowed flex items-center justify-center gap-2" | |
> | |
{loading ? ( | |
<> | |
<IconLoader2 className="w-5 h-5 animate-spin" /> | |
Converting... | |
</> | |
) : ( | |
'Convert to SQL' | |
)} | |
</button> | |
</form> | |
</motion.div> | |
{results && ( | |
<motion.div | |
initial={{ opacity: 0, y: 20 }} | |
animate={{ opacity: 1, y: 0 }} | |
className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-6" | |
> | |
<h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-4"> | |
Generated SQL & Results | |
</h2> | |
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 mb-4"> | |
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-300 mb-2"> | |
SQL Query | |
</h3> | |
<pre className="text-sm text-gray-800 dark:text-gray-200 whitespace-pre-wrap"> | |
{results.query} | |
</pre> | |
</div> | |
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4"> | |
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-300 mb-2"> | |
Query Results | |
</h3> | |
<pre className="text-sm text-gray-800 dark:text-gray-200 whitespace-pre-wrap"> | |
{JSON.stringify(results.results, null, 2)} | |
</pre> | |
</div> | |
{results?.visualization && ( | |
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 mt-4"> | |
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-300 mb-4"> | |
AI Generated Visualization | |
</h3> | |
<AIVisualization visualization={results.visualization} /> | |
</div> | |
)} | |
</motion.div> | |
)} | |
</div> | |
</div> | |
); | |
} |