Version 1.0

This commit is contained in:
Alexander R.
2026-07-21 22:24:25 +00:00
parent f3645fbfbc
commit 9edbfd3f77
4134 changed files with 1448752 additions and 1 deletions
+732
View File
@@ -0,0 +1,732 @@
import React, { useState, useEffect, useRef } from 'react';
import { Editor } from '@monaco-editor/react';
import { registerMonacoCompletions } 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 isDraggingLeft = useRef(false);
const isDraggingRight = useRef(false);
const isDraggingOutput = useRef(false);
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 W3Schools 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 W3Schools 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);
if (catalog.functions.length > 0) {
setSelectedTopic(catalog.functions[0]);
} else if (catalog.subjects.length > 0) {
setSelectedTopic(catalog.subjects[0]);
}
})
.catch((err) => console.error('Failed to load handbook catalog:', err));
}
}, [currentChallenge]);
// Global mousemove and mouseup listeners for drag-to-resize splitters
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (isDraggingLeft.current) {
const newWidth = Math.max(180, Math.min(600, e.clientX));
setLeftWidth(newWidth);
} else if (isDraggingRight.current) {
const newWidth = Math.max(220, Math.min(700, window.innerWidth - e.clientX));
setRightWidth(newWidth);
} else if (isDraggingOutput.current) {
const newHeight = Math.max(80, Math.min(650, window.innerHeight - e.clientY));
setOutputHeight(newHeight);
}
};
const handleMouseUp = () => {
isDraggingLeft.current = false;
isDraggingRight.current = false;
isDraggingOutput.current = false;
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);
};
}, []);
const startDraggingLeft = (e: React.MouseEvent) => {
e.preventDefault();
isDraggingLeft.current = true;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
};
const startDraggingRight = (e: React.MouseEvent) => {
e.preventDefault();
isDraggingRight.current = true;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
};
const startDraggingOutput = (e: React.MouseEvent) => {
e.preventDefault();
isDraggingOutput.current = true;
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(currentChallenge.language.toLowerCase() || 'python', 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(currentChallenge.language.toLowerCase() || 'python', 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('⏳ **Mentor is thinking and generating guidance...**\n\n*Analyzing your code, task requirements, and question...*');
setUserQuestion('');
try {
const res = await getGuidance(
cid,
currentChallenge.language.toLowerCase() || 'python',
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 W3Schools 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>W3Schools Handbook</span>
</button>
</div>
</header>
{/* Main Container */}
<div className="flex flex-1 overflow-hidden relative">
{/* Left Column: Challenge Specifications (Scalable Width) */}
<div
style={{ width: `${leftWidth}px` }}
className="border-r border-darkBorder 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-1.5 bg-darkBorder hover:bg-blue-500 active:bg-blue-600 cursor-col-resize flex items-center justify-center shrink-0 group transition-colors"
title="Drag to resize Challenge Panel"
>
<GripVertical size={10} className="text-gray-600 group-hover:text-white" />
</div>
{/* Middle Column: Monaco Code Editor & Resizable Output Console */}
<div className="flex-1 flex flex-col bg-[#1e1e1e] overflow-hidden">
<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">main.py</span>
<span className="text-[11px] text-gray-500">TactiTerm IDE</span>
</div>
{/* Code Editor */}
<div className="flex-1 relative">
<Editor
height="100%"
defaultLanguage={currentChallenge?.language.toLowerCase() || 'python'}
language={currentChallenge?.language.toLowerCase() || 'python'}
defaultValue=""
value={code}
theme="vs-dark"
onMount={(_, monaco) => 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-1.5 bg-darkBorder hover:bg-blue-500 active:bg-blue-600 cursor-row-resize flex items-center justify-center shrink-0 group transition-colors"
title="Drag up/down to resize Output Console"
>
<GripHorizontal size={10} className="text-gray-600 group-hover:text-white" />
</div>
{/* Scalable Output & Stdin Input Terminal Area */}
<div
style={{ height: `${outputHeight}px` }}
className="border-t border-darkBorder bg-black/90 p-4 font-mono text-xs flex flex-col overflow-hidden shrink-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-1.5 bg-darkBorder hover:bg-blue-500 active:bg-blue-600 cursor-col-resize flex items-center justify-center shrink-0 group transition-colors"
title="Drag to resize Right Sidebar"
>
<GripVertical size={10} className="text-gray-600 group-hover:text-white" />
</div>
)}
{/* Right Column Mode 1: Mentor Sidebar (Scalable Width) */}
{rightSidebarMode === 'mentor' && (
<div
style={{ width: `${rightWidth}px` }}
className="border-l border-darkBorder 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">
<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"
>
<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"
>
<Sparkles size={14} />
<span>Check</span>
</button>
</div>
</div>
</div>
)}
{/* Right Column Mode 2: W3Schools-Style Handbook Sidebar (Scalable Width) */}
{rightSidebarMode === 'handbook' && (
<div
style={{ width: `${rightWidth}px` }}
className="border-l border-darkBorder 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} /> W3Schools 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 */}
<div className="p-3 border-b border-darkBorder bg-darkBg flex flex-col gap-1.5 max-h-48 overflow-y-auto">
{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 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 W3Schools Reference Card View */}
<div className="flex-1 p-4 overflow-y-auto">
<div className="p-4 bg-darkBg border border-darkBorder rounded-xl shadow-inner">
<MarkdownView content={handbookExample} />
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default Workspace;