From 3c1535ed890e83153b5d5953951577c4677c8bdc Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Wed, 10 Sep 2025 18:46:50 +0100 Subject: [PATCH] feat(content): pattern comparisons --- .../versions/007_pattern_comparison.py | 25 +++++++++++++++++ backend/scripts/load_data.py | 2 ++ .../components/editor/problem-workspace.tsx | 27 ++++++++++++++++++- .../components/questions/question-detail.tsx | 9 +++++++ frontend/src/components/ui/callout.tsx | 9 ++++++- 5 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 backend/alembic/versions/007_pattern_comparison.py diff --git a/backend/alembic/versions/007_pattern_comparison.py b/backend/alembic/versions/007_pattern_comparison.py new file mode 100644 index 0000000..f4db3c4 --- /dev/null +++ b/backend/alembic/versions/007_pattern_comparison.py @@ -0,0 +1,25 @@ +"""add pattern_comparison to explanations + +Revision ID: 007 +Revises: 006 +Create Date: 2025-07-05 + +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +revision: str = "007" +down_revision: str | None = "006" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column("explanations", sa.Column("pattern_comparison", sa.Text(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("explanations", "pattern_comparison") diff --git a/backend/scripts/load_data.py b/backend/scripts/load_data.py index 62b81ac..59e5ebe 100644 --- a/backend/scripts/load_data.py +++ b/backend/scripts/load_data.py @@ -236,6 +236,7 @@ async def load_question( explanation.time_complexity = exp_data["time_complexity"] explanation.space_complexity = exp_data["space_complexity"] explanation.complexity_explanation = exp_data.get("complexity_explanation") + explanation.pattern_comparison = exp_data.get("pattern_comparison") else: explanation = Explanation( question_id=question.id, @@ -246,6 +247,7 @@ async def load_question( time_complexity=exp_data["time_complexity"], space_complexity=exp_data["space_complexity"], complexity_explanation=exp_data.get("complexity_explanation"), + pattern_comparison=exp_data.get("pattern_comparison"), ) session.add(explanation) diff --git a/frontend/src/components/editor/problem-workspace.tsx b/frontend/src/components/editor/problem-workspace.tsx index 6a6078b..17d08dd 100644 --- a/frontend/src/components/editor/problem-workspace.tsx +++ b/frontend/src/components/editor/problem-workspace.tsx @@ -4,6 +4,7 @@ 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"; @@ -45,6 +46,7 @@ import { ChevronRight, CheckCircle, XCircle, + GitCompare, } from "lucide-react"; import Link from "next/link"; @@ -115,6 +117,9 @@ 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] @@ -462,7 +467,14 @@ json.dumps(__result) const allVisiblePassed = visibleTestResults.every((r) => r.passed); const allHiddenPassed = response.total_passed === response.total_tests; if (allVisiblePassed && allHiddenPassed) { - markQuestionCompleted(question.slug, code); + const timeSpentMs = getTimeTrackerElapsed(); + const primaryPattern = question.patterns[0]?.slug || ""; + markQuestionCompleted(question.slug, { + primaryPattern, + difficulty: question.difficulty, + code, + timeSpentMs, + }); setIsCompleted(true); } @@ -480,6 +492,8 @@ json.dumps(__result) hiddenTestInputs, runTests, question.slug, + question.difficulty, + question.patterns, code, analyzeComplexity, ]); @@ -719,6 +733,17 @@ json.dumps(__result) )} + {/* Why This Pattern? */} + {question.explanation?.pattern_comparison && ( + + {question.explanation.pattern_comparison} + + )} + {/* Complexity Analysis */} {(question.explanation?.time_complexity || question.explanation?.space_complexity) && ( diff --git a/frontend/src/components/questions/question-detail.tsx b/frontend/src/components/questions/question-detail.tsx index d0cdefa..35a8cc7 100644 --- a/frontend/src/components/questions/question-detail.tsx +++ b/frontend/src/components/questions/question-detail.tsx @@ -12,6 +12,7 @@ import { Code, Clock, HardDrive, + GitCompare, } from "lucide-react"; import Link from "next/link"; import type { QuestionDetail as QuestionDetailType } from "@/types"; @@ -216,6 +217,14 @@ export function QuestionDetail({ question }: QuestionDetailProps) { )} + + {question.explanation.pattern_comparison && ( + +
+ {question.explanation.pattern_comparison} +
+
+ )} )} diff --git a/frontend/src/components/ui/callout.tsx b/frontend/src/components/ui/callout.tsx index c8c5107..490bfbe 100644 --- a/frontend/src/components/ui/callout.tsx +++ b/frontend/src/components/ui/callout.tsx @@ -4,10 +4,11 @@ import { Lightbulb, ClipboardList, Brain, + GitCompare, type LucideIcon, } from "lucide-react"; -type CalloutVariant = "warning" | "success" | "info" | "insight"; +type CalloutVariant = "warning" | "success" | "info" | "insight" | "pattern"; interface CalloutProps { children: React.ReactNode; @@ -45,6 +46,12 @@ const variantConfig: Record< bgClass: "bg-[var(--callout-insight-bg)]", iconClass: "text-[var(--callout-insight-fg)]", }, + pattern: { + icon: GitCompare, + borderClass: "border-purple-500/50", + bgClass: "bg-purple-500/10", + iconClass: "text-purple-500", + }, }; export function Callout({