feat(viz): sprint 1 - array visualisations
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import { useVisualization } from '@/lib/visualizations/use-visualization';
|
||||
import type { AlgorithmDefinition } from '@/lib/visualizations/types';
|
||||
import { VisualizationContainer } from '../core/visualization-container';
|
||||
import { ArrayView } from '../data-structures/array-view';
|
||||
import { CalculationBubble } from '../primitives/calculation-bubble';
|
||||
|
||||
interface PrefixSumVisualizationProps {
|
||||
algorithm: AlgorithmDefinition;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PrefixSumVisualization({
|
||||
algorithm,
|
||||
className,
|
||||
}: PrefixSumVisualizationProps) {
|
||||
const {
|
||||
currentStep,
|
||||
currentStepIndex,
|
||||
totalSteps,
|
||||
playback,
|
||||
controls,
|
||||
currentPhase,
|
||||
progress,
|
||||
} = useVisualization(algorithm);
|
||||
|
||||
const { dataState } = currentStep;
|
||||
const originalArray = dataState.arrays[0];
|
||||
const prefixArray = dataState.arrays[1] ?? null;
|
||||
const calculation = dataState.calculations[0] ?? null;
|
||||
|
||||
// Filter pointers for each array
|
||||
// Pointers for original array: those with index < original array length
|
||||
// Pointers for prefix array: shown on prefix array if it exists
|
||||
const originalPointers = prefixArray
|
||||
? dataState.pointers.filter(
|
||||
(p) =>
|
||||
p.name === 'num' ||
|
||||
p.name === 'curr' ||
|
||||
p.name === 'i' ||
|
||||
p.name === 'j'
|
||||
)
|
||||
: dataState.pointers;
|
||||
|
||||
const prefixPointers = prefixArray
|
||||
? dataState.pointers.filter(
|
||||
(p) =>
|
||||
p.name === 'prefix[i]' ||
|
||||
p.name === 'prefix[j+1]' ||
|
||||
(p.name !== 'num' &&
|
||||
p.name !== 'curr' &&
|
||||
p.name !== 'i' &&
|
||||
p.name !== 'j')
|
||||
)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<VisualizationContainer
|
||||
title={algorithm.title}
|
||||
pattern={algorithm.pattern}
|
||||
code={algorithm.code}
|
||||
currentLine={currentStep.codeLine}
|
||||
highlightLines={currentStep.codeHighlightLines}
|
||||
explanation={currentStep.explanation}
|
||||
decision={currentStep.decision}
|
||||
phase={currentPhase}
|
||||
variables={dataState.variables}
|
||||
currentStepIndex={currentStepIndex}
|
||||
totalSteps={totalSteps}
|
||||
isPlaying={playback.isPlaying}
|
||||
speed={playback.speed}
|
||||
controls={controls}
|
||||
progress={progress}
|
||||
className={className}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
{/* Fixed height area for calculation bubble */}
|
||||
<div className="flex h-8 items-center justify-center">
|
||||
<CalculationBubble calculation={calculation} />
|
||||
</div>
|
||||
|
||||
{/* Original array */}
|
||||
{originalArray && (
|
||||
<ArrayView
|
||||
array={originalArray}
|
||||
pointers={originalPointers}
|
||||
elementSize="sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Prefix sum array (appears during execution) */}
|
||||
{prefixArray && (
|
||||
<ArrayView
|
||||
array={prefixArray}
|
||||
pointers={prefixPointers}
|
||||
elementSize="sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</VisualizationContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { useVisualization } from "@/lib/visualizations/use-visualization";
|
||||
import type { AlgorithmDefinition } from "@/lib/visualizations/types";
|
||||
import { VisualizationContainer } from "../core/visualization-container";
|
||||
import { ArrayView } from "../data-structures/array-view";
|
||||
import { CalculationBubble } from "../primitives/calculation-bubble";
|
||||
import { useVisualization } from '@/lib/visualizations/use-visualization';
|
||||
import type { AlgorithmDefinition } from '@/lib/visualizations/types';
|
||||
import { VisualizationContainer } from '../core/visualization-container';
|
||||
import { ArrayView } from '../data-structures/array-view';
|
||||
import { CalculationBubble } from '../primitives/calculation-bubble';
|
||||
|
||||
interface TwoPointersVisualizationProps {
|
||||
algorithm: AlgorithmDefinition;
|
||||
@@ -59,7 +59,7 @@ export function TwoPointersVisualization({
|
||||
<ArrayView
|
||||
array={mainArray}
|
||||
pointers={dataState.pointers}
|
||||
elementSize="md"
|
||||
elementSize="sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { DecisionCallout, VisualizationPhase } from "@/lib/visualizations/types";
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { DecisionCallout, VisualizationPhase } from '@/lib/visualizations/types';
|
||||
|
||||
interface ExplanationPanelProps {
|
||||
explanation: string;
|
||||
@@ -12,19 +12,19 @@ interface ExplanationPanelProps {
|
||||
}
|
||||
|
||||
const PHASE_LABELS: Record<VisualizationPhase, string> = {
|
||||
problem: "Problem",
|
||||
intuition: "Intuition",
|
||||
pattern: "Pattern",
|
||||
code: "Code",
|
||||
execution: "Execution",
|
||||
problem: 'Problem',
|
||||
intuition: 'Intuition',
|
||||
pattern: 'Pattern',
|
||||
code: 'Code',
|
||||
execution: 'Execution',
|
||||
};
|
||||
|
||||
const PHASE_COLORS: Record<VisualizationPhase, string> = {
|
||||
problem: "bg-red-500/20 text-red-500",
|
||||
intuition: "bg-amber-500/20 text-amber-500",
|
||||
pattern: "bg-blue-500/20 text-blue-500",
|
||||
code: "bg-[var(--primary)]/20 text-[var(--primary)]",
|
||||
execution: "bg-green-500/20 text-green-500",
|
||||
problem: 'bg-error/20 text-error',
|
||||
intuition: 'bg-warning/20 text-warning',
|
||||
pattern: 'bg-info/20 text-info',
|
||||
code: 'bg-primary/20 text-primary',
|
||||
execution: 'bg-success/20 text-success',
|
||||
};
|
||||
|
||||
export function ExplanationPanel({
|
||||
@@ -36,14 +36,14 @@ export function ExplanationPanel({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-72 overflow-y-auto rounded-lg border border-[var(--border)] bg-[var(--card)] p-4",
|
||||
'h-28 overflow-y-auto rounded-lg border border-border bg-surface p-4',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2.5 py-0.5 text-xs font-medium",
|
||||
'rounded-full px-2.5 py-0.5 text-xs font-medium',
|
||||
PHASE_COLORS[phase]
|
||||
)}
|
||||
>
|
||||
@@ -59,7 +59,7 @@ export function ExplanationPanel({
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<p className="text-base leading-relaxed text-[var(--foreground)]">
|
||||
<p className="text-base leading-relaxed text-foreground">
|
||||
{explanation}
|
||||
</p>
|
||||
|
||||
@@ -67,23 +67,23 @@ export function ExplanationPanel({
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="mt-4 rounded-lg border border-[var(--primary)]/30 bg-[var(--primary)]/5 p-4"
|
||||
className="mt-4 rounded-lg border border-primary/30 bg-primary/5 p-4"
|
||||
>
|
||||
<div className="mb-2 text-sm font-medium text-[var(--primary)]">
|
||||
<div className="mb-2 text-sm font-medium text-primary">
|
||||
Decision Point
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium text-[var(--muted-foreground)]">Q:</span>
|
||||
<span className="text-[var(--foreground)]">{decision.question}</span>
|
||||
<span className="font-medium text-foreground-muted">Q:</span>
|
||||
<span className="text-foreground">{decision.question}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium text-[var(--muted-foreground)]">A:</span>
|
||||
<span className="font-mono text-[var(--primary)]">{decision.answer}</span>
|
||||
<span className="font-medium text-foreground-muted">A:</span>
|
||||
<span className="font-mono text-primary">{decision.answer}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium text-[var(--muted-foreground)]">→</span>
|
||||
<span className="text-green-500">{decision.action}</span>
|
||||
<span className="font-medium text-foreground-muted">→</span>
|
||||
<span className="text-success">{decision.action}</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import type {
|
||||
PlaybackSpeed,
|
||||
VisualizationControls,
|
||||
} from "@/lib/visualizations/types";
|
||||
} from '@/lib/visualizations/types';
|
||||
|
||||
interface StepControlsProps {
|
||||
currentStepIndex: number;
|
||||
@@ -33,107 +33,81 @@ export function StepControls({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-3 rounded-lg border border-[var(--border)] bg-[var(--card)] p-4",
|
||||
'flex items-center gap-4 rounded-lg border border-border bg-surface px-4 py-2',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={controls.goToFirst}
|
||||
disabled={isAtStart}
|
||||
aria-label="Go to first step"
|
||||
className={cn(
|
||||
"rounded-md p-2 transition-colors",
|
||||
"hover:bg-[var(--muted)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)]",
|
||||
"disabled:cursor-not-allowed disabled:opacity-40"
|
||||
)}
|
||||
>
|
||||
<FirstIcon />
|
||||
</button>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={controls.goToFirst}
|
||||
disabled={isAtStart}
|
||||
aria-label="Go to first step"
|
||||
className={cn(
|
||||
'rounded-md p-1.5 transition-colors',
|
||||
'hover:bg-surface-variant focus:outline-none focus:ring-2 focus:ring-primary',
|
||||
'disabled:cursor-not-allowed disabled:opacity-40'
|
||||
)}
|
||||
>
|
||||
<FirstIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={controls.stepBackward}
|
||||
disabled={isAtStart}
|
||||
aria-label="Previous step"
|
||||
className={cn(
|
||||
"rounded-md p-2 transition-colors",
|
||||
"hover:bg-[var(--muted)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)]",
|
||||
"disabled:cursor-not-allowed disabled:opacity-40"
|
||||
)}
|
||||
>
|
||||
<PreviousIcon />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={controls.stepBackward}
|
||||
disabled={isAtStart}
|
||||
aria-label="Previous step"
|
||||
className={cn(
|
||||
'rounded-md p-1.5 transition-colors',
|
||||
'hover:bg-surface-variant focus:outline-none focus:ring-2 focus:ring-primary',
|
||||
'disabled:cursor-not-allowed disabled:opacity-40'
|
||||
)}
|
||||
>
|
||||
<PreviousIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={controls.togglePlay}
|
||||
aria-label={isPlaying ? "Pause" : "Play"}
|
||||
className={cn(
|
||||
"rounded-full bg-[var(--primary)] p-3 text-white transition-colors",
|
||||
"hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:ring-offset-2"
|
||||
)}
|
||||
>
|
||||
{isPlaying ? <PauseIcon /> : <PlayIcon />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={controls.togglePlay}
|
||||
aria-label={isPlaying ? 'Pause' : 'Play'}
|
||||
className={cn(
|
||||
'rounded-full bg-primary p-2 text-white transition-colors',
|
||||
'hover:bg-primary-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-surface'
|
||||
)}
|
||||
>
|
||||
{isPlaying ? <PauseIcon /> : <PlayIcon />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={controls.stepForward}
|
||||
disabled={isAtEnd}
|
||||
aria-label="Next step"
|
||||
className={cn(
|
||||
"rounded-md p-2 transition-colors",
|
||||
"hover:bg-[var(--muted)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)]",
|
||||
"disabled:cursor-not-allowed disabled:opacity-40"
|
||||
)}
|
||||
>
|
||||
<NextIcon />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={controls.stepForward}
|
||||
disabled={isAtEnd}
|
||||
aria-label="Next step"
|
||||
className={cn(
|
||||
'rounded-md p-1.5 transition-colors',
|
||||
'hover:bg-surface-variant focus:outline-none focus:ring-2 focus:ring-primary',
|
||||
'disabled:cursor-not-allowed disabled:opacity-40'
|
||||
)}
|
||||
>
|
||||
<NextIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={controls.goToLast}
|
||||
disabled={isAtEnd}
|
||||
aria-label="Go to last step"
|
||||
className={cn(
|
||||
"rounded-md p-2 transition-colors",
|
||||
"hover:bg-[var(--muted)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)]",
|
||||
"disabled:cursor-not-allowed disabled:opacity-40"
|
||||
)}
|
||||
>
|
||||
<LastIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-[var(--muted-foreground)]">
|
||||
Step {currentStepIndex + 1} of {totalSteps}
|
||||
</span>
|
||||
|
||||
<select
|
||||
value={speed}
|
||||
onChange={(e) =>
|
||||
controls.setSpeed(parseFloat(e.target.value) as PlaybackSpeed)
|
||||
}
|
||||
aria-label="Playback speed"
|
||||
className={cn(
|
||||
"rounded-md border border-[var(--border)] bg-[var(--card)] px-2 py-1 text-sm",
|
||||
"focus:border-[var(--primary)] focus:outline-none focus:ring-1 focus:ring-[var(--primary)]"
|
||||
)}
|
||||
>
|
||||
{SPEED_OPTIONS.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{s}x
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={controls.goToLast}
|
||||
disabled={isAtEnd}
|
||||
aria-label="Go to last step"
|
||||
className={cn(
|
||||
'rounded-md p-1.5 transition-colors',
|
||||
'hover:bg-surface-variant focus:outline-none focus:ring-2 focus:ring-primary',
|
||||
'disabled:cursor-not-allowed disabled:opacity-40'
|
||||
)}
|
||||
>
|
||||
<LastIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative h-2 w-full overflow-hidden rounded-full bg-[var(--muted)]">
|
||||
<div className="relative h-2 flex-1 overflow-hidden rounded-full bg-surface-variant">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-0 w-full cursor-pointer"
|
||||
@@ -147,11 +121,35 @@ export function StepControls({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-[var(--primary)] transition-all duration-200"
|
||||
className="h-full bg-primary transition-all duration-200"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="whitespace-nowrap text-xs text-foreground-muted">
|
||||
{currentStepIndex + 1}/{totalSteps}
|
||||
</span>
|
||||
|
||||
<select
|
||||
value={speed}
|
||||
onChange={(e) =>
|
||||
controls.setSpeed(parseFloat(e.target.value) as PlaybackSpeed)
|
||||
}
|
||||
aria-label="Playback speed"
|
||||
className={cn(
|
||||
'rounded-md border border-border bg-surface px-1.5 py-0.5 text-xs',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
>
|
||||
{SPEED_OPTIONS.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{s}x
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ReactNode } from 'react';
|
||||
import type {
|
||||
AlgorithmCode,
|
||||
AlgorithmPattern,
|
||||
@@ -10,11 +10,11 @@ import type {
|
||||
VariableState,
|
||||
VisualizationControls,
|
||||
VisualizationPhase,
|
||||
} from "@/lib/visualizations/types";
|
||||
import { CodePanel } from "./code-panel";
|
||||
import { ExplanationPanel } from "./explanation-panel";
|
||||
import { StepControls } from "./step-controls";
|
||||
import { VariableInspector } from "./variable-inspector";
|
||||
} from '@/lib/visualizations/types';
|
||||
import { CodePanel } from './code-panel';
|
||||
import { ExplanationPanel } from './explanation-panel';
|
||||
import { StepControls } from './step-controls';
|
||||
import { VariableInspector } from './variable-inspector';
|
||||
|
||||
interface VisualizationContainerProps {
|
||||
title: string;
|
||||
@@ -58,44 +58,44 @@ export function VisualizationContainer({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-4 rounded-xl border border-[var(--border)] bg-[var(--card)] p-4",
|
||||
'flex flex-col rounded-xl border border-border bg-background-subtle p-4',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<header className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 className="text-xl font-semibold text-[var(--foreground)]">{title}</h2>
|
||||
<span className="rounded-full bg-[var(--primary)]/20 px-3 py-1 text-sm font-medium text-[var(--primary)]">
|
||||
<header className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 className="text-xl font-semibold text-foreground">{title}</h2>
|
||||
<span className="rounded-full bg-primary/20 px-3 py-1 text-sm font-medium text-primary">
|
||||
{pattern.name}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-[1fr_1.5fr]">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid gap-3 lg:grid-cols-[1fr_1.5fr]">
|
||||
<div className="flex flex-col gap-3">
|
||||
<CodePanel
|
||||
code={code}
|
||||
currentLine={currentLine}
|
||||
highlightLines={highlightLines}
|
||||
className="max-h-80 min-h-48"
|
||||
className="h-64"
|
||||
/>
|
||||
<VariableInspector variables={variables} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div
|
||||
className="flex h-64 items-center justify-center rounded-lg border border-[var(--border)] bg-[var(--background)] p-4"
|
||||
role="img"
|
||||
aria-label="Algorithm visualization"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<ExplanationPanel
|
||||
explanation={explanation}
|
||||
decision={decision}
|
||||
phase={phase}
|
||||
/>
|
||||
<div
|
||||
className="flex min-h-80 items-center justify-center rounded-lg border border-border bg-surface px-8 py-4 lg:h-auto"
|
||||
role="img"
|
||||
aria-label="Algorithm visualization"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ExplanationPanel
|
||||
explanation={explanation}
|
||||
decision={decision}
|
||||
phase={phase}
|
||||
className="mt-3"
|
||||
/>
|
||||
|
||||
<StepControls
|
||||
currentStepIndex={currentStepIndex}
|
||||
totalSteps={totalSteps}
|
||||
@@ -103,6 +103,7 @@ export function VisualizationContainer({
|
||||
speed={speed}
|
||||
controls={controls}
|
||||
progress={progress}
|
||||
className="mt-3"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,4 +14,5 @@ export { CalculationBubble } from "./primitives/calculation-bubble";
|
||||
export { ArrayView } from "./data-structures/array-view";
|
||||
|
||||
// Algorithm visualizations
|
||||
export { PrefixSumVisualization } from "./algorithms/prefix-sum";
|
||||
export { TwoPointersVisualization } from "./algorithms/two-pointers";
|
||||
|
||||
Reference in New Issue
Block a user