import React, { useState, useEffect, useRef } from 'react'; import { Editor } from '@monaco-editor/react'; import { registerMonacoCompletions, getMonacoLanguage, getLanguageFileName } from '../utils/monacoCompletions'; import { getChallenges, runCode, lintCode, getGuidance, getHandbookCatalog, getHandbookExample, Challenge, HandbookTopic, HandbookCatalog, } from '../api/client'; import { MarkdownView } from '../components/MarkdownView'; import { MessageSquare, ChevronLeft, Play, CheckCircle2, Sparkles, Code, Send, HelpCircle, BookMarked, X, Search, Zap, BookOpen, Maximize2, Minimize2, Terminal, GripVertical, GripHorizontal, } from 'lucide-react'; interface WorkspaceProps { challengeId: string; onBack: () => void; } const Workspace: React.FC = ({ challengeId, onBack }) => { const [allChallenges, setAllChallenges] = useState([]); const [currentChallenge, setCurrentChallenge] = useState(null); const [code, setCode] = useState(''); const [stdinText, setStdinText] = useState(''); // Resizable Layout Dimensions const [leftWidth, setLeftWidth] = useState(320); // Left Challenge Panel width in px const [rightWidth, setRightWidth] = useState(350); // Right Sidebar width in px const [outputHeight, setOutputHeight] = useState(180); // Output Console height in px const editorRef = useRef(null); const [draggingType, setDraggingType] = useState<'left' | 'right' | 'output' | null>(null); const [output, setOutput] = useState<{ status: 'idle' | 'success' | 'error'; text: string }>({ status: 'idle', text: 'Execution and lint output will appear here...', }); // Right sidebar state: 'mentor' | 'handbook' | 'closed' const [rightSidebarMode, setRightSidebarMode] = useState<'mentor' | 'handbook' | 'closed'>('mentor'); // Mentor state const [guidance, setGuidance] = useState('Ask the mentor a question or click Check Answer below...'); const [userQuestion, setUserQuestion] = useState(''); const [isLoadingGuidance, setIsLoadingGuidance] = useState(false); // Handbook state const [handbookCatalog, setHandbookCatalog] = useState({ functions: [], subjects: [] }); const [handbookTab, setHandbookTab] = useState<'functions' | 'subjects'>('functions'); const [searchQuery, setSearchQuery] = useState(''); const [selectedTopic, setSelectedTopic] = useState(null); const [handbookExample, setHandbookExample] = useState( 'Select a built-in function or topic above to view reference card...' ); const [isLoadingExample, setIsLoadingExample] = useState(false); const [isRunning, setIsRunning] = useState(false); const [isLinting, setIsLinting] = useState(false); // Load all challenges useEffect(() => { getChallenges() .then((challenges) => { setAllChallenges(challenges); const match = challenges.find((c) => (c.id || c.challenge_id) === challengeId); if (match) { setCurrentChallenge(match); } else if (challenges.length > 0) { setCurrentChallenge(challenges[0]); } }) .catch((err) => console.error('Failed to load challenges:', err)); }, [challengeId]); // Fetch handbook catalog when current challenge language changes useEffect(() => { if (currentChallenge) { getHandbookCatalog(currentChallenge.language) .then((catalog) => { setHandbookCatalog(catalog); setSelectedTopic(null); setHandbookExample(''); }) .catch((err) => console.error('Failed to load handbook catalog:', err)); } }, [currentChallenge]); // Global mousemove and mouseup listeners for drag-to-resize splitters useEffect(() => { if (!draggingType) return; const handleMouseMove = (e: MouseEvent) => { if (draggingType === 'left') { const newWidth = Math.max(150, Math.min(Math.floor(window.innerWidth * 0.6), e.clientX)); setLeftWidth(newWidth); } else if (draggingType === 'right') { const newWidth = Math.max(180, Math.min(Math.floor(window.innerWidth * 0.6), window.innerWidth - e.clientX)); setRightWidth(newWidth); } else if (draggingType === 'output') { const maxHeight = Math.max(150, window.innerHeight - 150); const newHeight = Math.max(60, Math.min(maxHeight, window.innerHeight - e.clientY)); setOutputHeight(newHeight); if (editorRef.current) { try { editorRef.current.layout(); } catch (_) {} } } }; const handleMouseUp = () => { setDraggingType(null); document.body.style.cursor = 'default'; document.body.style.userSelect = 'auto'; }; window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; }, [draggingType]); useEffect(() => { if (editorRef.current) { try { editorRef.current.layout(); } catch (_) {} } }, [outputHeight, leftWidth, rightWidth]); const startDraggingLeft = (e: React.MouseEvent) => { e.preventDefault(); setDraggingType('left'); document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }; const startDraggingRight = (e: React.MouseEvent) => { e.preventDefault(); setDraggingType('right'); document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }; const startDraggingOutput = (e: React.MouseEvent) => { e.preventDefault(); setDraggingType('output'); document.body.style.cursor = 'row-resize'; document.body.style.userSelect = 'none'; }; const handleSelectChallenge = (cid: string) => { const match = allChallenges.find((c) => (c.id || c.challenge_id) === cid); if (match) { setCurrentChallenge(match); } }; const handleRunCode = async (overrideStdin?: string) => { if (!code.trim() || !currentChallenge) return; setIsRunning(true); setOutput({ status: 'idle', text: 'Executing code...' }); const inputToSend = overrideStdin !== undefined ? overrideStdin : stdinText; try { const res = await runCode(getMonacoLanguage(currentChallenge.language), code, inputToSend); if (res.exit_code === 0) { setOutput({ status: 'success', text: res.stdout?.trim() ? `Output:\n${res.stdout}` : '✓ Code executed successfully (exit code 0, no stdout).', }); } else { const err = res.stderr?.trim() || res.stdout?.trim() || 'Unknown runtime error'; setOutput({ status: 'error', text: `✗ Runtime Error (exit code ${res.exit_code}):\n${err}` }); } } catch (err: any) { setOutput({ status: 'error', text: `✗ Error running code: ${err.message}` }); } finally { setIsRunning(false); } }; const handleSendStdin = () => { handleRunCode(stdinText); }; const handleLintCode = async () => { if (!code.trim() || !currentChallenge) return; setIsLinting(true); setOutput({ status: 'idle', text: 'Running syntax & style lint...' }); try { const res = await lintCode(getMonacoLanguage(currentChallenge.language), code); if (res.exit_code === 0) { setOutput({ status: 'success', text: '✓ Syntax & Style clean! No linting errors detected.' }); } else { const raw = (res.stdout || '') + '\n' + (res.stderr || ''); setOutput({ status: 'error', text: `✗ Lint Error(s):\n${raw.trim()}` }); } } catch (err: any) { setOutput({ status: 'error', text: `✗ Error linting code: ${err.message}` }); } finally { setIsLinting(false); } }; const handleAskMentor = async (questionToAsk?: string) => { if (!currentChallenge) return; const q = questionToAsk !== undefined ? questionToAsk : userQuestion.trim(); const cid = currentChallenge.id || currentChallenge.challenge_id; setIsLoadingGuidance(true); setRightSidebarMode('mentor'); setGuidance(''); setUserQuestion(''); try { const res = await getGuidance( cid, getMonacoLanguage(currentChallenge.language), code, q ); const qHeader = q ? `### Question / Evaluation:\n> ${q}\n\n---\n\n` : ''; setGuidance(`${qHeader}${res.mentor_response || 'No guidance received.'}`); } catch (err: any) { setGuidance(`⚠️ Error consulting Mentor: ${err.message}`); } finally { setIsLoadingGuidance(false); } }; const handleSelectHandbookTopic = async (topic: HandbookTopic) => { if (!currentChallenge) return; setSelectedTopic(topic); setIsLoadingExample(true); setHandbookExample(`⏳ **Generating reference card for '${topic.title}' in ${currentChallenge.language}...**\n\n*Consulting LLM backend...*`); try { const res = await getHandbookExample(currentChallenge.language, topic.id, topic.title); if (res.status === 'success') { setHandbookExample(res.example_markdown); } else { setHandbookExample(`⚠️ ${res.message}`); } } catch (err: any) { setHandbookExample(`⚠️ Error generating reference card: ${err.message}`); } finally { setIsLoadingExample(false); } }; const handleCheckAnswer = () => { const checkPrompt = 'Please evaluate my code implementation against all the requirements of this challenge. Check if my solution is complete and correct, point out any missing requirements or edge cases, and give me feedback on my answer.'; handleAskMentor(checkPrompt); }; const handleKeyDownQuestion = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleAskMentor(); } }; const handleKeyDownStdin = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); handleSendStdin(); } }; const activeCId = currentChallenge?.id || currentChallenge?.challenge_id || ''; // Filter handbook catalog based on active tab and search query const rawList = handbookTab === 'functions' ? handbookCatalog.functions : handbookCatalog.subjects; const filteredList = rawList.filter((item) => searchQuery.trim() === '' ? true : item.title.toLowerCase().includes(searchQuery.toLowerCase()) || item.desc.toLowerCase().includes(searchQuery.toLowerCase()) ); return (
{/* Header Bar */}
TactiTerm
{/* Challenge Selector */}
{/* Action Controls */}
{/* Dual Sidebar Toggles */}
{/* Main Container */}
{/* Full-Screen Drag Overlay to bypass Monaco Editor event capture */} {draggingType && (
{ if (draggingType === 'left') { const newWidth = Math.max(150, Math.min(Math.floor(window.innerWidth * 0.6), e.clientX)); setLeftWidth(newWidth); } else if (draggingType === 'right') { const newWidth = Math.max(180, Math.min(Math.floor(window.innerWidth * 0.6), window.innerWidth - e.clientX)); setRightWidth(newWidth); } else if (draggingType === 'output') { const maxHeight = Math.max(150, window.innerHeight - 150); const newHeight = Math.max(60, Math.min(maxHeight, window.innerHeight - e.clientY)); setOutputHeight(newHeight); if (editorRef.current) { try { editorRef.current.layout(); } catch (_) {} } } }} onMouseUp={() => { setDraggingType(null); document.body.style.cursor = 'default'; document.body.style.userSelect = 'auto'; }} className={`fixed inset-0 z-[99999] select-none ${ draggingType === 'output' ? 'cursor-row-resize' : 'cursor-col-resize' }`} /> )} {/* Left Column: Challenge Specifications (Scalable Width) */}

Challenge Details

{currentChallenge && ( {currentChallenge.language} )}
{currentChallenge ? ( <>

{currentChallenge.name}

{currentChallenge.difficulty}

Description

{currentChallenge.description}

{currentChallenge.requirements && currentChallenge.requirements.length > 0 && (

Requirements

    {currentChallenge.requirements.map((req, idx) => (
  • {req}
  • ))}
)} {currentChallenge.hints && currentChallenge.hints.length > 0 && (

Hints

    {currentChallenge.hints.map((hint, idx) => (
  • {hint}
  • ))}
)} ) : (

Loading details...

)}
{/* Resizable Splitter Handle: Left Column */}
{/* Middle Column: Monaco Code Editor & Resizable Output Console */}
{getLanguageFileName(currentChallenge?.language)} TactiTerm IDE
{/* Code Editor */}
{ editorRef.current = editor; registerMonacoCompletions(monaco); }} onChange={(val) => setCode(val || '')} options={{ minimap: { enabled: false }, fontSize: 13, automaticLayout: true, scrollBeyondLastLine: false, padding: { top: 12 }, quickSuggestions: true, suggestOnTriggerCharacters: true, }} />
{/* Resizable Splitter Handle: Output Console */}
{/* Scalable Output & Stdin Input Terminal Area */}
Output Console {output.status === 'success' && (clean)} {output.status === 'error' && (error)}
              {output.text}
            
{/* Interactive Stdin Input Bar */}
setStdinText(e.target.value)} onKeyDown={handleKeyDownStdin} placeholder="Type program input (stdin) and hit Enter to run with input..." className="flex-1 bg-darkSurface border border-darkBorder rounded-lg px-3 py-1.5 text-xs text-gray-200 placeholder-gray-500 focus:outline-none focus:border-blue-500" />
{/* Resizable Splitter Handle: Right Column */} {rightSidebarMode !== 'closed' && (
)} {/* Right Column Mode 1: Mentor Sidebar (Scalable Width) */} {rightSidebarMode === 'mentor' && (

Mentor

{/* Mentor Guidance View */}
{isLoadingGuidance ? (
) : (
)}
{/* Question Input Area */}