sql-agent / src /pages /index.tsx
fullstuckdev's picture
Upload 23 files
645ad9d verified
raw
history blame
14.1 kB
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>
);
}