'use client'; import { useState, useCallback, useEffect, useMemo } from 'react'; import { ChevronLeft, ChevronRight, FileText, Lightbulb, Image as ImageIcon } from 'lucide-react'; import { clsx } from 'clsx'; import { CodeEditor } from './CodeEditor'; import { ProblemList } from './ProblemList'; import { TestRunner } from './TestRunner'; import { AIHelper } from './AIHelper'; import { TASK_LABELS, CATEGORY_LABELS } from '@/config/constants'; import { extractCodeFromResponse, normalizeIndentation } from '@/lib/utils/response'; import type { CodingProblem, TestResult } from '@/types'; interface PracticeInterfaceProps { className?: string; } // State saved per problem interface ProblemState { code: string; testResult: TestResult | null; } export function PracticeInterface({ className }: PracticeInterfaceProps) { const [selectedProblem, setSelectedProblem] = useState(null); const [userCode, setUserCode] = useState(''); const [currentTestResult, setCurrentTestResult] = useState(null); // Store state per problem (code and test results) const [problemStates, setProblemStates] = useState>(new Map()); const [solvedProblems, setSolvedProblems] = useState>(() => { if (typeof window !== 'undefined') { const stored = localStorage.getItem('solvedProblems'); if (stored) { try { return new Set(JSON.parse(stored)); } catch { return new Set(); } } } return new Set(); }); const [isProblemListCollapsed, setIsProblemListCollapsed] = useState(false); const [isAIHelperCollapsed, setIsAIHelperCollapsed] = useState(true); const [problemListWidth, setProblemListWidth] = useState(320); const [aiHelperWidth, setAIHelperWidth] = useState(320); useEffect(() => { if (typeof window !== 'undefined') { localStorage.setItem('solvedProblems', JSON.stringify([...solvedProblems])); } }, [solvedProblems]); // Save current problem state when code changes useEffect(() => { if (selectedProblem && userCode) { setProblemStates(prev => { const newStates = new Map(prev); const existing = newStates.get(selectedProblem.id); newStates.set(selectedProblem.id, { code: userCode, testResult: existing?.testResult ?? currentTestResult, }); return newStates; }); } }, [selectedProblem, userCode]); // Extract code from question for function_completion problems const extractCodeFromQuestion = useCallback((question: string): { description: string; code: string | null } => { const codeBlockMatch = question.match(/```python\n([\s\S]*?)```/); if (codeBlockMatch) { // Remove the code block from the description const description = question.replace(/```python\n[\s\S]*?```/, '').trim(); return { description, code: codeBlockMatch[1].trim() }; } return { description: question, code: null }; }, []); // Get display description (without code block for function_completion) const displayDescription = useMemo(() => { if (!selectedProblem) return ''; if (selectedProblem.type === 'function_completion') { const { description } = extractCodeFromQuestion(selectedProblem.question); return description || 'Complete the function below:'; } return selectedProblem.question; }, [selectedProblem, extractCodeFromQuestion]); // Get the function signature for function_completion problems // Returns imports + def line + docstring (everything before 'pass') const getFunctionSignature = useCallback((question: string): string | null => { const { code } = extractCodeFromQuestion(question); if (!code) return null; const lines = code.split('\n'); const signatureLines: string[] = []; let foundDef = false; let inDocstring = false; let docstringChar = ''; let docstringComplete = false; for (const line of lines) { const trimmed = line.trim(); // Check if this is the def line if (!foundDef && trimmed.startsWith('def ')) { foundDef = true; signatureLines.push(line); continue; } // If we haven't found def yet, this is an import or other preamble - include it if (!foundDef) { signatureLines.push(line); continue; } // After def line, check for docstring if (!inDocstring && !docstringComplete && (line.includes('"""') || line.includes("'''"))) { signatureLines.push(line); docstringChar = line.includes('"""') ? '"""' : "'''"; // Check if docstring starts and ends on same line const count = (line.match(new RegExp(docstringChar.replace(/"/g, '\\"'), 'g')) || []).length; if (count >= 2) { // Docstring complete on one line docstringComplete = true; continue; } inDocstring = true; continue; } // Check for docstring end (multi-line docstring) if (inDocstring && line.includes(docstringChar)) { signatureLines.push(line); inDocstring = false; docstringComplete = true; continue; } // Still inside multi-line docstring if (inDocstring) { signatureLines.push(line); continue; } // After docstring is complete, stop at 'pass' or any actual code if (docstringComplete || foundDef) { if (trimmed === 'pass' || trimmed === '' || trimmed.startsWith('#')) { // Skip 'pass', empty lines, and comments after docstring continue; } // Found actual implementation code - stop here break; } } return signatureLines.join('\n'); }, [extractCodeFromQuestion]); const handleSelectProblem = useCallback((problem: CodingProblem) => { // Check if we have saved state for this problem const savedState = problemStates.get(problem.id); if (savedState) { // Restore saved code and test result setUserCode(savedState.code); setCurrentTestResult(savedState.testResult); } else { // Set initial code template based on problem type if (problem.type === 'function_completion') { const { code } = extractCodeFromQuestion(problem.question); if (code) { setUserCode(code + '\n # Your code here\n pass'); } else { setUserCode('# Write your solution here\n'); } } else { setUserCode('# Write your solution here\n'); } // Reset test result for new problem setCurrentTestResult(null); } setSelectedProblem(problem); }, [extractCodeFromQuestion, problemStates]); const handleTestComplete = useCallback((result: TestResult) => { setCurrentTestResult(result); if (selectedProblem) { // Save test result to problem state setProblemStates(prev => { const newStates = new Map(prev); newStates.set(selectedProblem.id, { code: userCode, testResult: result, }); return newStates; }); if (result.passed) { setSolvedProblems((prev) => new Set([...prev, selectedProblem.id])); } } }, [selectedProblem, userCode]); const toggleAIHelper = useCallback(() => { setIsAIHelperCollapsed(prev => !prev); }, []); // Handler to apply code from AI Helper to the editor const handleApplyCode = useCallback((code: string) => { if (!selectedProblem) { setUserCode(code); return; } // Extract actual code from markdown code blocks if present const extractedCode = extractCodeFromResponse(code, selectedProblem.entryPoint); if (selectedProblem.type === 'function_completion') { // For function completion, combine the function signature with the generated body const signature = getFunctionSignature(selectedProblem.question); if (signature) { // Check if the AI response already includes the full function definition const hasFullFunction = extractedCode.match(/^\s*def\s+\w+\s*\(/m); if (hasFullFunction) { // AI returned full function - use it directly const normalized = normalizeIndentation(extractedCode, 0); setUserCode(normalized); } else { // AI returned only the body - combine with signature // Normalize the body code to have consistent 4-space indentation for function body const normalizedBody = normalizeIndentation(extractedCode, 4); setUserCode(signature + '\n' + normalizedBody); } } else { setUserCode(extractedCode); } } else { // For code generation, replace the entire code const normalized = normalizeIndentation(extractedCode, 0); setUserCode(normalized); } }, [selectedProblem, getFunctionSignature]); return (
{/* Problem List Sidebar */}
{isProblemListCollapsed ? (
) : ( <>
{ e.preventDefault(); const startX = e.clientX; const startWidth = problemListWidth; const handleMouseMove = (moveEvent: MouseEvent) => { const newWidth = Math.min(500, Math.max(240, startWidth + moveEvent.clientX - startX)); setProblemListWidth(newWidth); }; const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }} /> )}
{/* Main Content */}
{selectedProblem ? ( <> {/* Problem Description - compact header */}
{TASK_LABELS[selectedProblem.type].label} {CATEGORY_LABELS[selectedProblem.category]} {selectedProblem.hasImage && ( Has image )}

{displayDescription}

{selectedProblem.imageUrl && (
Problem illustration
)}
{/* Test Runner - at top, compact */}
{/* Code Editor - takes remaining space */}
) : (

Practice Mode

Select a coding problem from the sidebar to start practicing. Solve problems and run unit tests to verify your solutions.

Use the AI Helper for hints and guidance
)}
{/* AI Helper Sidebar */}
{!isAIHelperCollapsed && ( <>
{ e.preventDefault(); const startX = e.clientX; const startWidth = aiHelperWidth; const handleMouseMove = (moveEvent: MouseEvent) => { const newWidth = Math.min(500, Math.max(240, startWidth - (moveEvent.clientX - startX))); setAIHelperWidth(newWidth); }; const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }} /> )}
); }