feat(frontend): add code editor

This commit is contained in:
2025-05-21 21:48:52 +01:00
parent f74f1d89b6
commit 7d789ba82f
5 changed files with 1045 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
"use client";
import Editor, { type OnMount } from "@monaco-editor/react";
import { useCallback, useRef } from "react";
interface CodeEditorProps {
value: string;
onChange: (value: string) => void;
}
export function CodeEditor({ value, onChange }: CodeEditorProps) {
const editorRef = useRef<Parameters<OnMount>[0] | null>(null);
const handleEditorMount: OnMount = useCallback((editor) => {
editorRef.current = editor;
}, []);
const handleChange = useCallback(
(newValue: string | undefined) => {
onChange(newValue ?? "");
},
[onChange]
);
return (
<div className="h-full">
<Editor
height="100%"
language="python"
theme="vs-dark"
value={value}
onChange={handleChange}
onMount={handleEditorMount}
options={{
minimap: { enabled: false },
fontSize: 14,
lineNumbers: "on",
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 4,
insertSpaces: true,
wordWrap: "on",
padding: { top: 12 },
}}
/>
</div>
);
}

View File

@@ -0,0 +1,4 @@
export { CodeEditor } from "./code-editor";
export { TestRunner } from "./test-runner";
export { TestResults } from "./test-results";
export { ProblemWorkspace } from "./problem-workspace";

View File

@@ -0,0 +1,627 @@
"use client";
import { useState, useCallback, useMemo } from "react";
import { CodeEditor } from "./code-editor";
import { TestResults } from "./test-results";
import { usePyodide } from "@/hooks/use-pyodide";
import { submitSolution } from "@/lib/api";
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 {
FileText,
BookOpen,
AlertCircle,
Brain,
ClipboardList,
AlertTriangle,
Lightbulb,
Code,
Play,
Send,
Loader2,
} 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 {
const match = signature.match(/def\s+(\w+)\s*\(/);
return match ? match[1] : "solution";
}
function generateStarterCode(signature: string): string {
return `${signature}
# Write your solution here
pass
`;
}
type LeftTab = "problem" | "explanation";
type RightTab = "code" | "results";
export function ProblemWorkspace({ question }: ProblemWorkspaceProps) {
const starterCode = useMemo(
() => generateStarterCode(question.function_signature!),
[question.function_signature]
);
const [code, setCode] = useState(starterCode);
const [leftTab, setLeftTab] = useState<LeftTab>("problem");
const [rightTab, setRightTab] = useState<RightTab>("code");
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<TestResult[]>([]);
const [hiddenPassedCount, setHiddenPassedCount] = useState(0);
const [hiddenTotalCount, setHiddenTotalCount] = useState(0);
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 generateTestHarness = useCallback(
(tests: Array<{ id: number; input: Record<string, unknown> }>) => {
const testsJson = JSON.stringify(tests);
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]
);
const runTests = useCallback(
async (tests: Array<{ id: number; input: Record<string, unknown> }>) => {
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 handleRunTests = useCallback(async () => {
if (!pyodide || isRunning) return;
setIsRunning(true);
setHiddenPassedCount(0);
setHiddenTotalCount(0);
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 &&
JSON.stringify(r.output) === JSON.stringify(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);
setRightTab("results");
} finally {
setIsRunning(false);
}
}, [pyodide, isRunning, visibleTestCases, runTests]);
const handleSubmit = useCallback(async () => {
if (!pyodide || isSubmitting) return;
setIsSubmitting(true);
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 &&
JSON.stringify(r.output) === JSON.stringify(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);
setRightTab("results");
} catch (error) {
console.error("Submission error:", error);
} finally {
setIsSubmitting(false);
}
}, [
pyodide,
isSubmitting,
visibleTestCases,
hiddenTestInputs,
runTests,
question.slug,
code,
]);
const isLoading = isRunning || isSubmitting;
return (
<div className="fixed inset-0 top-[65px] flex bg-[var(--background)]">
{/* Left panel - Problem description / Explanation */}
<div className="w-1/2 border-r border-[var(--border)] flex flex-col">
{/* Header - compact */}
<div className="px-4 pt-3 pb-0">
<div className="flex items-center justify-between gap-2 mb-2">
<div className="flex items-center gap-3">
<Link
href="/questions"
className="text-xs text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
>
&larr; Back
</Link>
<h1 className="text-lg font-bold">{question.title}</h1>
<Badge variant={getDifficultyVariant(question.difficulty)} className="text-xs">
{capitalize(question.difficulty)}
</Badge>
</div>
{question.leetcode_url && (
<a
href={question.leetcode_url}
target="_blank"
rel="noopener noreferrer"
className="text-[var(--primary)] hover:underline text-xs"
>
LeetCode
</a>
)}
</div>
<div className="flex flex-wrap gap-1.5 mb-2">
{question.categories.map((cat) => (
<Link key={cat.id} href={`/questions?category=${cat.slug}`}>
<Badge variant="category" className="text-xs py-0">
{cat.name}
</Badge>
</Link>
))}
{question.patterns.map((pat) => (
<Link key={pat.id} href={`/patterns/${pat.slug}`}>
<Badge variant="pattern" className="text-xs py-0">
{pat.name}
</Badge>
</Link>
))}
</div>
{/* Tabs */}
<div className="flex gap-1 border-b border-[var(--border)]">
<button
onClick={() => setLeftTab("problem")}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
leftTab === "problem"
? "border-[var(--primary)] text-[var(--foreground)]"
: "border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
}`}
>
Problem
</button>
<button
onClick={() => setLeftTab("explanation")}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
leftTab === "explanation"
? "border-[var(--primary)] text-[var(--foreground)]"
: "border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
}`}
>
Explanation
</button>
</div>
</div>
{/* Tab content - scrollable */}
<div className="flex-1 overflow-auto px-4 py-3">
{leftTab === "problem" ? (
<div className="space-y-6">
{/* Problem */}
<div>
<h2 className="flex items-center gap-2 text-lg font-semibold mb-3">
<FileText className="h-5 w-5" />
Problem
</h2>
<div className="prose-content">
<Markdown>{question.description}</Markdown>
</div>
</div>
{/* Constraints */}
{question.constraints && (
<div>
<h2 className="flex items-center gap-2 text-lg font-semibold mb-3">
<AlertCircle className="h-5 w-5" />
Constraints
</h2>
<Markdown>{question.constraints}</Markdown>
</div>
)}
{/* Examples */}
{question.examples && question.examples.length > 0 && (
<div>
<h2 className="flex items-center gap-2 text-lg font-semibold mb-3">
<BookOpen className="h-5 w-5" />
Examples
</h2>
<div className="space-y-3">
{question.examples.map((example, i) => (
<div
key={i}
className="p-3 rounded bg-[var(--secondary)] space-y-1"
>
<div>
<span className="font-medium">Input: </span>
<code>{example.input}</code>
</div>
<div>
<span className="font-medium">Output: </span>
<code>{example.output}</code>
</div>
{example.explanation && (
<div className="text-sm text-[var(--muted-foreground)]">
{example.explanation}
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
) : (
<div className="space-y-6">
{/* Intuition */}
{question.explanation?.intuition && (
<Callout variant="insight" icon={Brain} title="Intuition">
<Markdown>{question.explanation.intuition}</Markdown>
</Callout>
)}
{/* Approach */}
{question.explanation?.approach && (
<Callout variant="info" icon={ClipboardList} title="Approach">
<Markdown>{question.explanation.approach}</Markdown>
</Callout>
)}
{/* Common Pitfalls */}
{question.explanation?.common_pitfalls &&
question.explanation.common_pitfalls.length > 0 && (
<Callout
variant="warning"
icon={AlertTriangle}
title="Common Pitfalls"
>
<div className="space-y-4">
{question.explanation.common_pitfalls.map((pitfall, i) => (
<div key={i}>
<h4 className="font-medium mb-2">{pitfall.title}</h4>
<Markdown>{pitfall.description}</Markdown>
{pitfall.wrong_approach && pitfall.correct_approach && (
<div className="mt-2 flex gap-2 flex-wrap">
<ApproachBox variant="wrong">
{pitfall.wrong_approach}
</ApproachBox>
<ApproachBox variant="correct">
{pitfall.correct_approach}
</ApproachBox>
</div>
)}
</div>
))}
</div>
</Callout>
)}
{/* Key Takeaways */}
{question.explanation?.key_takeaways &&
question.explanation.key_takeaways.length > 0 && (
<Callout
variant="success"
icon={Lightbulb}
title="Key Takeaways"
>
<ul className="space-y-2">
{question.explanation.key_takeaways.map((takeaway, i) => (
<li
key={i}
className="[&>div]:inline [&>div>p]:inline"
>
<Markdown>{takeaway}</Markdown>
</li>
))}
</ul>
</Callout>
)}
{/* Complexity Analysis */}
{(question.explanation?.time_complexity ||
question.explanation?.space_complexity) && (
<div>
<h2 className="flex items-center gap-2 text-lg font-semibold mb-3">
Complexity Analysis
</h2>
<div className="space-y-2">
{question.explanation?.time_complexity && (
<p>
<strong>Time:</strong>{" "}
<Markdown>
{question.explanation.time_complexity}
</Markdown>
</p>
)}
{question.explanation?.space_complexity && (
<p>
<strong>Space:</strong>{" "}
<Markdown>
{question.explanation.space_complexity}
</Markdown>
</p>
)}
</div>
</div>
)}
{/* Solutions */}
{question.solutions && question.solutions.length > 0 && (
<div>
<h2 className="flex items-center gap-2 text-lg font-semibold mb-3">
<Code className="h-5 w-5" />
Solutions
</h2>
<div className="space-y-4">
{question.solutions.map((solution, i) => {
const content = (
<div className="space-y-3">
<div className="flex items-center gap-2">
<h3 className="font-medium">
{solution.approach_name}
</h3>
{solution.is_optimal && (
<Badge variant="optimal">Optimal</Badge>
)}
</div>
<CodeBlock language="python" code={solution.code} />
{solution.explanation && (
<div className="text-sm text-[var(--muted-foreground)]">
<Markdown>{solution.explanation}</Markdown>
</div>
)}
</div>
);
return solution.is_optimal ? (
<div key={i}>{content}</div>
) : (
<Collapsible
key={i}
title={`${solution.approach_name} (Non-optimal)`}
>
{content}
</Collapsible>
);
})}
</div>
</div>
)}
</div>
)}
</div>
</div>
{/* Right panel - Editor and test results */}
<div className="w-1/2 flex flex-col">
{/* Tabs header */}
<div className="px-4 pt-2 border-b border-[var(--border)] flex items-center justify-between">
<div className="flex gap-1">
<button
onClick={() => setRightTab("code")}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
rightTab === "code"
? "border-[var(--primary)] text-[var(--foreground)]"
: "border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
}`}
>
<Code className="h-4 w-4" />
Code
</button>
{hasRunTests && (
<button
onClick={() => setRightTab("results")}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
rightTab === "results"
? "border-[var(--primary)] text-[var(--foreground)]"
: "border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
}`}
>
<FileText className="h-4 w-4" />
Results
</button>
)}
</div>
<button
onClick={handleReset}
className="text-xs text-[var(--muted-foreground)] hover:text-[var(--foreground)] px-2 py-1"
>
Reset
</button>
</div>
{/* Tab content */}
<div className="flex-1 min-h-0 flex flex-col">
{rightTab === "code" ? (
<>
{/* Editor - full height */}
<div className="flex-1 min-h-0">
<CodeEditor value={code} onChange={setCode} />
</div>
{/* Action buttons */}
<div className="p-3 border-t border-[var(--border)] flex items-center justify-between bg-[var(--secondary)]">
<div className="text-sm text-[var(--muted-foreground)]">
{pyodideLoading ? (
<span className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Loading Python...
</span>
) : (
"Python ready"
)}
</div>
<div className="flex gap-2">
<button
onClick={handleRunTests}
disabled={pyodideLoading || isLoading}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium rounded bg-[var(--secondary)] border border-[var(--border)] hover:bg-[var(--accent)] disabled:opacity-50 disabled:cursor-not-allowed"
>
{isRunning ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Play className="h-4 w-4" />
)}
Run Tests
</button>
<button
onClick={handleSubmit}
disabled={pyodideLoading || isLoading}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium rounded bg-[var(--primary)] text-[var(--primary-foreground)] hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
Submit
</button>
</div>
</div>
</>
) : (
/* Results tab */
<div className="flex-1 overflow-auto p-4">
<TestResults
visibleResults={visibleResults}
hiddenPassedCount={hiddenPassedCount}
hiddenTotalCount={hiddenTotalCount}
visibleTestCases={visibleTestCases}
/>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,121 @@
"use client";
import { CheckCircle, XCircle, Loader2 } from "lucide-react";
import type { TestResult, VisibleTestCase } from "@/types";
interface TestResultsProps {
visibleResults: TestResult[];
hiddenPassedCount: number;
hiddenTotalCount: number;
isRunning?: boolean;
visibleTestCases?: VisibleTestCase[];
}
export function TestResults({
visibleResults,
hiddenPassedCount,
hiddenTotalCount,
isRunning,
visibleTestCases,
}: TestResultsProps) {
if (isRunning) {
return (
<div className="p-4 flex items-center gap-2 text-[var(--muted-foreground)]">
<Loader2 className="h-4 w-4 animate-spin" />
<span>Running tests...</span>
</div>
);
}
if (visibleResults.length === 0 && hiddenTotalCount === 0) {
return (
<div className="p-4 text-[var(--muted-foreground)]">
Click &quot;Run Tests&quot; to test your solution with visible test
cases, or &quot;Submit&quot; to run all tests.
</div>
);
}
const formatValue = (value: unknown): string => {
if (value === null || value === undefined) return "null";
if (typeof value === "string") return `"${value}"`;
return JSON.stringify(value);
};
const formatInput = (input: Record<string, unknown>): string => {
return Object.entries(input)
.map(([key, val]) => `${key} = ${formatValue(val)}`)
.join(", ");
};
return (
<div className="divide-y divide-[var(--border)]">
{visibleResults.map((result, index) => {
const testCase = visibleTestCases?.[index];
return (
<div key={result.test_id} className="p-4 space-y-2">
<div className="flex items-center gap-2">
{result.passed ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<XCircle className="h-4 w-4 text-red-500" />
)}
<span className="font-medium">
Test {result.test_id + 1}: {result.passed ? "Passed" : "Failed"}
</span>
</div>
{!result.passed && (
<div className="ml-6 space-y-1 text-sm">
{result.input && (
<div>
<span className="text-[var(--muted-foreground)]">
Input:{" "}
</span>
<code className="bg-[var(--secondary)] px-1 rounded">
{formatInput(result.input)}
</code>
</div>
)}
{testCase && (
<div>
<span className="text-[var(--muted-foreground)]">
Expected:{" "}
</span>
<code className="bg-[var(--secondary)] px-1 rounded text-green-400">
{formatValue(testCase.expected)}
</code>
</div>
)}
<div>
<span className="text-[var(--muted-foreground)]">Got: </span>
<code className="bg-[var(--secondary)] px-1 rounded text-red-400">
{result.error ? `Error: ${result.error}` : formatValue(result.actual)}
</code>
</div>
</div>
)}
</div>
);
})}
{hiddenTotalCount > 0 && (
<div className="p-4">
<div className="flex items-center gap-2">
{hiddenPassedCount === hiddenTotalCount ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<XCircle className="h-4 w-4 text-red-500" />
)}
<span className="font-medium">
Hidden Tests: {hiddenPassedCount}/{hiddenTotalCount} passed
</span>
</div>
<p className="ml-6 text-sm text-[var(--muted-foreground)] mt-1">
Hidden test details are not shown to prevent cheating.
</p>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,245 @@
"use client";
import { useState, useCallback } from "react";
import { Play, Send, Loader2 } from "lucide-react";
import { usePyodide } from "@/hooks/use-pyodide";
import { submitSolution } from "@/lib/api";
import { TestResults } from "./test-results";
import type {
VisibleTestCase,
HiddenTestInput,
TestResult,
HiddenTestOutput,
} from "@/types";
interface TestRunnerProps {
code: string;
functionName: string;
slug: string;
visibleTestCases: VisibleTestCase[];
hiddenTestInputs: HiddenTestInput[];
}
interface ExecutionResult {
test_id: number;
output: unknown;
error?: string;
}
export function TestRunner({
code,
functionName,
slug,
visibleTestCases,
hiddenTestInputs,
}: TestRunnerProps) {
const { pyodide, loading: pyodideLoading, runPython } = usePyodide();
const [isRunning, setIsRunning] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [visibleResults, setVisibleResults] = useState<TestResult[]>([]);
const [hiddenPassedCount, setHiddenPassedCount] = useState(0);
const [hiddenTotalCount, setHiddenTotalCount] = useState(0);
const generateTestHarness = useCallback(
(tests: Array<{ id: number; input: Record<string, unknown> }>) => {
const testsJson = JSON.stringify(tests);
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]
);
const runTests = useCallback(
async (tests: Array<{ id: number; input: Record<string, unknown> }>) => {
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 handleRunTests = useCallback(async () => {
if (!pyodide || isRunning) return;
setIsRunning(true);
setHiddenPassedCount(0);
setHiddenTotalCount(0);
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 &&
JSON.stringify(r.output) === JSON.stringify(testCase.expected);
return {
test_id: r.test_id,
passed,
input: testCase.input,
expected: testCase.expected,
actual: r.output,
error: r.error,
};
});
setVisibleResults(testResults);
} finally {
setIsRunning(false);
}
}, [pyodide, isRunning, visibleTestCases, runTests]);
const handleSubmit = useCallback(async () => {
if (!pyodide || isSubmitting) return;
setIsSubmitting(true);
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 &&
JSON.stringify(r.output) === JSON.stringify(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(slug, {
code,
hidden_outputs: hiddenOutputs,
});
setHiddenPassedCount(response.total_passed);
setHiddenTotalCount(response.total_tests);
} catch (error) {
console.error("Submission error:", error);
} finally {
setIsSubmitting(false);
}
}, [
pyodide,
isSubmitting,
visibleTestCases,
hiddenTestInputs,
runTests,
slug,
code,
]);
const isLoading = isRunning || isSubmitting;
return (
<div className="flex flex-col h-full">
<div className="flex items-center gap-2 px-3 py-2 bg-[var(--secondary)] border-b border-[var(--border)]">
<button
onClick={handleRunTests}
disabled={pyodideLoading || isLoading}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded bg-[var(--muted)] hover:bg-[var(--muted)]/80 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isRunning ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Play className="h-4 w-4" />
)}
Run Tests
</button>
<button
onClick={handleSubmit}
disabled={pyodideLoading || isLoading}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded bg-green-600 hover:bg-green-700 text-white disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isSubmitting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
Submit
</button>
{pyodideLoading && (
<span className="text-sm text-[var(--muted-foreground)] ml-2">
Loading Python runtime...
</span>
)}
</div>
<div className="flex-1 overflow-auto">
<TestResults
visibleResults={visibleResults}
hiddenPassedCount={hiddenPassedCount}
hiddenTotalCount={hiddenTotalCount}
isRunning={isLoading}
visibleTestCases={visibleTestCases}
/>
</div>
</div>
);
}