import { useState, useRef, useEffect } from 'react'; import { supabase } from '../supabaseClient'; interface TerminalEmulatorProps { onOpenWindow: (windowId: string) => void; onClose: () => void; } // Custom helper to format result rows as a clean ASCII text table function formatAsciiTable(columns: string[], rows: any[]): string[] { if (columns.length === 0 || rows.length === 0) { return ['Empty result set.']; } // Calculate column widths const widths: Record = {}; columns.forEach(col => { widths[col] = col.length; }); rows.forEach(row => { columns.forEach(col => { const valStr = row[col] !== null && row[col] !== undefined ? String(row[col]) : 'NULL'; if (valStr.length > (widths[col] || 0)) { widths[col] = valStr.length; } }); }); // Build grid components const separator = '+' + columns.map(col => '-'.repeat(widths[col] + 2)).join('+') + '+'; const header = '|' + columns.map(col => ` ${col.padEnd(widths[col])} `).join('|') + '|'; const lines: string[] = [separator, header, separator]; rows.forEach(row => { const rowLine = '|' + columns.map(col => { const valStr = row[col] !== null && row[col] !== undefined ? String(row[col]) : 'NULL'; return ` ${valStr.padEnd(widths[col])} `; }).join('|') + '|'; lines.push(rowLine); }); lines.push(separator); return lines; } export default function TerminalEmulator({ onOpenWindow, onClose }: TerminalEmulatorProps) { const [history, setHistory] = useState([ 'AlgoSpaced OS [Version 1.0.24]', '(c) Retro Arcade Systems. All rights reserved.', '', 'Type "help" for a list of available commands.', '' ]); const [input, setInput] = useState(''); const containerRef = useRef(null); useEffect(() => { if (containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight; } }, [history]); // Tokenize inputs while respecting single/double quoted strings const tokenize = (cmd: string): { command: string; args: string[] } => { const trimmed = cmd.trim(); const regex = /[^\s"']+|"([^"]*)"|'([^']*)'/g; const args: string[] = []; let match; while ((match = regex.exec(trimmed)) !== null) { args.push(match[1] || match[2] || match[0]); } return { command: args[0]?.toLowerCase() || '', args: args.slice(1) }; }; const handleCommand = async (cmd: string) => { const trimmed = cmd.trim(); if (trimmed === '') { setHistory(prev => [...prev, '$']); return; } const { command, args } = tokenize(trimmed); let output: string[] = [`$ ${trimmed}`]; switch (command) { case 'help': output.push( 'Available commands:', ' help - Display this documentation', ' streak - Retrieve your active Leitner combo streak metrics', ' sync - Run google drive file import sync', ' reviews - Fetch Leetcode numbers due for daily review', ' python play - Open Daily Python Challenge window', ' db init - Initialize SQL session and open playground window', ' db status - Check active SQL session objectives', ' db query [Q] - Execute query and format results inside CLI', ' open - Open window (usage: open queue | journey | system)', ' clear - Clear terminal buffer', ' exit - Close the terminal shell' ); break; case 'clear': setHistory([]); return; case 'exit': output.push('Closing terminal session...'); setTimeout(() => { onClose(); }, 500); break; case 'open': { const target = args[0]?.toLowerCase(); if (target === 'queue' || target === 'daily' || target === 'reviews') { onOpenWindow('queue'); output.push('Launching Daily Queue window...'); } else if (target === 'journey' || target === 'path' || target === 'map') { onOpenWindow('journey'); output.push('Launching Journey Path window...'); } else if (target === 'system' || target === 'computer' || target === 'sys') { onOpenWindow('computer'); output.push('Launching System Information window...'); } else if (target === 'python' || target === 'challenge') { onOpenWindow('python'); output.push('Launching Daily Python Challenge window...'); } else if (target === 'db' || target === 'mysql' || target === 'sql') { onOpenWindow('mysql'); output.push('Launching MySQL Playground window...'); } else { output.push('Usage: open [queue | journey | system | python | db]'); } break; } case 'streak': output.push('Connecting to streak.sys database...'); try { const { data: { session } } = await supabase.auth.getSession(); if (!session) { output.push('Error: Unauthenticated session'); break; } const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/streak`, { headers: { 'Authorization': `Bearer ${session.access_token}` } }); if (res.ok) { const data = await res.json(); output.push( 'Database Response:', ` Active Streak: ${data.current_streak} days`, ` Max Combo: ${data.longest_streak} days`, ` Last Activity: ${data.last_active_date ? new Date(data.last_active_date).toLocaleDateString() : 'N/A'}` ); } else { output.push('Error: Failed to query database endpoint'); } } catch { output.push('Error: Connection timed out'); } break; case 'sync': output.push('Initializing Google Drive synchronization sync.exe...'); try { const { data: { session } } = await supabase.auth.getSession(); if (!session) { output.push('Error: Unauthenticated session'); break; } const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/sync`, { method: 'POST', headers: { 'Authorization': `Bearer ${session.access_token}` } }); if (res.ok) { const data = await res.json(); output.push(`Sync Result: ${data.message}`); } else { output.push('Error: Sync command failed at backend host'); } } catch { output.push('Error: Cloud host unreachable'); } break; case 'reviews': output.push('Querying leitner_cards partition...'); try { const { data: { session } } = await supabase.auth.getSession(); if (!session) { output.push('Error: Unauthenticated session'); break; } const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/reviews/due`, { headers: { 'Authorization': `Bearer ${session.access_token}` } }); if (res.ok) { const data = await res.json(); if (data.length === 0) { output.push('Queue status: CLEAR! 0 cards due.'); } else { output.push( `Queue status: ${data.length} card(s) due:`, ...data.map((r: any) => ` - LeetCode ${r.problems?.problem_number}: ${r.problems?.name} (Box ${r.box_level})`) ); } } else { output.push('Error: Failed to fetch due items'); } } catch { output.push('Error: Database connection lost'); } break; case 'python': { const subCommand = args[0]?.toLowerCase(); if (subCommand === 'play') { output.push('Launching Daily Python Challenge window...'); onOpenWindow('python'); } else { output.push('Usage: python play'); } break; } case 'db': { const subCommand = args[0]?.toLowerCase(); if (subCommand === 'init') { output.push('Initializing MySQL Playground and launching window...'); onOpenWindow('mysql'); try { const { data: { session } } = await supabase.auth.getSession(); if (!session) { output.push('Error: Unauthenticated session'); break; } 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(); output.push( `Database Session Created: "${data.title}"`, `Objective: ${data.objective}` ); } else { output.push('Error: Failed to initialize DB session on backend host.'); } } catch { output.push('Error: Server connection lost.'); } } else if (subCommand === 'status') { output.push('Fetching SQL playground status...'); onOpenWindow('mysql'); try { const { data: { session } } = await supabase.auth.getSession(); if (!session) { output.push('Error: Unauthenticated session'); break; } 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) { output.push( `Active SQL Challenge: "${data.title}"`, `Objective: ${data.objective}`, `Status: ${data.completed ? 'COMPLETED (Done)' : 'IN PROGRESS'}` ); } else { output.push(data.message); } } else { output.push('Error: Failed to fetch DB status.'); } } catch { output.push('Error: Server timed out.'); } } else if (subCommand === 'query') { // Parse SQL query which resides in the remaining input const sqlIdx = trimmed.toLowerCase().indexOf('query'); const sql = sqlIdx !== -1 ? trimmed.substring(sqlIdx + 5).trim().replace(/^["']|["']$/g, '') : ''; if (!sql) { output.push('Usage: db query [SQL Statement] - Remember to wrap query in double quotes.'); break; } output.push(`Running query: ${sql}`); onOpenWindow('mysql'); try { const { data: { session } } = await supabase.auth.getSession(); if (!session) { output.push('Error: Unauthenticated session'); break; } 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: sql }) }); if (res.ok) { const data = await res.json(); if (data.success) { output.push( `Query OK. Affected ${data.rows_affected} rows.`, ...formatAsciiTable(data.columns, data.rows) ); if (data.is_correct) { output.push('OBJECTIVE ACHIEVED! Congratulations.'); } } else { output.push(`SQL Error: ${data.error}`); } } else { output.push('Error: SQL execution failed.'); } } catch { output.push('Error: Database server unreachable.'); } } else { output.push('Usage: db init | db status | db query "SELECT..."'); } break; } default: output.push(`Command not recognized: "${command}". Type "help" for a list of available actions.`); } setHistory(prev => [...prev, ...output, '']); }; return (
{history.map((line, idx) => (
{line}
))}
{ e.preventDefault(); handleCommand(input); setInput(''); }} className="flex items-center gap-1.5 border-t border-slate-700 pt-2 shrink-0 select-none" > $ setInput(e.target.value)} className="flex-1 bg-transparent text-[#4ADE80] outline-none border-none p-0 focus:ring-0 font-code-mono text-xs md:text-sm" autoFocus placeholder="Type command..." />
); }