feat(viz): interactive algorithm viz system

This commit is contained in:
2025-08-23 20:28:22 +01:00
parent f6d4bc3a03
commit e5ebe7b188
17 changed files with 1321 additions and 1 deletions

View File

@@ -61,6 +61,15 @@
/* Badge colors - Optimal (green) */
--badge-optimal: #16a34a;
--badge-optimal-bg: #dcfce7;
/* Visualization colors */
--viz-default: #f3f4f6;
--viz-active: #3b82f6;
--viz-comparing: #f59e0b;
--viz-found: #22c55e;
--viz-visited: #9ca3af;
--viz-swapping: #8b5cf6;
--viz-transition: 300ms;
}
@media (prefers-color-scheme: dark) {
@@ -125,6 +134,14 @@
/* Badge colors - Optimal (green, dark mode) */
--badge-optimal: #4ade80;
--badge-optimal-bg: rgba(34, 197, 94, 0.2);
/* Visualization colors (dark mode) */
--viz-default: #374151;
--viz-active: #3b82f6;
--viz-comparing: #f59e0b;
--viz-found: #22c55e;
--viz-visited: #6b7280;
--viz-swapping: #8b5cf6;
}
}
@@ -139,3 +156,26 @@ body {
.prose-content {
line-height: 1.7;
}
/* Visualization element transitions */
.viz-element {
transition:
background-color var(--viz-transition) ease,
transform var(--viz-transition) ease,
opacity var(--viz-transition) ease;
}
.viz-cell-bg {
transition:
fill var(--viz-transition) ease;
}
.viz-value {
transition:
fill var(--viz-transition) ease;
}
.viz-pointer {
transition:
transform var(--viz-transition) ease;
}

View File

@@ -13,6 +13,7 @@ import {
RecognitionSignals,
RelatedPatterns,
} from "@/components/patterns";
import { PatternVisualization } from "@/components/visualization";
interface PageProps {
params: Promise<{ slug: string }>;
@@ -104,7 +105,19 @@ export default async function PatternDetailPage({ params }: PageProps) {
</Card>
)}
{/* Visualization - ASCII diagram walkthrough */}
{/* Interactive Visualization */}
{pattern.visualization_examples && pattern.visualization_examples.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Interactive Visualization</CardTitle>
</CardHeader>
<CardContent>
<PatternVisualization examples={pattern.visualization_examples} />
</CardContent>
</Card>
)}
{/* Static Visualization - ASCII diagram walkthrough (fallback) */}
{pattern.visualization && (
<Card>
<CardHeader>

View File

@@ -0,0 +1,11 @@
export { VisualizationContainer } from "./visualization-container";
export {
VisualizationProvider,
useVisualization,
} from "./visualization-context";
export { StepController } from "./step-controller";
export { StepDescription } from "./step-description";
export { VariablesPane } from "./variables-pane";
export { StructureRenderer } from "./structure-renderer";
export { PatternVisualization } from "./pattern-visualization";
export { ArrayVisualizer } from "./visualizers";

View File

@@ -0,0 +1,67 @@
"use client";
import { useState } from "react";
import { VisualizationContainer } from "./visualization-container";
import type { VisualizationExample } from "@/types";
import type { VisualizationData, VisualizationStep } from "@/types/visualization";
interface PatternVisualizationProps {
examples: VisualizationExample[];
}
function transformToVisualizationData(
example: VisualizationExample
): VisualizationData {
const steps: VisualizationStep[] = example.steps.map((step) => ({
id: step.id,
description: step.description,
structures: step.structures as VisualizationStep["structures"],
variables: step.variables,
codeHighlight: step.codeHighlight,
}));
return {
id: example.id,
title: example.title,
code: example.code,
steps,
totalSteps: steps.length,
};
}
export function PatternVisualization({ examples }: PatternVisualizationProps) {
const [selectedIndex, setSelectedIndex] = useState(0);
if (!examples || examples.length === 0) {
return null;
}
const selectedExample = examples[selectedIndex];
const visualizationData = transformToVisualizationData(selectedExample);
return (
<div className="space-y-4">
{/* Example selector (if multiple) */}
{examples.length > 1 && (
<div className="flex flex-wrap gap-2">
{examples.map((example, index) => (
<button
key={example.id}
onClick={() => setSelectedIndex(index)}
className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
index === selectedIndex
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-muted/80"
}`}
>
{example.title}
</button>
))}
</div>
)}
{/* Visualization */}
<VisualizationContainer data={visualizationData} />
</div>
);
}

View File

@@ -0,0 +1,174 @@
"use client";
import { useVisualization } from "./visualization-context";
import type { SpeedPreset } from "@/types/visualization";
const speedLabels: Record<SpeedPreset, string> = {
slow: "0.5x",
normal: "1x",
fast: "2x",
};
const speedOrder: SpeedPreset[] = ["slow", "normal", "fast"];
export function StepController() {
const {
stepIndex,
totalSteps,
isPlaying,
speed,
nextStep,
prevStep,
togglePlayback,
setSpeed,
canGoNext,
canGoPrev,
} = useVisualization();
const cycleSpeed = () => {
const currentIndex = speedOrder.indexOf(speed);
const nextIndex = (currentIndex + 1) % speedOrder.length;
setSpeed(speedOrder[nextIndex]);
};
return (
<div
className="flex items-center justify-between gap-4 rounded-lg border border-border bg-card p-3"
role="toolbar"
aria-label="Visualization controls"
>
{/* Step counter */}
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="font-medium">
Step {stepIndex + 1} of {totalSteps}
</span>
</div>
{/* Navigation controls */}
<div className="flex items-center gap-2">
{/* Previous */}
<button
onClick={prevStep}
disabled={!canGoPrev}
className="flex h-9 w-9 items-center justify-center rounded-md border border-border bg-background transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
aria-label="Previous step"
title="Previous step (←)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="m15 18-6-6 6-6" />
</svg>
</button>
{/* Play/Pause */}
<button
onClick={togglePlayback}
className="flex h-9 w-9 items-center justify-center rounded-md border border-border bg-primary text-primary-foreground transition-colors hover:bg-primary/90"
aria-label={isPlaying ? "Pause" : "Play"}
title={isPlaying ? "Pause (Space)" : "Play (Space)"}
>
{isPlaying ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<rect x="6" y="4" width="4" height="16" />
<rect x="14" y="4" width="4" height="16" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
)}
</button>
{/* Next */}
<button
onClick={nextStep}
disabled={!canGoNext}
className="flex h-9 w-9 items-center justify-center rounded-md border border-border bg-background transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
aria-label="Next step"
title="Next step (→)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="m9 18 6-6-6-6" />
</svg>
</button>
</div>
{/* Speed control */}
<button
onClick={cycleSpeed}
className="flex h-8 min-w-[48px] items-center justify-center rounded-md border border-border bg-background px-2 text-sm font-medium transition-colors hover:bg-muted"
aria-label={`Playback speed: ${speedLabels[speed]}`}
title="Cycle playback speed"
>
{speedLabels[speed]}
</button>
{/* Progress bar */}
<div
className="hidden flex-1 sm:block"
role="progressbar"
aria-valuenow={stepIndex + 1}
aria-valuemin={1}
aria-valuemax={totalSteps}
aria-label={`Step ${stepIndex + 1} of ${totalSteps}`}
>
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full bg-primary transition-all duration-300"
style={{ width: `${((stepIndex + 1) / totalSteps) * 100}%` }}
/>
</div>
</div>
{/* Keyboard hints */}
<div className="hidden items-center gap-1 text-xs text-muted-foreground lg:flex">
<kbd className="rounded border border-border bg-muted px-1.5 py-0.5">
</kbd>
<kbd className="rounded border border-border bg-muted px-1.5 py-0.5">
</kbd>
<span className="mx-1">step</span>
<kbd className="rounded border border-border bg-muted px-1.5 py-0.5">
Space
</kbd>
<span>play</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,21 @@
"use client";
import { useVisualization } from "./visualization-context";
export function StepDescription() {
const { currentStep } = useVisualization();
if (!currentStep) {
return null;
}
return (
<div
className="rounded-lg border border-border bg-muted/50 p-4"
role="status"
aria-live="polite"
>
<p className="text-sm leading-relaxed">{currentStep.description}</p>
</div>
);
}

View File

@@ -0,0 +1,88 @@
"use client";
import { useVisualization } from "./visualization-context";
import { ArrayVisualizer } from "./visualizers/array-visualizer";
import type { DataStructureState } from "@/types/visualization";
interface StructureVisualizerProps {
name: string;
data: DataStructureState;
}
function StructureVisualizer({ name, data }: StructureVisualizerProps) {
switch (data.type) {
case "array":
return <ArrayVisualizer data={data} name={name} />;
case "linkedlist":
// Phase 3: LinkedListVisualizer
return (
<div className="text-muted-foreground">
LinkedList visualization coming soon
</div>
);
case "stack":
// Phase 3: StackVisualizer
return (
<div className="text-muted-foreground">
Stack visualization coming soon
</div>
);
case "tree":
// Phase 3: TreeVisualizer
return (
<div className="text-muted-foreground">
Tree visualization coming soon
</div>
);
case "graph":
// Phase 5: GraphVisualizer
return (
<div className="text-muted-foreground">
Graph visualization coming soon
</div>
);
case "matrix":
// Phase 5: MatrixVisualizer
return (
<div className="text-muted-foreground">
Matrix visualization coming soon
</div>
);
default:
return (
<div className="text-muted-foreground">
Unknown data structure type
</div>
);
}
}
export function StructureRenderer() {
const { currentStep } = useVisualization();
if (!currentStep?.structures) {
return (
<div className="text-center text-muted-foreground">
No data structures to display
</div>
);
}
const structureEntries = Object.entries(currentStep.structures);
if (structureEntries.length === 0) {
return (
<div className="text-center text-muted-foreground">
No data structures to display
</div>
);
}
return (
<div className="flex flex-col gap-6">
{structureEntries.map(([name, data]) => (
<StructureVisualizer key={name} name={name} data={data} />
))}
</div>
);
}

View File

@@ -0,0 +1,43 @@
"use client";
import { useVisualization } from "./visualization-context";
function formatValue(value: unknown): string {
if (value === null) return "null";
if (value === undefined) return "undefined";
if (typeof value === "string") return `"${value}"`;
if (typeof value === "boolean") return value ? "true" : "false";
if (Array.isArray(value)) return `[${value.map(formatValue).join(", ")}]`;
if (typeof value === "object") return JSON.stringify(value);
return String(value);
}
export function VariablesPane() {
const { currentStep } = useVisualization();
if (!currentStep?.variables || Object.keys(currentStep.variables).length === 0) {
return null;
}
return (
<div className="rounded-lg border border-border bg-card p-3">
<h4 className="mb-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
Variables
</h4>
<div className="flex flex-wrap gap-3">
{Object.entries(currentStep.variables).map(([name, value]) => (
<div
key={name}
className="flex items-center gap-2 rounded-md bg-muted px-2 py-1"
>
<span className="font-mono text-sm font-medium text-primary">
{name}
</span>
<span className="text-muted-foreground">=</span>
<span className="font-mono text-sm">{formatValue(value)}</span>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,51 @@
"use client";
import { VisualizationProvider } from "./visualization-context";
import { StepController } from "./step-controller";
import { StepDescription } from "./step-description";
import { VariablesPane } from "./variables-pane";
import { StructureRenderer } from "./structure-renderer";
import type { VisualizationData } from "@/types/visualization";
interface VisualizationContainerProps {
data: VisualizationData;
className?: string;
}
export function VisualizationContainer({
data,
className = "",
}: VisualizationContainerProps) {
if (!data.steps || data.steps.length === 0) {
return (
<div className="rounded-lg border border-border bg-muted/50 p-8 text-center text-muted-foreground">
No visualization steps available
</div>
);
}
return (
<VisualizationProvider data={data}>
<div className={`flex flex-col gap-4 ${className}`}>
{/* Title */}
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">{data.title}</h3>
</div>
{/* Step description */}
<StepDescription />
{/* Data structure visualization */}
<div className="rounded-lg border border-border bg-card p-4">
<StructureRenderer />
</div>
{/* Variables */}
<VariablesPane />
{/* Controls */}
<StepController />
</div>
</VisualizationProvider>
);
}

View File

@@ -0,0 +1,222 @@
"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
} from "react";
import type {
SpeedPreset,
VisualizationData,
VisualizationStep,
} from "@/types/visualization";
const speedValues: Record<SpeedPreset, number> = {
slow: 2000,
normal: 1000,
fast: 500,
};
interface VisualizationContextValue {
// Data
data: VisualizationData | null;
currentStep: VisualizationStep | null;
stepIndex: number;
totalSteps: number;
// Playback state
isPlaying: boolean;
speed: SpeedPreset;
// Navigation actions
goToStep: (index: number) => void;
nextStep: () => void;
prevStep: () => void;
play: () => void;
pause: () => void;
togglePlayback: () => void;
setSpeed: (speed: SpeedPreset) => void;
// State queries
canGoNext: boolean;
canGoPrev: boolean;
}
const VisualizationContext = createContext<VisualizationContextValue | null>(
null
);
interface VisualizationProviderProps {
data: VisualizationData;
initialStep?: number;
autoPlay?: boolean;
children: ReactNode;
}
export function VisualizationProvider({
data,
initialStep = 0,
autoPlay = false,
children,
}: VisualizationProviderProps) {
const [stepIndex, setStepIndex] = useState(initialStep);
const [isPlaying, setIsPlaying] = useState(autoPlay);
const [speed, setSpeed] = useState<SpeedPreset>("normal");
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const totalSteps = data.steps.length;
const currentStep = data.steps[stepIndex] ?? null;
const canGoNext = stepIndex < totalSteps - 1;
const canGoPrev = stepIndex > 0;
const goToStep = useCallback(
(index: number) => {
const clampedIndex = Math.max(0, Math.min(index, totalSteps - 1));
setStepIndex(clampedIndex);
},
[totalSteps]
);
const nextStep = useCallback(() => {
if (canGoNext) {
setStepIndex((prev) => prev + 1);
} else {
setIsPlaying(false);
}
}, [canGoNext]);
const prevStep = useCallback(() => {
if (canGoPrev) {
setStepIndex((prev) => prev - 1);
}
}, [canGoPrev]);
const play = useCallback(() => {
if (stepIndex >= totalSteps - 1) {
setStepIndex(0);
}
setIsPlaying(true);
}, [stepIndex, totalSteps]);
const pause = useCallback(() => {
setIsPlaying(false);
}, []);
const togglePlayback = useCallback(() => {
if (isPlaying) {
pause();
} else {
play();
}
}, [isPlaying, play, pause]);
// Auto-advance when playing
useEffect(() => {
if (isPlaying) {
intervalRef.current = setInterval(() => {
setStepIndex((prev) => {
if (prev >= totalSteps - 1) {
setIsPlaying(false);
return prev;
}
return prev + 1;
});
}, speedValues[speed]);
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [isPlaying, speed, totalSteps]);
// Keyboard navigation
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Don't capture if user is typing in an input
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement
) {
return;
}
switch (event.key) {
case "ArrowLeft":
event.preventDefault();
prevStep();
break;
case "ArrowRight":
event.preventDefault();
nextStep();
break;
case " ":
event.preventDefault();
togglePlayback();
break;
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [prevStep, nextStep, togglePlayback]);
const value = useMemo<VisualizationContextValue>(
() => ({
data,
currentStep,
stepIndex,
totalSteps,
isPlaying,
speed,
goToStep,
nextStep,
prevStep,
play,
pause,
togglePlayback,
setSpeed,
canGoNext,
canGoPrev,
}),
[
data,
currentStep,
stepIndex,
totalSteps,
isPlaying,
speed,
goToStep,
nextStep,
prevStep,
play,
pause,
togglePlayback,
canGoNext,
canGoPrev,
]
);
return (
<VisualizationContext.Provider value={value}>
{children}
</VisualizationContext.Provider>
);
}
export function useVisualization(): VisualizationContextValue {
const context = useContext(VisualizationContext);
if (!context) {
throw new Error(
"useVisualization must be used within a VisualizationProvider"
);
}
return context;
}

View File

@@ -0,0 +1,263 @@
"use client";
import { useMemo } from "react";
import type { ArrayState, ElementState } from "@/types/visualization";
interface ArrayVisualizerProps {
data: ArrayState;
name?: string;
}
const CELL_WIDTH = 56;
const CELL_HEIGHT = 56;
const CELL_GAP = 4;
const INDEX_AREA_HEIGHT = 20;
const POINTER_HEIGHT = 24;
const ANNOTATION_HEIGHT = 20;
const SVG_PADDING = 16;
function getStateColor(state: ElementState): string {
switch (state) {
case "active":
return "var(--viz-active)";
case "comparing":
return "var(--viz-comparing)";
case "found":
return "var(--viz-found)";
case "visited":
return "var(--viz-visited)";
case "swapping":
return "var(--viz-swapping)";
default:
return "var(--viz-default)";
}
}
function getTextColor(state: ElementState): string {
switch (state) {
case "active":
case "found":
case "swapping":
return "white";
case "comparing":
return "var(--foreground)";
case "visited":
return "var(--muted-foreground)";
default:
return "var(--foreground)";
}
}
export function ArrayVisualizer({ data, name }: ArrayVisualizerProps) {
const { values, pointers = {} } = data;
const dimensions = useMemo(() => {
const hasPointers = Object.keys(pointers).length > 0;
const hasAnnotations = values.some(
(v) => v.annotations && v.annotations.length > 0
);
const contentWidth =
values.length * CELL_WIDTH + (values.length - 1) * CELL_GAP;
const width = contentWidth + SVG_PADDING * 2;
let height = CELL_HEIGHT + INDEX_AREA_HEIGHT + SVG_PADDING * 2;
if (hasPointers) height += POINTER_HEIGHT + 16;
if (hasAnnotations) height += ANNOTATION_HEIGHT;
return {
width,
height,
contentWidth,
hasPointers,
hasAnnotations,
startX: SVG_PADDING,
startY: hasAnnotations
? SVG_PADDING + ANNOTATION_HEIGHT
: SVG_PADDING + 8,
};
}, [values, pointers]);
// Group pointers by position for stacking
const pointersByPosition = useMemo(() => {
const grouped: Record<number, string[]> = {};
for (const [label, index] of Object.entries(pointers)) {
if (!grouped[index]) {
grouped[index] = [];
}
grouped[index].push(label);
}
return grouped;
}, [pointers]);
return (
<div className="flex flex-col gap-2">
{name && (
<span className="text-sm font-medium text-muted-foreground">
{name}
</span>
)}
<div className="overflow-x-auto">
<svg
width={dimensions.width}
height={dimensions.height}
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
className="viz-array"
role="img"
aria-label={`Array visualization with ${values.length} elements`}
>
{/* Array cells */}
<g>
{values.map((element, index) => {
const x = dimensions.startX + index * (CELL_WIDTH + CELL_GAP);
const y = dimensions.startY;
return (
<g
key={index}
className="viz-element"
data-state={element.state}
>
{/* Cell background */}
<rect
x={x}
y={y}
width={CELL_WIDTH}
height={CELL_HEIGHT}
rx={6}
fill={getStateColor(element.state)}
className="viz-cell-bg"
/>
{/* Cell border */}
<rect
x={x}
y={y}
width={CELL_WIDTH}
height={CELL_HEIGHT}
rx={6}
fill="none"
stroke="var(--border)"
strokeWidth={1}
/>
{/* Value */}
<text
x={x + CELL_WIDTH / 2}
y={y + CELL_HEIGHT / 2}
textAnchor="middle"
dominantBaseline="middle"
className="viz-value"
fill={getTextColor(element.state)}
fontSize={18}
fontWeight={500}
fontFamily="var(--font-mono, monospace)"
>
{String(element.value)}
</text>
{/* Annotations (above cell) */}
{element.annotations && element.annotations.length > 0 && (
<text
x={x + CELL_WIDTH / 2}
y={y - 6}
textAnchor="middle"
className="viz-annotation"
fill="var(--primary)"
fontSize={12}
fontWeight={600}
>
{element.annotations.join(", ")}
</text>
)}
</g>
);
})}
</g>
{/* Pointers (below cells, before indices) */}
{dimensions.hasPointers && (
<g className="viz-pointers">
{Object.entries(pointersByPosition).map(([posStr, labels]) => {
const position = parseInt(posStr, 10);
if (position < 0 || position >= values.length) return null;
const x =
dimensions.startX +
position * (CELL_WIDTH + CELL_GAP) +
CELL_WIDTH / 2;
const y =
dimensions.startY + CELL_HEIGHT + INDEX_AREA_HEIGHT + POINTER_HEIGHT;
return (
<g key={posStr} className="viz-pointer">
{/* Arrow */}
<line
x1={x}
y1={y}
x2={x}
y2={y - 10}
stroke="var(--primary)"
strokeWidth={2}
markerEnd="url(#arrowhead)"
/>
{/* Label(s) */}
<text
x={x}
y={y + 12}
textAnchor="middle"
fill="var(--primary)"
fontSize={12}
fontWeight={600}
fontFamily="var(--font-mono, monospace)"
>
{labels.join(", ")}
</text>
</g>
);
})}
</g>
)}
{/* Index labels (rendered last to appear on top) */}
<g className="viz-indices">
{values.map((_, index) => {
const x = dimensions.startX + index * (CELL_WIDTH + CELL_GAP);
const y = dimensions.startY;
return (
<text
key={index}
x={x + CELL_WIDTH / 2}
y={y + CELL_HEIGHT + INDEX_AREA_HEIGHT / 2 + 4}
textAnchor="middle"
dominantBaseline="middle"
className="viz-index"
fill="var(--muted-foreground)"
fontSize={11}
>
{index}
</text>
);
})}
</g>
{/* Arrow marker definition */}
<defs>
<marker
id="arrowhead"
markerWidth="8"
markerHeight="6"
refX="0"
refY="3"
orient="auto"
>
<polygon points="0 0, 8 3, 0 6" fill="var(--primary)" />
</marker>
</defs>
</svg>
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export { ArrayVisualizer } from "./array-visualizer";

View File

@@ -57,6 +57,22 @@ export interface LearningProgression {
challenge: LearningQuestion[];
}
export interface VisualizationExampleStep {
id: string;
description: string;
structures: Record<string, unknown>;
variables?: Record<string, unknown>;
codeHighlight?: { startLine: number; endLine: number };
}
export interface VisualizationExample {
id: string;
title: string;
input?: Record<string, unknown>;
code: string;
steps: VisualizationExampleStep[];
}
export interface PatternTutorial extends Pattern {
metaphor: string | null;
core_concept: string | null;
@@ -69,6 +85,7 @@ export interface PatternTutorial extends Pattern {
prerequisite_patterns: RelatedPattern[] | null;
difficulty_level: number | null;
learning_progression: LearningProgression | null;
visualization_examples: VisualizationExample[] | null;
}
export interface QuestionListItem {

View File

@@ -0,0 +1,160 @@
// Visualization System Types
export type ElementState =
| "default"
| "active"
| "comparing"
| "found"
| "visited"
| "swapping";
// Array Data Structure
export interface ArrayElement {
value: unknown;
state: ElementState;
annotations?: string[];
}
export interface ArrayState {
type: "array";
values: ArrayElement[];
pointers?: Record<string, number>;
}
// Linked List Data Structure
export interface LinkedListNode {
value: unknown;
state: ElementState;
annotations?: string[];
}
export interface LinkedListState {
type: "linkedlist";
nodes: LinkedListNode[];
pointers?: Record<string, number>;
}
// Stack Data Structure
export interface StackState {
type: "stack";
values: Array<{
value: unknown;
state: ElementState;
annotations?: string[];
}>;
}
// Tree Data Structure
export interface TreeNode {
value: unknown;
state: ElementState;
annotations?: string[];
left?: TreeNode | null;
right?: TreeNode | null;
}
export interface TreeState {
type: "tree";
root: TreeNode | null;
pointers?: Record<string, string>;
}
// Graph Data Structure
export interface GraphNode {
id: string;
value: unknown;
state: ElementState;
annotations?: string[];
}
export interface GraphEdge {
from: string;
to: string;
weight?: number;
state: ElementState;
}
export interface GraphState {
type: "graph";
nodes: GraphNode[];
edges: GraphEdge[];
directed?: boolean;
}
// Matrix Data Structure
export interface MatrixCell {
value: unknown;
state: ElementState;
annotations?: string[];
}
export interface MatrixState {
type: "matrix";
rows: MatrixCell[][];
pointers?: Record<string, { row: number; col: number }>;
}
// Union type for all data structures
export type DataStructureState =
| ArrayState
| LinkedListState
| StackState
| TreeState
| GraphState
| MatrixState;
// Code highlighting range
export interface CodeHighlight {
startLine: number;
endLine: number;
}
// Transition configuration
export interface TransitionConfig {
duration?: number;
emphasis?: string;
}
// Core visualization step
export interface VisualizationStep {
id: string;
description: string;
structures: Record<string, DataStructureState>;
variables?: Record<string, unknown>;
codeHighlight?: CodeHighlight;
transition?: TransitionConfig;
}
// Complete visualization data
export interface VisualizationData {
id: string;
title: string;
code: string;
steps: VisualizationStep[];
totalSteps: number;
}
// Visualization example from API (stored in pattern YAML)
export interface VisualizationExample {
id: string;
title: string;
input?: Record<string, unknown>;
code: string;
steps: VisualizationStep[];
}
// Playback state for context
export interface PlaybackState {
currentStep: number;
isPlaying: boolean;
speed: number;
}
// Speed presets (ms per step)
export const SPEED_PRESETS = {
slow: 2000,
normal: 1000,
fast: 500,
} as const;
export type SpeedPreset = keyof typeof SPEED_PRESETS;