submit ux improvements
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useState, useCallback, useMemo, useEffect } from "react";
|
||||
import { CodeEditor } from "./code-editor";
|
||||
import { TestResults } from "./test-results";
|
||||
import { usePyodide } from "@/hooks/use-pyodide";
|
||||
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";
|
||||
@@ -12,6 +13,19 @@ 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,
|
||||
@@ -24,6 +38,13 @@ import {
|
||||
Play,
|
||||
Send,
|
||||
Loader2,
|
||||
PanelLeftClose,
|
||||
PanelRightClose,
|
||||
Columns2,
|
||||
Gauge,
|
||||
ChevronRight,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -38,19 +59,60 @@ interface ExecutionResult {
|
||||
}
|
||||
|
||||
function extractFunctionName(signature: string): string {
|
||||
const match = signature.match(/def\s+(\w+)\s*\(/);
|
||||
return match ? match[1] : "solution";
|
||||
// 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
|
||||
`;
|
||||
}
|
||||
|
||||
type LeftTab = "problem" | "explanation";
|
||||
/**
|
||||
* 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) {
|
||||
const starterCode = useMemo(
|
||||
@@ -60,6 +122,7 @@ export function ProblemWorkspace({ question }: ProblemWorkspaceProps) {
|
||||
const [code, setCode] = useState(starterCode);
|
||||
const [leftTab, setLeftTab] = useState<LeftTab>("problem");
|
||||
const [rightTab, setRightTab] = useState<RightTab>("code");
|
||||
const [viewMode, setViewMode] = useState<ViewMode>("split");
|
||||
const [hasRunTests, setHasRunTests] = useState(false);
|
||||
|
||||
// Test execution state
|
||||
@@ -69,6 +132,18 @@ export function ProblemWorkspace({ question }: ProblemWorkspaceProps) {
|
||||
const [visibleResults, setVisibleResults] = useState<TestResult[]>([]);
|
||||
const [hiddenPassedCount, setHiddenPassedCount] = useState(0);
|
||||
const [hiddenTotalCount, setHiddenTotalCount] = useState(0);
|
||||
const [complexityResult, setComplexityResult] = useState<ComplexityResult | null>(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!),
|
||||
@@ -88,9 +163,127 @@ export function ProblemWorkspace({ question }: ProblemWorkspaceProps) {
|
||||
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<string, unknown> }>) => {
|
||||
// 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
|
||||
|
||||
@@ -109,7 +302,7 @@ for test in __tests__:
|
||||
json.dumps(__results__)
|
||||
`;
|
||||
},
|
||||
[code, functionName]
|
||||
[code, functionName, signature]
|
||||
);
|
||||
|
||||
const runTests = useCallback(
|
||||
@@ -138,12 +331,38 @@ json.dumps(__results__)
|
||||
[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) => ({
|
||||
@@ -155,8 +374,7 @@ json.dumps(__results__)
|
||||
const testResults: TestResult[] = results.map((r, idx) => {
|
||||
const testCase = visibleTestCases[idx];
|
||||
const passed =
|
||||
!r.error &&
|
||||
JSON.stringify(r.output) === JSON.stringify(testCase.expected);
|
||||
!r.error && compareOutputs(r.output, testCase.expected);
|
||||
|
||||
return {
|
||||
test_id: r.test_id,
|
||||
@@ -170,16 +388,20 @@ json.dumps(__results__)
|
||||
|
||||
setVisibleResults(testResults);
|
||||
setHasRunTests(true);
|
||||
setRightTab("results");
|
||||
|
||||
// Run complexity analysis in the background
|
||||
const complexity = await analyzeComplexity(code);
|
||||
setComplexityResult(complexity);
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
}, [pyodide, isRunning, visibleTestCases, runTests]);
|
||||
}, [pyodide, isRunning, visibleTestCases, runTests, analyzeComplexity, code]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (!pyodide || isSubmitting) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
setComplexityResult(null);
|
||||
|
||||
try {
|
||||
// Run visible tests first
|
||||
@@ -193,8 +415,7 @@ json.dumps(__results__)
|
||||
(r, idx) => {
|
||||
const testCase = visibleTestCases[idx];
|
||||
const passed =
|
||||
!r.error &&
|
||||
JSON.stringify(r.output) === JSON.stringify(testCase.expected);
|
||||
!r.error && compareOutputs(r.output, testCase.expected);
|
||||
|
||||
return {
|
||||
test_id: r.test_id,
|
||||
@@ -232,6 +453,20 @@ json.dumps(__results__)
|
||||
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) {
|
||||
markQuestionCompleted(question.slug, code);
|
||||
setIsCompleted(true);
|
||||
}
|
||||
|
||||
// Auto-switch to Results tab after submit
|
||||
setRightTab("results");
|
||||
} catch (error) {
|
||||
console.error("Submission error:", error);
|
||||
@@ -246,6 +481,7 @@ json.dumps(__results__)
|
||||
runTests,
|
||||
question.slug,
|
||||
code,
|
||||
analyzeComplexity,
|
||||
]);
|
||||
|
||||
const isLoading = isRunning || isSubmitting;
|
||||
@@ -253,7 +489,12 @@ json.dumps(__results__)
|
||||
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">
|
||||
{viewMode !== "right" && (
|
||||
<div
|
||||
className={`${
|
||||
viewMode === "left" ? "w-full" : "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">
|
||||
@@ -268,7 +509,14 @@ json.dumps(__results__)
|
||||
<Badge variant={getDifficultyVariant(question.difficulty)} className="text-xs">
|
||||
{capitalize(question.difficulty)}
|
||||
</Badge>
|
||||
{isCompleted && (
|
||||
<span className="flex items-center gap-1 text-xs text-green-500">
|
||||
<CheckCircle className="h-3.5 w-3.5" />
|
||||
Completed
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{question.leetcode_url && (
|
||||
<a
|
||||
href={question.leetcode_url}
|
||||
@@ -279,6 +527,18 @@ json.dumps(__results__)
|
||||
LeetCode ↗
|
||||
</a>
|
||||
)}
|
||||
<button
|
||||
onClick={() => toggleViewMode("left")}
|
||||
className="p-1.5 rounded hover:bg-[var(--accent)] text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
|
||||
title={viewMode === "left" ? "Split view" : "Maximize problem"}
|
||||
>
|
||||
{viewMode === "left" ? (
|
||||
<Columns2 className="h-4 w-4" />
|
||||
) : (
|
||||
<PanelRightClose className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5 mb-2">
|
||||
@@ -320,12 +580,22 @@ json.dumps(__results__)
|
||||
>
|
||||
Explanation
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLeftTab("solutions")}
|
||||
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
||||
leftTab === "solutions"
|
||||
? "border-[var(--primary)] text-[var(--foreground)]"
|
||||
: "border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
|
||||
}`}
|
||||
>
|
||||
Solutions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab content - scrollable */}
|
||||
<div className="flex-1 overflow-auto px-4 py-3">
|
||||
{leftTab === "problem" ? (
|
||||
{leftTab === "problem" && (
|
||||
<div className="space-y-6">
|
||||
{/* Problem */}
|
||||
<div>
|
||||
@@ -381,7 +651,9 @@ json.dumps(__results__)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{leftTab === "explanation" && (
|
||||
<div className="space-y-6">
|
||||
{/* Intuition */}
|
||||
{question.explanation?.intuition && (
|
||||
@@ -474,14 +746,12 @@ json.dumps(__results__)
|
||||
</div>
|
||||
</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>
|
||||
{leftTab === "solutions" && (
|
||||
<div className="space-y-6">
|
||||
{question.solutions && question.solutions.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{question.solutions.map((solution, i) => {
|
||||
const content = (
|
||||
@@ -515,17 +785,36 @@ json.dumps(__results__)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[var(--muted-foreground)]">
|
||||
No solutions available for this problem yet.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right panel - Editor and test results */}
|
||||
<div className="w-1/2 flex flex-col">
|
||||
{viewMode !== "left" && (
|
||||
<div
|
||||
className={`${viewMode === "right" ? "w-full" : "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 items-center gap-2">
|
||||
<button
|
||||
onClick={() => toggleViewMode("right")}
|
||||
className="p-1.5 rounded hover:bg-[var(--accent)] text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
|
||||
title={viewMode === "right" ? "Split view" : "Maximize editor"}
|
||||
>
|
||||
{viewMode === "right" ? (
|
||||
<Columns2 className="h-4 w-4" />
|
||||
) : (
|
||||
<PanelLeftClose className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => setRightTab("code")}
|
||||
@@ -538,7 +827,14 @@ json.dumps(__results__)
|
||||
<Code className="h-4 w-4" />
|
||||
Code
|
||||
</button>
|
||||
{hasRunTests && (
|
||||
{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 (
|
||||
<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 ${
|
||||
@@ -547,10 +843,16 @@ json.dumps(__results__)
|
||||
: "border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
|
||||
}`}
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
{allPassed ? (
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
Results
|
||||
</button>
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
@@ -564,11 +866,130 @@ json.dumps(__results__)
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
{rightTab === "code" ? (
|
||||
<>
|
||||
{/* Editor - full height */}
|
||||
{/* Editor */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<CodeEditor value={code} onChange={setCode} />
|
||||
</div>
|
||||
|
||||
{/* Complexity Analysis - shown after running tests */}
|
||||
{complexityResult?.success && (
|
||||
<div className="border-t border-[var(--border)] bg-[var(--secondary)]/50 px-4 py-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Gauge className="h-4 w-4 text-[var(--primary)]" />
|
||||
<span className="font-medium">
|
||||
Estimated: {complexityResult.complexity}
|
||||
</span>
|
||||
{(complexityResult.details?.findings?.length ||
|
||||
complexityResult.details?.recursive_calls?.length) && (
|
||||
<button
|
||||
onClick={() => setIsComplexityExpanded(!isComplexityExpanded)}
|
||||
className="flex items-center gap-1 text-xs text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors ml-2"
|
||||
>
|
||||
<ChevronRight
|
||||
className={`h-3 w-3 transition-transform ${
|
||||
isComplexityExpanded ? "rotate-90" : ""
|
||||
}`}
|
||||
/>
|
||||
Details
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isComplexityExpanded && complexityResult.details && (
|
||||
<div className="mt-2 space-y-2 text-xs">
|
||||
{/* Loop findings */}
|
||||
{complexityResult.details.findings
|
||||
?.filter((f) => f.type === "loop")
|
||||
.map((finding, i) => (
|
||||
<div key={`loop-${i}`} className="space-y-0.5">
|
||||
<div className="text-[var(--muted-foreground)]">
|
||||
{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})`
|
||||
: ""}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-3 flex items-center gap-2">
|
||||
<span className="text-[var(--muted-foreground)]/60 text-[10px]">
|
||||
L{finding.line}
|
||||
</span>
|
||||
<code className="bg-[var(--secondary)] px-1 rounded text-[var(--foreground)]/80">
|
||||
{finding.snippet}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Sort operations */}
|
||||
{complexityResult.details.findings
|
||||
?.filter((f) => f.kind === "sort")
|
||||
.map((finding, i) => (
|
||||
<div key={`sort-${i}`} className="space-y-0.5">
|
||||
<div className="text-[var(--muted-foreground)]">
|
||||
Sorting operation → O(n log n)
|
||||
</div>
|
||||
<div className="ml-3 flex items-center gap-2">
|
||||
<span className="text-[var(--muted-foreground)]/60 text-[10px]">
|
||||
L{finding.line}
|
||||
</span>
|
||||
<code className="bg-[var(--secondary)] px-1 rounded text-[var(--foreground)]/80">
|
||||
{finding.snippet}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Linear search warnings */}
|
||||
{complexityResult.details.findings
|
||||
?.filter((f) => f.kind === "linear_search")
|
||||
.map((finding, i) => (
|
||||
<div key={`search-${i}`} className="space-y-0.5">
|
||||
<div className="text-yellow-500">
|
||||
{finding.note || "Linear search in loop"}
|
||||
</div>
|
||||
<div className="ml-3 flex items-center gap-2">
|
||||
<span className="text-[var(--muted-foreground)]/60 text-[10px]">
|
||||
L{finding.line}
|
||||
</span>
|
||||
<code className="bg-[var(--secondary)] px-1 rounded text-[var(--foreground)]/80">
|
||||
{finding.snippet}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Recursive calls */}
|
||||
{complexityResult.details.recursive_calls?.map((call, i) => (
|
||||
<div key={`rec-${i}`} className="space-y-0.5">
|
||||
<div className="text-[var(--muted-foreground)]">
|
||||
Recursive call
|
||||
</div>
|
||||
<div className="ml-3 flex items-center gap-2">
|
||||
<span className="text-[var(--muted-foreground)]/60 text-[10px]">
|
||||
L{call.line}
|
||||
</span>
|
||||
<code className="bg-[var(--secondary)] px-1 rounded text-[var(--foreground)]/80">
|
||||
{call.snippet}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</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)]">
|
||||
@@ -611,17 +1032,59 @@ json.dumps(__results__)
|
||||
</>
|
||||
) : (
|
||||
/* Results tab */
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<div className="flex-1 overflow-auto p-4 flex flex-col">
|
||||
<TestResults
|
||||
visibleResults={visibleResults}
|
||||
hiddenPassedCount={hiddenPassedCount}
|
||||
hiddenTotalCount={hiddenTotalCount}
|
||||
visibleTestCases={visibleTestCases}
|
||||
/>
|
||||
|
||||
{/* Complexity Analysis in Results tab */}
|
||||
{complexityResult?.success && (
|
||||
<div className="mt-4 p-3 rounded bg-[var(--secondary)]/50 border border-[var(--border)]">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Gauge className="h-4 w-4 text-[var(--primary)]" />
|
||||
<span className="font-medium">
|
||||
Estimated Complexity: {complexityResult.complexity}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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 (
|
||||
<div className="mt-6 p-4 rounded-lg bg-green-500/10 border border-green-500/30">
|
||||
<div className="flex items-center gap-2 text-green-500 font-semibold mb-3">
|
||||
<CheckCircle className="h-5 w-5" />
|
||||
All tests passed!
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/questions"
|
||||
className="px-4 py-2 text-sm font-medium rounded bg-[var(--secondary)] border border-[var(--border)] hover:bg-[var(--accent)] transition-colors"
|
||||
>
|
||||
Back to Questions
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user