feat(viz): two pointers narrative
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { AlgorithmCode } from "@/lib/visualizations/types";
|
||||
|
||||
interface CodePanelProps {
|
||||
code: AlgorithmCode;
|
||||
currentLine?: number;
|
||||
highlightLines?: number[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const LANGUAGE_MAP: Record<AlgorithmCode["language"], string> = {
|
||||
typescript: "typescript",
|
||||
javascript: "javascript",
|
||||
python: "python",
|
||||
};
|
||||
|
||||
export function CodePanel({
|
||||
code,
|
||||
currentLine,
|
||||
highlightLines = [],
|
||||
className,
|
||||
}: CodePanelProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentLine && containerRef.current) {
|
||||
const container = containerRef.current;
|
||||
const lineHeight = 24;
|
||||
const lineTop = (currentLine - 1) * lineHeight;
|
||||
const containerHeight = container.clientHeight;
|
||||
const scrollTarget = lineTop - containerHeight / 2 + lineHeight / 2;
|
||||
|
||||
container.scrollTo({
|
||||
top: Math.max(0, scrollTarget),
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}, [currentLine]);
|
||||
|
||||
const codeLines = code.code.trim().split("\n");
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
"overflow-auto rounded-lg border border-[var(--border)] bg-[#282c34]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="relative min-w-max">
|
||||
{codeLines.map((line, i) => {
|
||||
const lineNumber = i + 1;
|
||||
const isCurrentLine = lineNumber === currentLine;
|
||||
const isHighlighted = highlightLines.includes(lineNumber);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
"flex transition-colors duration-200",
|
||||
isCurrentLine && "bg-[var(--primary)]/20",
|
||||
isHighlighted && !isCurrentLine && "bg-[var(--primary)]/10"
|
||||
)}
|
||||
>
|
||||
<span className="inline-block w-10 flex-shrink-0 select-none py-0.5 pr-3 text-right font-mono text-xs text-[var(--muted-foreground)]/60">
|
||||
{lineNumber}
|
||||
</span>
|
||||
{isCurrentLine && (
|
||||
<span
|
||||
className="inline-block w-4 flex-shrink-0 py-0.5 font-mono text-xs text-[var(--primary)]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
→
|
||||
</span>
|
||||
)}
|
||||
{!isCurrentLine && (
|
||||
<span
|
||||
className="inline-block w-4 flex-shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<SyntaxHighlighter
|
||||
language={LANGUAGE_MAP[code.language]}
|
||||
style={oneDark}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: "2px 8px 2px 0",
|
||||
background: "transparent",
|
||||
fontSize: "0.875rem",
|
||||
lineHeight: "1.5",
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
fontFamily: "var(--font-mono, monospace)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{line || " "}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { DecisionCallout, VisualizationPhase } from "@/lib/visualizations/types";
|
||||
|
||||
interface ExplanationPanelProps {
|
||||
explanation: string;
|
||||
decision?: DecisionCallout;
|
||||
phase: VisualizationPhase;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PHASE_LABELS: Record<VisualizationPhase, string> = {
|
||||
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",
|
||||
};
|
||||
|
||||
export function ExplanationPanel({
|
||||
explanation,
|
||||
decision,
|
||||
phase,
|
||||
className,
|
||||
}: ExplanationPanelProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-72 overflow-y-auto rounded-lg border border-[var(--border)] bg-[var(--card)] 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",
|
||||
PHASE_COLORS[phase]
|
||||
)}
|
||||
>
|
||||
{PHASE_LABELS[phase]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={explanation}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<p className="text-base leading-relaxed text-[var(--foreground)]">
|
||||
{explanation}
|
||||
</p>
|
||||
|
||||
{decision && (
|
||||
<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"
|
||||
>
|
||||
<div className="mb-2 text-sm font-medium text-[var(--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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
PlaybackSpeed,
|
||||
VisualizationControls,
|
||||
} from "@/lib/visualizations/types";
|
||||
|
||||
interface StepControlsProps {
|
||||
currentStepIndex: number;
|
||||
totalSteps: number;
|
||||
isPlaying: boolean;
|
||||
speed: PlaybackSpeed;
|
||||
controls: VisualizationControls;
|
||||
progress: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SPEED_OPTIONS: PlaybackSpeed[] = [0.5, 1, 2];
|
||||
|
||||
export function StepControls({
|
||||
currentStepIndex,
|
||||
totalSteps,
|
||||
isPlaying,
|
||||
speed,
|
||||
controls,
|
||||
progress,
|
||||
className,
|
||||
}: StepControlsProps) {
|
||||
const isAtStart = currentStepIndex === 0;
|
||||
const isAtEnd = currentStepIndex === totalSteps - 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-3 rounded-lg border border-[var(--border)] bg-[var(--card)] p-4",
|
||||
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>
|
||||
|
||||
<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.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.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.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>
|
||||
</div>
|
||||
|
||||
<div className="relative h-2 w-full overflow-hidden rounded-full bg-[var(--muted)]">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-0 w-full cursor-pointer"
|
||||
aria-label="Jump to step"
|
||||
onClick={(e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const percent = x / rect.width;
|
||||
const step = Math.round(percent * (totalSteps - 1));
|
||||
controls.goToStep(step);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-[var(--primary)] transition-all duration-200"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PlayIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function PauseIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M6.75 5.25a.75.75 0 01.75-.75H9a.75.75 0 01.75.75v13.5a.75.75 0 01-.75.75H7.5a.75.75 0 01-.75-.75V5.25zm7.5 0A.75.75 0 0115 4.5h1.5a.75.75 0 01.75.75v13.5a.75.75 0 01-.75.75H15a.75.75 0 01-.75-.75V5.25z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviousIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M7.72 12.53a.75.75 0 010-1.06l7.5-7.5a.75.75 0 111.06 1.06L9.31 12l6.97 6.97a.75.75 0 11-1.06 1.06l-7.5-7.5z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function NextIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.28 11.47a.75.75 0 010 1.06l-7.5 7.5a.75.75 0 01-1.06-1.06L14.69 12 7.72 5.03a.75.75 0 011.06-1.06l7.5 7.5z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function FirstIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.25 5.25a.75.75 0 01.75.75v12a.75.75 0 01-1.5 0V6a.75.75 0 01.75-.75zm6.22.53a.75.75 0 011.06 0l6 6a.75.75 0 010 1.06l-6 6a.75.75 0 01-1.06-1.06L16.19 12 11.47 6.84a.75.75 0 010-1.06z"
|
||||
clipRule="evenodd"
|
||||
transform="scale(-1, 1) translate(-24, 0)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LastIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18.75 5.25a.75.75 0 01.75.75v12a.75.75 0 01-1.5 0V6a.75.75 0 01.75-.75zm-6.22.53a.75.75 0 00-1.06 0l-6 6a.75.75 0 000 1.06l6 6a.75.75 0 001.06-1.06L7.81 12l4.72-5.16a.75.75 0 000-1.06z"
|
||||
clipRule="evenodd"
|
||||
transform="scale(-1, 1) translate(-24, 0)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { VariableState } from "@/lib/visualizations/types";
|
||||
|
||||
interface VariableInspectorProps {
|
||||
variables: VariableState[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function VariableInspector({
|
||||
variables,
|
||||
className,
|
||||
}: VariableInspectorProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-40 overflow-y-auto rounded-lg border border-[var(--border)] bg-[var(--card)] p-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="mb-3 text-xs font-medium uppercase tracking-wide text-[var(--muted-foreground)]">
|
||||
Variables
|
||||
</div>
|
||||
<div className="flex min-h-10 flex-wrap gap-3">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{variables.length === 0 && (
|
||||
<span className="text-sm text-[var(--muted-foreground)]/50">
|
||||
No variables yet
|
||||
</span>
|
||||
)}
|
||||
{variables.map((variable) => (
|
||||
<motion.div
|
||||
key={variable.id}
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
className="flex items-center gap-2 rounded-md border border-[var(--border)] bg-[var(--muted)] px-3 py-2"
|
||||
>
|
||||
<span className="font-mono text-sm text-[var(--muted-foreground)]">
|
||||
{variable.name}
|
||||
</span>
|
||||
<span className="text-[var(--muted-foreground)]">=</span>
|
||||
<motion.span
|
||||
key={`${variable.id}-${variable.value}`}
|
||||
initial={{ scale: 1.2, color: "var(--primary)" }}
|
||||
animate={{ scale: 1, color: "var(--foreground)" }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="font-mono text-sm font-medium"
|
||||
>
|
||||
{String(variable.value)}
|
||||
</motion.span>
|
||||
{variable.previousValue !== undefined &&
|
||||
variable.previousValue !== variable.value && (
|
||||
<span className="ml-1 text-xs text-[var(--muted-foreground)] line-through">
|
||||
{String(variable.previousValue)}
|
||||
</span>
|
||||
)}
|
||||
{variable.derivation && (
|
||||
<span className="ml-1 text-xs text-[var(--muted-foreground)]">
|
||||
({variable.derivation})
|
||||
</span>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ReactNode } from "react";
|
||||
import type {
|
||||
AlgorithmCode,
|
||||
AlgorithmPattern,
|
||||
DecisionCallout,
|
||||
PlaybackSpeed,
|
||||
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";
|
||||
|
||||
interface VisualizationContainerProps {
|
||||
title: string;
|
||||
pattern: AlgorithmPattern;
|
||||
code: AlgorithmCode;
|
||||
currentLine?: number;
|
||||
highlightLines?: number[];
|
||||
explanation: string;
|
||||
decision?: DecisionCallout;
|
||||
phase: VisualizationPhase;
|
||||
variables: VariableState[];
|
||||
currentStepIndex: number;
|
||||
totalSteps: number;
|
||||
isPlaying: boolean;
|
||||
speed: PlaybackSpeed;
|
||||
controls: VisualizationControls;
|
||||
progress: number;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function VisualizationContainer({
|
||||
title,
|
||||
pattern,
|
||||
code,
|
||||
currentLine,
|
||||
highlightLines,
|
||||
explanation,
|
||||
decision,
|
||||
phase,
|
||||
variables,
|
||||
currentStepIndex,
|
||||
totalSteps,
|
||||
isPlaying,
|
||||
speed,
|
||||
controls,
|
||||
progress,
|
||||
children,
|
||||
className,
|
||||
}: VisualizationContainerProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-4 rounded-xl border border-[var(--border)] bg-[var(--card)] 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)]">
|
||||
{pattern.name}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-[1fr_1.5fr]">
|
||||
<div className="flex flex-col gap-4">
|
||||
<CodePanel
|
||||
code={code}
|
||||
currentLine={currentLine}
|
||||
highlightLines={highlightLines}
|
||||
className="max-h-80 min-h-48"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<StepControls
|
||||
currentStepIndex={currentStepIndex}
|
||||
totalSteps={totalSteps}
|
||||
isPlaying={isPlaying}
|
||||
speed={speed}
|
||||
controls={controls}
|
||||
progress={progress}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user