algospaced-dsa / algospaced-ui /src /components /TerminalEmulator.tsx
iamfebin's picture
Configure AlgoSpaced for Hugging Face Spaces deployment
aaa634c
Raw
History Blame Contribute Delete
13.7 kB
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>
);
}