submit ux improvements
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback, useMemo } from "react";
|
import { useState, useCallback, useMemo, useEffect } from "react";
|
||||||
import { CodeEditor } from "./code-editor";
|
import { CodeEditor } from "./code-editor";
|
||||||
import { TestResults } from "./test-results";
|
import { TestResults } from "./test-results";
|
||||||
import { usePyodide } from "@/hooks/use-pyodide";
|
import { usePyodide } from "@/hooks/use-pyodide";
|
||||||
import { submitSolution } from "@/lib/api";
|
import { submitSolution } from "@/lib/api";
|
||||||
|
import { markQuestionCompleted, getSavedSolution, isQuestionCompleted } from "@/lib/progress";
|
||||||
import type { QuestionDetail, TestResult, HiddenTestOutput } from "@/types";
|
import type { QuestionDetail, TestResult, HiddenTestOutput } from "@/types";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Markdown } from "@/components/ui/markdown";
|
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 { Callout, ApproachBox } from "@/components/ui/callout";
|
||||||
import { Collapsible } from "@/components/ui/collapsible";
|
import { Collapsible } from "@/components/ui/collapsible";
|
||||||
import { getDifficultyVariant, capitalize } from "@/lib/utils";
|
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 {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
@@ -24,6 +38,13 @@ import {
|
|||||||
Play,
|
Play,
|
||||||
Send,
|
Send,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
PanelLeftClose,
|
||||||
|
PanelRightClose,
|
||||||
|
Columns2,
|
||||||
|
Gauge,
|
||||||
|
ChevronRight,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
@@ -38,19 +59,60 @@ interface ExecutionResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function extractFunctionName(signature: string): string {
|
function extractFunctionName(signature: string): string {
|
||||||
const match = signature.match(/def\s+(\w+)\s*\(/);
|
// Check for class-based signature: "class MinStack" -> "MinStack"
|
||||||
return match ? match[1] : "solution";
|
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 {
|
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}
|
return `${signature}
|
||||||
# Write your solution here
|
# Write your solution here
|
||||||
pass
|
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 RightTab = "code" | "results";
|
||||||
|
type ViewMode = "split" | "left" | "right";
|
||||||
|
|
||||||
export function ProblemWorkspace({ question }: ProblemWorkspaceProps) {
|
export function ProblemWorkspace({ question }: ProblemWorkspaceProps) {
|
||||||
const starterCode = useMemo(
|
const starterCode = useMemo(
|
||||||
@@ -60,6 +122,7 @@ export function ProblemWorkspace({ question }: ProblemWorkspaceProps) {
|
|||||||
const [code, setCode] = useState(starterCode);
|
const [code, setCode] = useState(starterCode);
|
||||||
const [leftTab, setLeftTab] = useState<LeftTab>("problem");
|
const [leftTab, setLeftTab] = useState<LeftTab>("problem");
|
||||||
const [rightTab, setRightTab] = useState<RightTab>("code");
|
const [rightTab, setRightTab] = useState<RightTab>("code");
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>("split");
|
||||||
const [hasRunTests, setHasRunTests] = useState(false);
|
const [hasRunTests, setHasRunTests] = useState(false);
|
||||||
|
|
||||||
// Test execution state
|
// Test execution state
|
||||||
@@ -69,6 +132,18 @@ export function ProblemWorkspace({ question }: ProblemWorkspaceProps) {
|
|||||||
const [visibleResults, setVisibleResults] = useState<TestResult[]>([]);
|
const [visibleResults, setVisibleResults] = useState<TestResult[]>([]);
|
||||||
const [hiddenPassedCount, setHiddenPassedCount] = useState(0);
|
const [hiddenPassedCount, setHiddenPassedCount] = useState(0);
|
||||||
const [hiddenTotalCount, setHiddenTotalCount] = 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(
|
const functionName = useMemo(
|
||||||
() => extractFunctionName(question.function_signature!),
|
() => extractFunctionName(question.function_signature!),
|
||||||
@@ -88,9 +163,127 @@ export function ProblemWorkspace({ question }: ProblemWorkspaceProps) {
|
|||||||
setCode(starterCode);
|
setCode(starterCode);
|
||||||
}, [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(
|
const generateTestHarness = useCallback(
|
||||||
(tests: Array<{ id: number; input: Record<string, unknown> }>) => {
|
(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);
|
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 `
|
return `
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -109,7 +302,7 @@ for test in __tests__:
|
|||||||
json.dumps(__results__)
|
json.dumps(__results__)
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
[code, functionName]
|
[code, functionName, signature]
|
||||||
);
|
);
|
||||||
|
|
||||||
const runTests = useCallback(
|
const runTests = useCallback(
|
||||||
@@ -138,12 +331,38 @@ json.dumps(__results__)
|
|||||||
[generateTestHarness, runPython]
|
[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 () => {
|
const handleRunTests = useCallback(async () => {
|
||||||
if (!pyodide || isRunning) return;
|
if (!pyodide || isRunning) return;
|
||||||
|
|
||||||
setIsRunning(true);
|
setIsRunning(true);
|
||||||
setHiddenPassedCount(0);
|
setHiddenPassedCount(0);
|
||||||
setHiddenTotalCount(0);
|
setHiddenTotalCount(0);
|
||||||
|
setComplexityResult(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tests = visibleTestCases.map((tc) => ({
|
const tests = visibleTestCases.map((tc) => ({
|
||||||
@@ -155,8 +374,7 @@ json.dumps(__results__)
|
|||||||
const testResults: TestResult[] = results.map((r, idx) => {
|
const testResults: TestResult[] = results.map((r, idx) => {
|
||||||
const testCase = visibleTestCases[idx];
|
const testCase = visibleTestCases[idx];
|
||||||
const passed =
|
const passed =
|
||||||
!r.error &&
|
!r.error && compareOutputs(r.output, testCase.expected);
|
||||||
JSON.stringify(r.output) === JSON.stringify(testCase.expected);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
test_id: r.test_id,
|
test_id: r.test_id,
|
||||||
@@ -170,16 +388,20 @@ json.dumps(__results__)
|
|||||||
|
|
||||||
setVisibleResults(testResults);
|
setVisibleResults(testResults);
|
||||||
setHasRunTests(true);
|
setHasRunTests(true);
|
||||||
setRightTab("results");
|
|
||||||
|
// Run complexity analysis in the background
|
||||||
|
const complexity = await analyzeComplexity(code);
|
||||||
|
setComplexityResult(complexity);
|
||||||
} finally {
|
} finally {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
}
|
}
|
||||||
}, [pyodide, isRunning, visibleTestCases, runTests]);
|
}, [pyodide, isRunning, visibleTestCases, runTests, analyzeComplexity, code]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(async () => {
|
const handleSubmit = useCallback(async () => {
|
||||||
if (!pyodide || isSubmitting) return;
|
if (!pyodide || isSubmitting) return;
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
setComplexityResult(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Run visible tests first
|
// Run visible tests first
|
||||||
@@ -193,8 +415,7 @@ json.dumps(__results__)
|
|||||||
(r, idx) => {
|
(r, idx) => {
|
||||||
const testCase = visibleTestCases[idx];
|
const testCase = visibleTestCases[idx];
|
||||||
const passed =
|
const passed =
|
||||||
!r.error &&
|
!r.error && compareOutputs(r.output, testCase.expected);
|
||||||
JSON.stringify(r.output) === JSON.stringify(testCase.expected);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
test_id: r.test_id,
|
test_id: r.test_id,
|
||||||
@@ -232,6 +453,20 @@ json.dumps(__results__)
|
|||||||
setHiddenPassedCount(response.total_passed);
|
setHiddenPassedCount(response.total_passed);
|
||||||
setHiddenTotalCount(response.total_tests);
|
setHiddenTotalCount(response.total_tests);
|
||||||
setHasRunTests(true);
|
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");
|
setRightTab("results");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Submission error:", error);
|
console.error("Submission error:", error);
|
||||||
@@ -246,6 +481,7 @@ json.dumps(__results__)
|
|||||||
runTests,
|
runTests,
|
||||||
question.slug,
|
question.slug,
|
||||||
code,
|
code,
|
||||||
|
analyzeComplexity,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isLoading = isRunning || isSubmitting;
|
const isLoading = isRunning || isSubmitting;
|
||||||
@@ -253,7 +489,12 @@ json.dumps(__results__)
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 top-[65px] flex bg-[var(--background)]">
|
<div className="fixed inset-0 top-[65px] flex bg-[var(--background)]">
|
||||||
{/* Left panel - Problem description / Explanation */}
|
{/* 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 */}
|
{/* Header - compact */}
|
||||||
<div className="px-4 pt-3 pb-0">
|
<div className="px-4 pt-3 pb-0">
|
||||||
<div className="flex items-center justify-between gap-2 mb-2">
|
<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">
|
<Badge variant={getDifficultyVariant(question.difficulty)} className="text-xs">
|
||||||
{capitalize(question.difficulty)}
|
{capitalize(question.difficulty)}
|
||||||
</Badge>
|
</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>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
{question.leetcode_url && (
|
{question.leetcode_url && (
|
||||||
<a
|
<a
|
||||||
href={question.leetcode_url}
|
href={question.leetcode_url}
|
||||||
@@ -279,6 +527,18 @@ json.dumps(__results__)
|
|||||||
LeetCode ↗
|
LeetCode ↗
|
||||||
</a>
|
</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>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-1.5 mb-2">
|
<div className="flex flex-wrap gap-1.5 mb-2">
|
||||||
@@ -320,12 +580,22 @@ json.dumps(__results__)
|
|||||||
>
|
>
|
||||||
Explanation
|
Explanation
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab content - scrollable */}
|
{/* Tab content - scrollable */}
|
||||||
<div className="flex-1 overflow-auto px-4 py-3">
|
<div className="flex-1 overflow-auto px-4 py-3">
|
||||||
{leftTab === "problem" ? (
|
{leftTab === "problem" && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Problem */}
|
{/* Problem */}
|
||||||
<div>
|
<div>
|
||||||
@@ -381,7 +651,9 @@ json.dumps(__results__)
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)}
|
||||||
|
|
||||||
|
{leftTab === "explanation" && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Intuition */}
|
{/* Intuition */}
|
||||||
{question.explanation?.intuition && (
|
{question.explanation?.intuition && (
|
||||||
@@ -474,14 +746,12 @@ json.dumps(__results__)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Solutions */}
|
{leftTab === "solutions" && (
|
||||||
{question.solutions && question.solutions.length > 0 && (
|
<div className="space-y-6">
|
||||||
<div>
|
{question.solutions && question.solutions.length > 0 ? (
|
||||||
<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">
|
<div className="space-y-4">
|
||||||
{question.solutions.map((solution, i) => {
|
{question.solutions.map((solution, i) => {
|
||||||
const content = (
|
const content = (
|
||||||
@@ -515,17 +785,36 @@ json.dumps(__results__)
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<p className="text-[var(--muted-foreground)]">
|
||||||
|
No solutions available for this problem yet.
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Right panel - Editor and test results */}
|
{/* 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 */}
|
{/* Tabs header */}
|
||||||
<div className="px-4 pt-2 border-b border-[var(--border)] flex items-center justify-between">
|
<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">
|
<div className="flex gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setRightTab("code")}
|
onClick={() => setRightTab("code")}
|
||||||
@@ -538,7 +827,14 @@ json.dumps(__results__)
|
|||||||
<Code className="h-4 w-4" />
|
<Code className="h-4 w-4" />
|
||||||
Code
|
Code
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={() => setRightTab("results")}
|
onClick={() => setRightTab("results")}
|
||||||
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
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)]"
|
: "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
|
Results
|
||||||
</button>
|
</button>
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
@@ -564,11 +866,130 @@ json.dumps(__results__)
|
|||||||
<div className="flex-1 min-h-0 flex flex-col">
|
<div className="flex-1 min-h-0 flex flex-col">
|
||||||
{rightTab === "code" ? (
|
{rightTab === "code" ? (
|
||||||
<>
|
<>
|
||||||
{/* Editor - full height */}
|
{/* Editor */}
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
<CodeEditor value={code} onChange={setCode} />
|
<CodeEditor value={code} onChange={setCode} />
|
||||||
</div>
|
</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 */}
|
{/* Action buttons */}
|
||||||
<div className="p-3 border-t border-[var(--border)] flex items-center justify-between bg-[var(--secondary)]">
|
<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)]">
|
<div className="text-sm text-[var(--muted-foreground)]">
|
||||||
@@ -611,17 +1032,59 @@ json.dumps(__results__)
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
/* Results tab */
|
/* Results tab */
|
||||||
<div className="flex-1 overflow-auto p-4">
|
<div className="flex-1 overflow-auto p-4 flex flex-col">
|
||||||
<TestResults
|
<TestResults
|
||||||
visibleResults={visibleResults}
|
visibleResults={visibleResults}
|
||||||
hiddenPassedCount={hiddenPassedCount}
|
hiddenPassedCount={hiddenPassedCount}
|
||||||
hiddenTotalCount={hiddenTotalCount}
|
hiddenTotalCount={hiddenTotalCount}
|
||||||
visibleTestCases={visibleTestCases}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user