Spaces:
Sleeping
Sleeping
| 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<string, number> = {}; | |
| 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<string[]>([ | |
| '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<HTMLDivElement>(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 ( | |
| <div className="w-full h-full bg-[#1E293B] text-[#4ADE80] font-code-mono p-4 flex flex-col overflow-hidden text-xs md:text-sm select-text"> | |
| <div ref={containerRef} className="flex-1 overflow-y-auto mb-2 custom-scrollbar space-y-1"> | |
| {history.map((line, idx) => ( | |
| <div key={idx} className="whitespace-pre-wrap leading-relaxed min-h-[1.2em]"> | |
| {line} | |
| </div> | |
| ))} | |
| </div> | |
| <form | |
| onSubmit={(e) => { | |
| e.preventDefault(); | |
| handleCommand(input); | |
| setInput(''); | |
| }} | |
| className="flex items-center gap-1.5 border-t border-slate-700 pt-2 shrink-0 select-none" | |
| > | |
| <span className="font-bold text-[#ffcc00]">$</span> | |
| <input | |
| type="text" | |
| value={input} | |
| onChange={(e) => 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..." | |
| /> | |
| </form> | |
| </div> | |
| ); | |
| } | |