algospaced-dsa / algospaced-ui /src /components /MySQLPlayground.tsx
iamfebin's picture
Configure AlgoSpaced for Hugging Face Spaces deployment
aaa634c
Raw
History Blame Contribute Delete
14.1 kB
import { useState, useEffect } from 'react';
import { supabase } from '../supabaseClient';
interface MySQLStatusData {
active?: boolean;
title: string;
objective: string;
points: number;
challenge_type: string;
schema: Record<string, Array<{ name: string; type: string }>>;
completed: boolean;
total_score: number;
}
export default function MySQLPlayground() {
const [status, setStatus] = useState<MySQLStatusData | null>(null);
const [query, setQuery] = useState('');
const [running, setRunning] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [results, setResults] = useState<{
columns: string[];
rows: any[];
rowsAffected: number;
isCorrect?: boolean;
} | null>(null);
const fetchStatus = async () => {
setLoading(true);
setError('');
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
setError('Unauthenticated session');
return;
}
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/challenges/mysql/status`, {
headers: { 'Authorization': `Bearer ${session.access_token}` }
});
if (res.ok) {
const data = await res.json();
if (data.active) {
setStatus(data);
} else {
setStatus(null);
}
} else {
setError('Failed to fetch playground status.');
}
} catch {
setError('Connection timed out.');
} finally {
setLoading(false);
}
};
const handleInit = async () => {
setLoading(true);
setError('');
setResults(null);
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
setError('Unauthenticated session');
return;
}
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/challenges/mysql/init`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${session.access_token}` }
});
if (res.ok) {
const data = await res.json();
setStatus({
active: true,
title: data.title,
objective: data.objective,
points: data.points,
schema: data.schema,
challenge_type: data.challenge_type,
completed: false,
total_score: data.total_score
});
} else {
setError('Failed to initialize mock database playground.');
}
} catch {
setError('Server connection error.');
} finally {
setLoading(false);
}
};
const handleRunQuery = async () => {
if (!query.trim()) return;
setRunning(true);
setError('');
setResults(null);
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
setError('Unauthenticated session');
return;
}
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/challenges/mysql/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`
},
body: JSON.stringify({ query })
});
if (res.ok) {
const data = await res.json();
if (data.success) {
setResults({
columns: data.columns,
rows: data.rows,
rowsAffected: data.rows_affected,
isCorrect: data.is_correct
});
if (data.is_correct) {
setStatus(prev => prev ? { ...prev, completed: true, total_score: data.total_score } : null);
}
if (data.schema) {
setStatus(prev => prev ? { ...prev, schema: data.schema } : null);
}
} else {
setError(data.error);
}
} else {
setError('Database server query failure.');
}
} catch {
setError('Query connection lost.');
} finally {
setRunning(false);
}
};
useEffect(() => {
fetchStatus();
}, []);
if (loading) {
return (
<div className="w-full h-full bg-paper-white p-6 flex flex-col items-center justify-center font-arcade text-[10px]">
<div className="animate-bounce mb-2">INITIALIZING SQL PLAYGROUND...</div>
<div className="w-48 h-4 bg-white border-[3px] border-ink-black overflow-hidden relative">
<div className="absolute inset-y-0 left-0 bg-[#0ea5e9] w-1/2 animate-pulse border-r-2 border-ink-black" />
</div>
</div>
);
}
// Dashboard state: no active database initialized
if (!status) {
return (
<div className="w-full h-full bg-paper-white p-6 flex flex-col justify-between items-center text-center text-ink-black text-xs md:text-sm font-sans select-none">
<div className="flex-1 flex flex-col justify-center items-center gap-4">
<div className="w-16 h-16 bg-white border-[3px] border-ink-black flex items-center justify-center shadow-[4px_4px_0_0_#1E293B]">
<span className="material-symbols-outlined text-4xl text-[#0ea5e9]" style={{ fontVariationSettings: "'FILL' 1" }}>
database
</span>
</div>
<h3 className="font-headline-md font-bold text-lg">Interactive MySQL Playground</h3>
<p className="max-w-md text-slate-600 leading-relaxed font-semibold">
Test and run relational queries against isolated schemas dynamically built by the AI generator. Includes full support for:
</p>
<div className="flex flex-wrap justify-center gap-2 font-mono text-[10px] text-slate-700 font-bold uppercase mt-1">
<span className="bg-blue-100 border border-blue-400 px-2 py-0.5">SELECT / JOIN</span>
<span className="bg-emerald-100 border border-emerald-400 px-2 py-0.5">UPDATE / DELETE</span>
<span className="bg-purple-100 border border-purple-400 px-2 py-0.5">WINDOW FUNCTIONS</span>
</div>
</div>
<div className="w-full border-t border-slate-200 pt-4 flex flex-col gap-2 shrink-0">
<button
onClick={handleInit}
className="w-full py-2.5 bg-[#0ea5e9] hover:bg-sky-400 text-white font-window-title font-bold text-xs uppercase border-[3px] border-ink-black shadow-[4px_4px_0px_rgba(30,41,59,1)] active:translate-y-0.5 active:shadow-none transition-all cursor-pointer text-center"
>
CREATE NEW DATABASE INSTANCE
</button>
</div>
</div>
);
}
return (
<div className="w-full h-full bg-paper-white text-ink-black flex flex-col overflow-hidden text-xs md:text-sm">
{/* Play Workspace Scrollable Body */}
<div className="flex-1 overflow-y-auto p-4 custom-scrollbar flex flex-col gap-4">
{/* Objectives Badge Header */}
<div className="flex justify-between items-center bg-slate-100 border-[3px] border-ink-black p-3 shrink-0">
<div className="flex gap-2">
<span className="bg-blue-100 text-blue-800 px-2 py-0.5 border-2 border-ink-black font-bold text-[9px] uppercase">
{status.challenge_type}
</span>
<span className="bg-yellow-100 text-yellow-800 px-2 py-0.5 border-2 border-ink-black font-bold text-[9px]">
+{status.points} PTS
</span>
{status.completed ? (
<span className="bg-emerald-500 text-white px-2 py-0.5 border-2 border-ink-black font-bold text-[9px]">
SOLVED
</span>
) : (
<span className="bg-rose-500 text-white px-2 py-0.5 border-2 border-ink-black font-bold text-[9px]">
UNSOLVED
</span>
)}
</div>
<div className="font-arcade text-[8px] text-sky-700 font-bold bg-white border-2 border-ink-black px-2 py-0.5 shadow-[1px_1px_0_0_#1E293B]">
SCORE: {status.total_score}
</div>
</div>
{/* Objective banner */}
<div className="border-[3px] border-yellow-400 bg-yellow-50 p-3 rounded font-sans leading-relaxed text-xs">
<h4 className="font-arcade text-[9px] text-yellow-800 mb-1">MISSION OBJECTIVE</h4>
<p className="font-bold text-slate-800">{status.objective}</p>
</div>
{/* Schema Tree */}
<div className="border-[3px] border-ink-black bg-white p-3">
<div className="font-bold text-[10px] border-b border-slate-300 pb-1 mb-2 flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm">schema</span>
<span>SCHEMA EXPLORER (MOCK INSTANCE)</span>
</div>
<div className="grid grid-cols-2 gap-3 max-h-[140px] overflow-y-auto custom-scrollbar font-mono text-[10px]">
{Object.entries(status.schema).map(([tableName, cols]) => (
<div key={tableName} className="border border-slate-300 p-2 bg-slate-50">
<div className="font-bold text-blue-700 border-b border-slate-200 pb-0.5 mb-1 flex items-center gap-1">
<span className="material-symbols-outlined text-[12px]">table_chart</span>
{tableName}
</div>
<ul className="space-y-0.5 text-slate-600 text-[9px]">
{cols.map((col, cIdx) => (
<li key={cIdx} className="flex justify-between">
<span>{col.name}</span>
<span className="text-slate-400 italic text-[8px]">{col.type}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
{/* Query Input Editor */}
<div className="flex flex-col min-h-[120px]">
<div className="font-bold text-[10px] mb-1 tracking-wide uppercase flex items-center gap-1">
<span className="material-symbols-outlined text-sm text-sky-600">terminal</span>
<span>Enter SQL Query:</span>
</div>
<textarea
value={query}
onChange={(e) => setQuery(e.target.value)}
className="flex-1 w-full bg-slate-900 text-[#4ADE80] font-mono p-3 border-[3px] border-ink-black rounded outline-none focus:ring-2 focus:ring-sky-500 text-xs leading-relaxed resize-none"
placeholder="SELECT * FROM customers WHERE score > 100;"
spellCheck="false"
/>
</div>
{/* Run Button bar */}
<div className="flex gap-3">
<button
onClick={handleInit}
className="flex-1 bg-white hover:bg-slate-100 text-ink-black border-[3px] border-ink-black font-bold font-window-title text-xs py-2 shadow-[3px_3px_0px_rgba(30,41,59,1)] active:translate-y-0.5 active:shadow-none cursor-pointer"
>
NEW PROBLEM
</button>
<button
disabled={running}
onClick={handleRunQuery}
className="flex-[2] bg-highlight-pink text-white font-window-title font-bold py-2 border-[3px] border-ink-black shadow-[3px_3px_0px_rgba(30,41,59,1)] active:translate-y-0.5 active:shadow-none hover:bg-pink-400 transition-all disabled:opacity-50 cursor-pointer text-center text-xs"
>
{running ? 'EXECUTING SQL...' : 'EXECUTE SQL QUERY'}
</button>
</div>
{/* Query Error Console */}
{error && (
<div className="border-[3px] border-red-500 bg-red-50 text-red-800 p-3 font-mono text-xs whitespace-pre-wrap">
<div className="font-bold mb-1">SQL RUNTIME / SYNTAX ERROR:</div>
{error}
</div>
)}
{/* Tabular Result Grid */}
{results && (
<div className="border-[3px] border-ink-black bg-white p-3 flex flex-col">
<div className="font-bold text-[10px] border-b border-slate-300 pb-1 mb-2 flex justify-between items-center">
<span>QUERY OUTPUT BUFFER</span>
{results.isCorrect && (
<span className="text-green-600 font-bold animate-bounce text-[9px]">
✓ MISSION ACHIEVED!
</span>
)}
</div>
<div className="text-[9px] text-slate-500 mb-2 font-mono">
Rows Affected: {results.rowsAffected} | Total Returned: {results.rows.length}
</div>
{results.rows.length > 0 ? (
<div className="overflow-x-auto border border-slate-300 max-h-[180px] custom-scrollbar">
<table className="border-collapse w-full font-mono text-[9px] min-w-full">
<thead>
<tr className="bg-slate-100 border-b border-slate-300">
{results.columns.map((col, idx) => (
<th key={idx} className="border-r border-slate-300 p-1.5 text-left font-bold text-slate-700">
{col}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
{results.rows.map((row, rIdx) => (
<tr key={rIdx} className="hover:bg-slate-50">
{results.columns.map((col, cIdx) => (
<td key={cIdx} className="border-r border-slate-200 p-1.5 text-slate-800 whitespace-nowrap">
{row[col] !== null ? String(row[col]) : <span className="text-slate-300 italic">NULL</span>}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="p-3 border border-dashed border-slate-300 text-center text-slate-400 italic font-sans text-xs">
Query executed successfully. Result set is empty (or modification completed).
</div>
)}
</div>
)}
</div>
</div>
);
}