Files
TactiTerm/frontend/src/pages/Workspace.tsx
T

816 lines
34 KiB
TypeScript

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<WorkspaceProps> = ({ challengeId, onBack }) => {
const [allChallenges, setAllChallenges] = useState<Challenge[]>([]);
const [currentChallenge, setCurrentChallenge] = useState<Challenge | null>(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<any>(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<string>('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<HandbookCatalog>({ functions: [], subjects: [] });
const [handbookTab, setHandbookTab] = useState<'functions' | 'subjects'>('functions');
const [searchQuery, setSearchQuery] = useState('');
const [selectedTopic, setSelectedTopic] = useState<HandbookTopic | null>(null);
const [handbookExample, setHandbookExample] = useState<string>(
'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<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleAskMentor();
}
};
const handleKeyDownStdin = (e: React.KeyboardEvent<HTMLInputElement>) => {
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 (
<div className="flex flex-col h-screen bg-darkBg text-gray-100 font-sans overflow-hidden">
{/* Header Bar */}
<header className="h-14 border-b border-darkBorder bg-darkSurface px-4 flex justify-between items-center select-none shadow-md z-10 shrink-0">
<div className="flex items-center gap-4">
<button
onClick={onBack}
className="p-1.5 hover:bg-gray-800 text-gray-400 hover:text-white rounded-lg transition-colors flex items-center gap-1 text-xs"
>
<ChevronLeft size={18} />
<span>Dashboard</span>
</button>
<div className="h-4 w-px bg-darkBorder" />
<div className="flex items-center gap-2">
<Code className="text-blue-500" size={20} />
<span className="font-bold text-sm text-white">TactiTerm</span>
</div>
<div className="h-4 w-px bg-darkBorder" />
{/* Challenge Selector */}
<select
value={activeCId}
onChange={(e) => handleSelectChallenge(e.target.value)}
className="bg-darkBg border border-darkBorder text-gray-200 text-xs rounded-lg px-3 py-1.5 focus:outline-none focus:border-blue-500 font-medium"
>
{allChallenges.map((c) => {
const cid = c.id || c.challenge_id;
return (
<option key={cid} value={cid}>
{c.name} ({c.difficulty})
</option>
);
})}
</select>
</div>
{/* Action Controls */}
<div className="flex items-center gap-2">
<button
onClick={() => handleRunCode()}
disabled={isRunning || !code.trim()}
className="px-3.5 py-1.5 bg-green-600 hover:bg-green-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm"
>
{isRunning ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<Play size={14} fill="currentColor" />
)}
<span>Run</span>
</button>
<button
onClick={handleLintCode}
disabled={isLinting || !code.trim()}
className="px-3.5 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm"
>
{isLinting ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<CheckCircle2 size={14} />
)}
<span>Lint</span>
</button>
<button
onClick={handleCheckAnswer}
disabled={isLoadingGuidance || !code.trim()}
className="px-3.5 py-1.5 bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm"
>
<Sparkles size={14} />
<span>Check Answer</span>
</button>
{/* Dual Sidebar Toggles */}
<button
onClick={() => setRightSidebarMode(rightSidebarMode === 'mentor' ? 'closed' : 'mentor')}
className={`px-3.5 py-1.5 font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm border ${
rightSidebarMode === 'mentor'
? 'bg-amber-500/20 text-amber-300 border-amber-500/40'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700 border-gray-700'
}`}
>
<MessageSquare size={14} />
<span>Mentor</span>
</button>
<button
onClick={() => setRightSidebarMode(rightSidebarMode === 'handbook' ? 'closed' : 'handbook')}
className={`px-3.5 py-1.5 font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm border ${
rightSidebarMode === 'handbook'
? 'bg-indigo-500/20 text-indigo-300 border-indigo-500/40'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700 border-gray-700'
}`}
>
<BookMarked size={14} />
<span>Syntax Handbook</span>
</button>
</div>
</header>
{/* Main Container */}
<div className="flex flex-1 overflow-hidden relative">
{/* Full-Screen Drag Overlay to bypass Monaco Editor event capture */}
{draggingType && (
<div
onMouseMove={(e) => {
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) */}
<div
style={{ width: `${leftWidth}px` }}
className="border-r border-transparent bg-darkBg flex flex-col overflow-hidden shrink-0"
>
<div className="p-3 bg-darkSurface border-b border-darkBorder flex justify-between items-center">
<h2 className="text-xs font-bold uppercase tracking-wider text-gray-400">Challenge Details</h2>
{currentChallenge && (
<span className="px-2 py-0.5 bg-blue-500/10 border border-blue-500/20 text-blue-400 text-[10px] uppercase font-bold rounded">
{currentChallenge.language}
</span>
)}
</div>
<div className="flex-1 p-5 overflow-y-auto space-y-6">
{currentChallenge ? (
<>
<div>
<h1 className="text-xl font-bold text-white mb-2">{currentChallenge.name}</h1>
<span
className={`inline-block px-2.5 py-0.5 text-[10px] uppercase font-bold rounded border ${
currentChallenge.difficulty.toLowerCase() === 'easy'
? 'bg-green-500/10 text-green-400 border-green-500/20'
: currentChallenge.difficulty.toLowerCase() === 'medium'
? 'bg-amber-500/10 text-amber-400 border-amber-500/20'
: 'bg-red-500/10 text-red-400 border-red-500/20'
}`}
>
{currentChallenge.difficulty}
</span>
</div>
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-blue-400 mb-2">Description</h3>
<p className="text-xs text-gray-300 leading-relaxed bg-darkSurface p-3 rounded-xl border border-darkBorder/50">
{currentChallenge.description}
</p>
</div>
{currentChallenge.requirements && currentChallenge.requirements.length > 0 && (
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-blue-400 mb-2">Requirements</h3>
<ul className="space-y-1.5">
{currentChallenge.requirements.map((req, idx) => (
<li key={idx} className="text-xs text-gray-300 flex items-start gap-2">
<span className="text-blue-500 font-bold"></span>
<span>{req}</span>
</li>
))}
</ul>
</div>
)}
{currentChallenge.hints && currentChallenge.hints.length > 0 && (
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-amber-400 mb-2">Hints</h3>
<ul className="space-y-1.5">
{currentChallenge.hints.map((hint, idx) => (
<li key={idx} className="text-xs text-gray-400 flex items-start gap-2 bg-amber-500/5 p-2 rounded-lg border border-amber-500/10">
<HelpCircle size={14} className="text-amber-400 shrink-0 mt-0.5" />
<span>{hint}</span>
</li>
))}
</ul>
</div>
)}
</>
) : (
<p className="text-xs text-gray-400">Loading details...</p>
)}
</div>
</div>
{/* Resizable Splitter Handle: Left Column */}
<div
onMouseDown={startDraggingLeft}
className="w-2.5 -mx-1 z-10 bg-transparent hover:bg-blue-500/50 active:bg-blue-600 cursor-col-resize flex items-center justify-center shrink-0 group transition-colors select-none"
title="Drag to resize Challenge Panel"
>
<GripVertical size={12} className="text-gray-600/40 group-hover:text-white" />
</div>
{/* Middle Column: Monaco Code Editor & Resizable Output Console */}
<div className="flex-1 flex flex-col bg-[#1e1e1e] overflow-hidden min-w-0 min-h-0">
<div className="bg-darkBg text-gray-400 px-4 py-2 text-xs flex justify-between items-center border-b border-darkBorder shrink-0">
<span className="font-mono text-blue-400">{getLanguageFileName(currentChallenge?.language)}</span>
<span className="text-[11px] text-gray-500">TactiTerm IDE</span>
</div>
{/* Code Editor */}
<div className="flex-1 min-h-0 relative">
<Editor
height="100%"
defaultLanguage={getMonacoLanguage(currentChallenge?.language)}
language={getMonacoLanguage(currentChallenge?.language)}
defaultValue=""
value={code}
theme="vs-dark"
onMount={(editor, monaco) => {
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,
}}
/>
</div>
{/* Resizable Splitter Handle: Output Console */}
<div
onMouseDown={startDraggingOutput}
className="h-2.5 -my-1 z-10 bg-transparent hover:bg-blue-500/50 active:bg-blue-600 cursor-row-resize flex items-center justify-center shrink-0 group transition-colors select-none"
title="Drag up/down to resize Output Console"
>
<GripHorizontal size={12} className="text-gray-600/40 group-hover:text-white" />
</div>
{/* Scalable Output & Stdin Input Terminal Area */}
<div
style={{ height: `${outputHeight}px` }}
className="border-t border-transparent bg-black/90 p-4 font-mono text-xs flex flex-col overflow-hidden shrink-0 min-h-0"
>
<div className="flex justify-between items-center text-gray-400 pb-2 mb-2 border-b border-gray-800 text-[11px] uppercase font-bold shrink-0">
<div className="flex items-center gap-2">
<Terminal size={14} className="text-blue-400" />
<span>Output Console</span>
{output.status === 'success' && <span className="text-green-400 lowercase font-normal">(clean)</span>}
{output.status === 'error' && <span className="text-red-400 lowercase font-normal">(error)</span>}
</div>
</div>
<pre className={`flex-1 overflow-y-auto whitespace-pre-wrap leading-relaxed ${
output.status === 'error' ? 'text-red-400' : 'text-emerald-400'
}`}>
{output.text}
</pre>
{/* Interactive Stdin Input Bar */}
<div className="mt-3 pt-3 border-t border-gray-800 flex gap-2 font-sans shrink-0">
<input
type="text"
value={stdinText}
onChange={(e) => 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"
/>
<button
onClick={handleSendStdin}
disabled={isRunning}
className="px-3.5 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center gap-1.5 shadow-sm"
>
<Send size={13} />
<span>Send Input</span>
</button>
</div>
</div>
</div>
{/* Resizable Splitter Handle: Right Column */}
{rightSidebarMode !== 'closed' && (
<div
onMouseDown={startDraggingRight}
className="w-2.5 -mx-1 z-10 bg-transparent hover:bg-blue-500/50 active:bg-blue-600 cursor-col-resize flex items-center justify-center shrink-0 group transition-colors select-none"
title="Drag to resize Right Sidebar"
>
<GripVertical size={12} className="text-gray-600/40 group-hover:text-white" />
</div>
)}
{/* Right Column Mode 1: Mentor Sidebar (Scalable Width) */}
{rightSidebarMode === 'mentor' && (
<div
style={{ width: `${rightWidth}px` }}
className="border-l border-transparent bg-darkSurface flex flex-col overflow-hidden shrink-0"
>
<div className="p-3 border-b border-darkBorder bg-darkBg flex justify-between items-center">
<h2 className="flex items-center gap-2 font-bold text-amber-400 text-xs uppercase tracking-wider">
<MessageSquare size={16} /> Mentor
</h2>
<button
onClick={() => setRightSidebarMode('closed')}
className="text-gray-400 hover:text-white text-xs px-2 py-0.5 rounded hover:bg-gray-800"
>
<X size={16} />
</button>
</div>
{/* Mentor Guidance View */}
<div className="flex-1 p-4 overflow-y-auto space-y-4">
{isLoadingGuidance ? (
<div className="flex flex-col items-center justify-center py-16 gap-4">
<div className="relative flex items-center justify-center">
<div className="w-10 h-10 border-4 border-amber-500/20 border-t-amber-400 rounded-full animate-spin" />
<Sparkles size={16} className="absolute text-amber-400 animate-pulse" />
</div>
</div>
) : (
<div className="p-4 bg-darkBg border border-darkBorder rounded-xl shadow-inner">
<MarkdownView content={guidance} />
</div>
)}
</div>
{/* Question Input Area */}
<div className="p-4 border-t border-darkBorder bg-darkBg flex flex-col gap-2">
<label className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">
Ask Question (Enter to Send):
</label>
<textarea
value={userQuestion}
onChange={(e) => setUserQuestion(e.target.value)}
onKeyDown={handleKeyDownQuestion}
placeholder="e.g. How should I structure my loop? Why is my variable returning None?"
className="w-full h-20 bg-darkSurface border border-darkBorder rounded-lg p-2.5 text-xs text-gray-200 placeholder-gray-500 focus:outline-none focus:border-amber-500 resize-none font-sans"
/>
<div className="flex gap-2">
<button
onClick={() => handleAskMentor()}
disabled={isLoadingGuidance}
className="flex-1 py-2 bg-amber-600 hover:bg-amber-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center justify-center gap-1.5 shadow-sm"
>
{isLoadingGuidance ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<Send size={14} />
)}
<span>Ask Mentor</span>
</button>
<button
onClick={handleCheckAnswer}
disabled={isLoadingGuidance || !code.trim()}
className="py-2 px-3 bg-emerald-700 hover:bg-emerald-600 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center justify-center gap-1"
title="Check Answer"
>
{isLoadingGuidance ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<Sparkles size={14} />
)}
<span>Check</span>
</button>
</div>
</div>
</div>
)}
{/* Right Column Mode 2: Handbook Sidebar (Scalable Width) */}
{rightSidebarMode === 'handbook' && (
<div
style={{ width: `${rightWidth}px` }}
className="border-l border-transparent bg-darkSurface flex flex-col overflow-hidden shrink-0"
>
<div className="p-3 border-b border-darkBorder bg-darkBg flex justify-between items-center">
<h2 className="flex items-center gap-2 font-bold text-indigo-400 text-xs uppercase tracking-wider">
<BookMarked size={16} /> Syntax Reference ({currentChallenge?.language || 'Python'})
</h2>
<button
onClick={() => setRightSidebarMode('closed')}
className="text-gray-400 hover:text-white text-xs px-2 py-0.5 rounded hover:bg-gray-800"
>
<X size={16} />
</button>
</div>
{/* Category Tabs & Search Bar */}
<div className="p-3 border-b border-darkBorder bg-darkBg flex flex-col gap-2.5">
<div className="flex bg-darkSurface p-1 rounded-xl border border-darkBorder">
<button
onClick={() => setHandbookTab('functions')}
className={`flex-1 py-1.5 text-[11px] font-bold rounded-lg transition-all flex items-center justify-center gap-1.5 ${
handbookTab === 'functions'
? 'bg-indigo-600 text-white shadow-sm'
: 'text-gray-400 hover:text-white'
}`}
>
<Zap size={13} />
<span>Built-ins</span>
</button>
<button
onClick={() => setHandbookTab('subjects')}
className={`flex-1 py-1.5 text-[11px] font-bold rounded-lg transition-all flex items-center justify-center gap-1.5 ${
handbookTab === 'subjects'
? 'bg-indigo-600 text-white shadow-sm'
: 'text-gray-400 hover:text-white'
}`}
>
<BookOpen size={13} />
<span>Subjects</span>
</button>
</div>
<div className="relative">
<Search size={14} className="absolute left-2.5 top-2.5 text-gray-500" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Filter functions or topics..."
className="w-full bg-darkSurface border border-darkBorder rounded-lg pl-8 pr-3 py-1.5 text-xs text-gray-200 focus:outline-none focus:border-indigo-500 font-sans"
/>
</div>
</div>
{/* Topic Catalog List (Fills full height when no topic selected) */}
<div className={`p-3 bg-darkBg flex flex-col gap-1.5 overflow-y-auto ${
selectedTopic ? 'max-h-52 border-b border-darkBorder' : 'flex-1'
}`}>
{selectedTopic && (
<div className="flex justify-between items-center pb-1 text-[11px] text-gray-400 font-sans border-b border-darkBorder/40 mb-1">
<span>Selected: <strong className="text-indigo-400">{selectedTopic.title}</strong></span>
<button
onClick={() => {
setSelectedTopic(null);
setHandbookExample('');
}}
className="text-indigo-400 hover:text-indigo-300 underline text-[10px] font-semibold"
>
Clear selection (Show all)
</button>
</div>
)}
{filteredList.length === 0 ? (
<p className="text-xs text-gray-500 italic p-2">No matching functions or topics found.</p>
) : (
filteredList.map((item) => (
<button
key={item.id}
onClick={() => handleSelectHandbookTopic(item)}
className={`text-left p-2.5 rounded-lg text-xs transition-all border ${
selectedTopic?.id === item.id
? 'bg-indigo-600/20 border-indigo-500/50 text-indigo-300 font-bold'
: 'bg-darkSurface border-darkBorder/50 text-gray-300 hover:bg-gray-800'
}`}
>
<div className="font-semibold flex items-center gap-1.5">
{handbookTab === 'functions' ? <Zap size={12} className="text-amber-400" /> : <BookOpen size={12} className="text-blue-400" />}
<span>{item.title}</span>
</div>
<div className="text-[10px] text-gray-400 font-normal line-clamp-1 mt-0.5">{item.desc}</div>
</button>
))
)}
</div>
{/* Generated LLM Reference Card View (ONLY renders after selecting a topic) */}
{selectedTopic && (
<div className="flex-1 p-4 overflow-y-auto bg-darkSurface border-t border-darkBorder">
<div className="p-4 bg-darkBg border border-darkBorder rounded-xl shadow-inner">
{isLoadingExample ? (
<div className="flex flex-col items-center justify-center py-16 gap-4">
<div className="relative flex items-center justify-center">
<div className="w-10 h-10 border-4 border-indigo-500/20 border-t-indigo-400 rounded-full animate-spin" />
<BookMarked size={16} className="absolute text-indigo-400 animate-pulse" />
</div>
</div>
) : (
<MarkdownView content={handbookExample} />
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
);
};
export default Workspace;