"use client"; import { useState, useCallback, useMemo, useEffect } from "react"; import { CodeEditor } from "./code-editor"; import { TestResults } from "./test-results"; import { usePyodide } from "@/hooks/use-pyodide"; import { useTimeTracker, getTimeTrackerElapsed } from "@/hooks/use-time-tracker"; import { submitSolution } from "@/lib/api"; import { markQuestionCompleted, getSavedSolution, isQuestionCompleted } from "@/lib/progress"; import type { QuestionDetail, TestResult, HiddenTestOutput } from "@/types"; import { Badge } from "@/components/ui/badge"; import { Markdown } from "@/components/ui/markdown"; import { CodeBlock } from "@/components/ui/code-block"; import { Callout, ApproachBox } from "@/components/ui/callout"; import { Collapsible } from "@/components/ui/collapsible"; import { getDifficultyVariant, capitalize } from "@/lib/utils"; import { detectProblemType, TREE_NODE_CLASS, LIST_NODE_CLASS, BUILD_TREE, TREE_TO_ARRAY, BUILD_LIST, LIST_TO_ARRAY, } from "@/lib/python-helpers"; import { COMPLEXITY_ANALYZER, type ComplexityResult, } from "@/lib/complexity-analyzer"; import { FileText, BookOpen, AlertCircle, Brain, ClipboardList, AlertTriangle, Lightbulb, Code, Play, Send, Loader2, PanelLeftClose, PanelRightClose, Columns2, Gauge, ChevronRight, CheckCircle, XCircle, GitCompare, } from "lucide-react"; import Link from "next/link"; interface ProblemWorkspaceProps { question: QuestionDetail; } interface ExecutionResult { test_id: number; output: unknown; error?: string; } function extractFunctionName(signature: string): string { // Check for class-based signature: "class MinStack" -> "MinStack" const classMatch = signature.match(/class\s+(\w+)/); if (classMatch) return classMatch[1]; // Function: "def two_sum(" -> "two_sum" const funcMatch = signature.match(/def\s+(\w+)\s*\(/); return funcMatch ? funcMatch[1] : "solution"; } function generateStarterCode(signature: string): string { // Class-based problems need a different starter template if (signature.startsWith("class ")) { return `${signature}: def __init__(self): # Initialize your data structure here pass `; } return `${signature} # Write your solution here pass `; } /** * Compare actual and expected outputs with flexible matching. * Handles unordered array comparison for problems like subsets. */ function compareOutputs(actual: unknown, expected: unknown): boolean { // Direct equality check if (JSON.stringify(actual) === JSON.stringify(expected)) { return true; } // Handle unordered array comparison (e.g., subsets, word-break-ii) if (Array.isArray(actual) && Array.isArray(expected)) { if (actual.length !== expected.length) { return false; } // Sort both arrays by their JSON string representation for comparison const sortFn = (a: unknown, b: unknown) => JSON.stringify(a).localeCompare(JSON.stringify(b)); const sortedActual = [...actual].sort(sortFn); const sortedExpected = [...expected].sort(sortFn); return JSON.stringify(sortedActual) === JSON.stringify(sortedExpected); } return false; } type LeftTab = "problem" | "explanation" | "solutions"; type RightTab = "code" | "results"; type ViewMode = "split" | "left" | "right"; export function ProblemWorkspace({ question }: ProblemWorkspaceProps) { // Start time tracking for this question useTimeTracker(question.slug); const starterCode = useMemo( () => generateStarterCode(question.function_signature!), [question.function_signature] ); const [code, setCode] = useState(starterCode); const [leftTab, setLeftTab] = useState("problem"); const [rightTab, setRightTab] = useState("code"); const [viewMode, setViewMode] = useState("split"); const [hasRunTests, setHasRunTests] = useState(false); // Test execution state const { pyodide, loading: pyodideLoading, runPython } = usePyodide(); const [isRunning, setIsRunning] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [visibleResults, setVisibleResults] = useState([]); const [hiddenPassedCount, setHiddenPassedCount] = useState(0); const [hiddenTotalCount, setHiddenTotalCount] = useState(0); const [complexityResult, setComplexityResult] = useState(null); const [isComplexityExpanded, setIsComplexityExpanded] = useState(false); const [isCompleted, setIsCompleted] = useState(false); // Load saved solution and completion status on mount useEffect(() => { const saved = getSavedSolution(question.slug); if (saved) { setCode(saved); } setIsCompleted(isQuestionCompleted(question.slug)); }, [question.slug]); const functionName = useMemo( () => extractFunctionName(question.function_signature!), [question.function_signature] ); const visibleTestCases = useMemo( () => question.visible_test_cases ?? [], [question.visible_test_cases] ); const hiddenTestInputs = useMemo( () => question.hidden_test_inputs ?? [], [question.hidden_test_inputs] ); const handleReset = useCallback(() => { setCode(starterCode); }, [starterCode]); const toggleViewMode = useCallback((panel: "left" | "right") => { setViewMode((current) => { if (current === "split") { return panel; // Maximize the clicked panel } return "split"; // Return to split view }); }, []); const signature = question.function_signature ?? ""; const generateTestHarness = useCallback( (tests: Array<{ id: number; input: Record }>) => { // Use first test to detect problem type const firstInput = tests[0]?.input ?? {}; const problemType = detectProblemType(signature, firstInput); const testsJson = JSON.stringify(tests); if (problemType === "class-based") { // Class-based harness: operations/args format return ` import json ${code} __tests__ = json.loads('${testsJson.replace(/'/g, "\\'")}') __results__ = [] for test in __tests__: try: operations = test["input"]["operations"] arguments = test["input"]["args"] outputs = [] instance = None for op, args in zip(operations, arguments): if instance is None: # First operation is always the constructor instance = globals()[op](*args) outputs.append(None) else: method_result = getattr(instance, op)(*args) outputs.append(method_result) __results__.append({"test_id": test["id"], "output": outputs}) except Exception as e: __results__.append({"test_id": test["id"], "output": None, "error": str(e)}) json.dumps(__results__) `; } if (problemType === "tree") { // Tree-based harness: convert arrays to TreeNode, convert result back return ` import json ${TREE_NODE_CLASS} ${BUILD_TREE} ${TREE_TO_ARRAY} ${code} __tests__ = json.loads('${testsJson.replace(/'/g, "\\'")}') __results__ = [] for test in __tests__: try: processed = dict(test["input"]) # Convert tree parameters from array to TreeNode for key in ["root", "tree", "p", "q"]: if key in processed and isinstance(processed[key], list): processed[key] = __build_tree(processed[key]) result = ${functionName}(**processed) # Convert TreeNode result back to array if isinstance(result, TreeNode): result = __tree_to_array(result) __results__.append({"test_id": test["id"], "output": result}) except Exception as e: __results__.append({"test_id": test["id"], "output": None, "error": str(e)}) json.dumps(__results__) `; } if (problemType === "linkedlist") { // Linked list harness: convert arrays to ListNode, convert result back return ` import json ${LIST_NODE_CLASS} ${BUILD_LIST} ${LIST_TO_ARRAY} ${code} __tests__ = json.loads('${testsJson.replace(/'/g, "\\'")}') __results__ = [] for test in __tests__: try: processed = dict(test["input"]) # Convert list parameters from array to ListNode for key in ["head", "l1", "l2", "list1", "list2", "node"]: if key in processed and isinstance(processed[key], list): processed[key] = __build_list(processed[key]) result = ${functionName}(**processed) # Convert ListNode result back to array if isinstance(result, ListNode): result = __list_to_array(result) __results__.append({"test_id": test["id"], "output": result}) except Exception as e: __results__.append({"test_id": test["id"], "output": None, "error": str(e)}) json.dumps(__results__) `; } // Simple function harness (default) return ` import json ${code} __tests__ = json.loads('${testsJson.replace(/'/g, "\\'")}') __results__ = [] for test in __tests__: try: result = ${functionName}(**test["input"]) __results__.append({"test_id": test["id"], "output": result}) except Exception as e: __results__.append({"test_id": test["id"], "output": None, "error": str(e)}) json.dumps(__results__) `; }, [code, functionName, signature] ); const runTests = useCallback( async (tests: Array<{ id: number; input: Record }>) => { const harness = generateTestHarness(tests); const { output, error } = await runPython(harness); if (error) { return tests.map((t) => ({ test_id: t.id, output: null, error: error, })); } try { return JSON.parse(output as string) as ExecutionResult[]; } catch { return tests.map((t) => ({ test_id: t.id, output: null, error: "Failed to parse output", })); } }, [generateTestHarness, runPython] ); const analyzeComplexity = useCallback( async (userCode: string) => { const analyzerCode = ` ${COMPLEXITY_ANALYZER} import json __user_code = '''${userCode.replace(/'/g, "\\'")}''' __result = analyze_complexity(__user_code) json.dumps(__result) `; const { output, error } = await runPython(analyzerCode); if (error) { return { success: false, error } as ComplexityResult; } try { return JSON.parse(output as string) as ComplexityResult; } catch { return { success: false, error: "Failed to parse analysis" } as ComplexityResult; } }, [runPython] ); const handleRunTests = useCallback(async () => { if (!pyodide || isRunning) return; setIsRunning(true); setHiddenPassedCount(0); setHiddenTotalCount(0); setComplexityResult(null); try { const tests = visibleTestCases.map((tc) => ({ id: tc.id, input: tc.input, })); const results = await runTests(tests); const testResults: TestResult[] = results.map((r, idx) => { const testCase = visibleTestCases[idx]; const passed = !r.error && compareOutputs(r.output, testCase.expected); return { test_id: r.test_id, passed, input: testCase.input, expected: testCase.expected, actual: r.output, error: r.error, }; }); setVisibleResults(testResults); setHasRunTests(true); // Run complexity analysis in the background const complexity = await analyzeComplexity(code); setComplexityResult(complexity); } finally { setIsRunning(false); } }, [pyodide, isRunning, visibleTestCases, runTests, analyzeComplexity, code]); const handleSubmit = useCallback(async () => { if (!pyodide || isSubmitting) return; setIsSubmitting(true); setComplexityResult(null); try { // Run visible tests first const visibleTests = visibleTestCases.map((tc) => ({ id: tc.id, input: tc.input, })); const visibleExecutionResults = await runTests(visibleTests); const visibleTestResults: TestResult[] = visibleExecutionResults.map( (r, idx) => { const testCase = visibleTestCases[idx]; const passed = !r.error && compareOutputs(r.output, testCase.expected); return { test_id: r.test_id, passed, input: testCase.input, expected: testCase.expected, actual: r.output, error: r.error, }; } ); setVisibleResults(visibleTestResults); // Run hidden tests const hiddenTests = hiddenTestInputs.map((tc) => ({ id: tc.id, input: tc.input, })); const hiddenExecutionResults = await runTests(hiddenTests); // Submit hidden outputs to server for validation const hiddenOutputs: HiddenTestOutput[] = hiddenExecutionResults.map( (r) => ({ test_id: r.test_id, output: r.output, }) ); const response = await submitSolution(question.slug, { code, hidden_outputs: hiddenOutputs, }); setHiddenPassedCount(response.total_passed); setHiddenTotalCount(response.total_tests); setHasRunTests(true); // Run complexity analysis in the background const complexity = await analyzeComplexity(code); setComplexityResult(complexity); // Check if all tests passed const allVisiblePassed = visibleTestResults.every((r) => r.passed); const allHiddenPassed = response.total_passed === response.total_tests; if (allVisiblePassed && allHiddenPassed) { const timeSpentMs = getTimeTrackerElapsed(); const primaryPattern = question.patterns[0]?.slug || ""; markQuestionCompleted(question.slug, { primaryPattern, difficulty: question.difficulty, code, timeSpentMs, }); setIsCompleted(true); } // Auto-switch to Results tab after submit setRightTab("results"); } catch (error) { console.error("Submission error:", error); } finally { setIsSubmitting(false); } }, [ pyodide, isSubmitting, visibleTestCases, hiddenTestInputs, runTests, question.slug, question.difficulty, question.patterns, code, analyzeComplexity, ]); const isLoading = isRunning || isSubmitting; return (
{/* Left panel - Problem description / Explanation */} {viewMode !== "right" && (
{/* Header - compact */}
← Back

{question.title}

{capitalize(question.difficulty)} {isCompleted && ( Completed )}
{question.leetcode_url && ( LeetCode ↗ )}
{question.categories.map((cat) => ( {cat.name} ))} {question.patterns.map((pat) => ( {pat.name} ))}
{/* Tabs */}
{/* Tab content - scrollable */}
{leftTab === "problem" && (
{/* Problem */}

Problem

{question.description}
{/* Constraints */} {question.constraints && (

Constraints

{question.constraints}
)} {/* Examples */} {question.examples && question.examples.length > 0 && (

Examples

{question.examples.map((example, i) => (
Input: {example.input}
Output: {example.output}
{example.explanation && (
{example.explanation}
)}
))}
)}
)} {leftTab === "explanation" && (
{/* Intuition */} {question.explanation?.intuition && ( {question.explanation.intuition} )} {/* Approach */} {question.explanation?.approach && ( {question.explanation.approach} )} {/* Common Pitfalls */} {question.explanation?.common_pitfalls && question.explanation.common_pitfalls.length > 0 && (
{question.explanation.common_pitfalls.map((pitfall, i) => (

{pitfall.title}

{pitfall.description} {pitfall.wrong_approach && pitfall.correct_approach && (
{pitfall.wrong_approach} {pitfall.correct_approach}
)}
))}
)} {/* Key Takeaways */} {question.explanation?.key_takeaways && question.explanation.key_takeaways.length > 0 && (
    {question.explanation.key_takeaways.map((takeaway, i) => (
  • {takeaway}
  • ))}
)} {/* Why This Pattern? */} {question.explanation?.pattern_comparison && ( {question.explanation.pattern_comparison} )} {/* Complexity Analysis */} {(question.explanation?.time_complexity || question.explanation?.space_complexity) && (

Complexity Analysis

{question.explanation?.time_complexity && (

Time:{" "} {question.explanation.time_complexity}

)} {question.explanation?.space_complexity && (

Space:{" "} {question.explanation.space_complexity}

)}
)}
)} {leftTab === "solutions" && (
{question.solutions && question.solutions.length > 0 ? (
{question.solutions.map((solution, i) => { const content = (

{solution.approach_name}

{solution.is_optimal && ( Optimal )}
{solution.explanation && (
{solution.explanation}
)}
); return solution.is_optimal ? (
{content}
) : ( {content} ); })}
) : (

No solutions available for this problem yet.

)}
)}
)} {/* Right panel - Editor and test results */} {viewMode !== "left" && (
{/* Tabs header */}
{hasRunTests && (() => { const visiblePassed = visibleResults.filter(r => r.passed).length; const visibleTotal = visibleResults.length; const allVisiblePassed = visibleTotal > 0 && visiblePassed === visibleTotal; const allHiddenPassed = hiddenTotalCount === 0 || hiddenPassedCount === hiddenTotalCount; const allPassed = allVisiblePassed && allHiddenPassed; return ( ); })()}
{/* Tab content */}
{rightTab === "code" ? ( <> {/* Editor */}
{/* Complexity Analysis - shown after running tests */} {complexityResult?.success && (
Estimated: {complexityResult.complexity} {(complexityResult.details?.findings?.length || complexityResult.details?.recursive_calls?.length) && ( )}
{isComplexityExpanded && complexityResult.details && (
{/* Loop findings */} {complexityResult.details.findings ?.filter((f) => f.type === "loop") .map((finding, i) => (
{finding.kind === "for" ? ( <> Loop over {finding.iterates_over || "iterable"} {finding.depth && finding.depth > 1 ? ` (nested, depth ${finding.depth})` : ""}{" "} → O(n) ) : ( <> While loop {finding.depth && finding.depth > 1 ? ` (nested, depth ${finding.depth})` : ""} )}
L{finding.line} {finding.snippet}
))} {/* Sort operations */} {complexityResult.details.findings ?.filter((f) => f.kind === "sort") .map((finding, i) => (
Sorting operation → O(n log n)
L{finding.line} {finding.snippet}
))} {/* Linear search warnings */} {complexityResult.details.findings ?.filter((f) => f.kind === "linear_search") .map((finding, i) => (
{finding.note || "Linear search in loop"}
L{finding.line} {finding.snippet}
))} {/* Recursive calls */} {complexityResult.details.recursive_calls?.map((call, i) => (
Recursive call
L{call.line} {call.snippet}
))}
)}
)} {/* Action buttons */}
{pyodideLoading ? ( Loading Python... ) : ( "Python ready" )}
) : ( /* Results tab */
{/* Complexity Analysis in Results tab */} {complexityResult?.success && (
Estimated Complexity: {complexityResult.complexity}
)} {/* Success state with navigation */} {(() => { const visiblePassed = visibleResults.filter((r) => r.passed).length; const visibleTotal = visibleResults.length; const allVisiblePassed = visibleTotal > 0 && visiblePassed === visibleTotal; const allHiddenPassed = hiddenTotalCount === 0 || hiddenPassedCount === hiddenTotalCount; const allPassed = allVisiblePassed && allHiddenPassed && hiddenTotalCount > 0; if (!allPassed) return null; return (
All tests passed!
Back to Questions
); })()}
)}
)}
); }