Skip to content

Commit 71e28f5

Browse files
authored
Merge pull request #98 from Scc33/claude/phase-5-primitives
Phase 5: visual primitives refactor
2 parents ba1d42a + a178e5e commit 71e28f5

26 files changed

Lines changed: 160 additions & 168 deletions

‎__tests__/context/AlgorithmContext.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const baseState: VisualizationState = {
1313
};
1414

1515
const fakeViz: AlgorithmVisualization = {
16+
primitive: "array-bars",
1617
steps: [
1718
{ array: [3, 1, 2], comparing: [], swapped: false, completed: [] },
1819
{ array: [1, 2, 3], comparing: [], swapped: false, completed: [0, 1, 2] },

components/visualizer/SortingVisualization.tsx renamed to components/primitives/ArrayBars.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
import type { SortingStep } from "@/lib/types";
22

3-
interface SortingVisualizationProps {
3+
interface ArrayBarsProps {
44
step: SortingStep;
55
maxValue: number;
66
}
77

8-
export default function SortingVisualization({
9-
step,
10-
maxValue,
11-
}: SortingVisualizationProps) {
8+
export default function ArrayBars({ step, maxValue }: ArrayBarsProps) {
129
const { array, comparing, swapped, completed } = step;
1310

1411
return (
1512
<div className="flex h-64 w-full items-end justify-center bg-white p-6">
1613
{array.map((value, index) => {
1714
const height = (value / maxValue) * 100;
1815

19-
// Determine the bar color based on its state
2016
let barColor = "bg-blue-400";
2117
if (completed.includes(index)) {
2218
barColor = "bg-green-400";

components/visualizer/SearchVisualization.tsx renamed to components/primitives/ArrayCells.tsx

Lines changed: 8 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,11 @@
11
import type { SearchStep } from "@/lib/types";
2-
import { useAlgorithm } from "@/context/AlgorithmContext";
3-
import { getSearchAlgorithm } from "@/lib/algorithms";
4-
import { useState, useEffect } from "react";
52

6-
interface SearchVisualizationProps {
3+
interface ArrayCellsProps {
74
step: SearchStep;
8-
maxValue: number;
95
}
106

11-
export default function SearchVisualization({
12-
step,
13-
}: SearchVisualizationProps) {
7+
export default function ArrayCells({ step }: ArrayCellsProps) {
148
const { array, current, target, found, visited } = step;
15-
const { state, dispatch } = useAlgorithm();
16-
const [inputTarget, setInputTarget] = useState(target.toString());
17-
18-
// Update the input field when the target changes externally
19-
useEffect(() => {
20-
setInputTarget(target.toString());
21-
}, [target]);
22-
23-
const handleTargetChange = (e: React.ChangeEvent<HTMLInputElement>) => {
24-
const newValue = e.target.value;
25-
setInputTarget(newValue);
26-
27-
// Only process valid numbers
28-
const newTarget = parseInt(newValue);
29-
if (!isNaN(newTarget) && newTarget !== target) {
30-
updateTarget(newTarget);
31-
}
32-
};
33-
34-
const updateTarget = (newTarget: number) => {
35-
// Update the target value
36-
dispatch({ type: "SET_TARGET", payload: newTarget });
37-
38-
// Regenerate visualization with the new target
39-
const algorithmFunction = getSearchAlgorithm(state.algorithm);
40-
if (algorithmFunction) {
41-
try {
42-
const viz = algorithmFunction(state.data, newTarget);
43-
dispatch({ type: "GENERATE_VISUALIZATION", payload: viz });
44-
} catch (error) {
45-
console.error("Error generating visualization:", error);
46-
}
47-
}
48-
};
499

5010
const isNotFound = current === -1 && visited.length > 0;
5111
let statusClass = "text-gray-600";
@@ -60,37 +20,22 @@ export default function SearchVisualization({
6020

6121
return (
6222
<div className="flex w-full flex-col items-center space-y-8 bg-white p-6">
63-
<div className="mb-4 flex w-full flex-wrap items-center gap-4">
64-
<div className="flex items-center gap-2">
65-
<label htmlFor="target-input" className="font-medium text-gray-600">
66-
Target Value:
67-
</label>
68-
<input
69-
id="target-input"
70-
type="number"
71-
value={inputTarget}
72-
onChange={handleTargetChange}
73-
className="w-16 rounded border border-gray-300 px-2 py-1 text-center text-gray-600"
74-
/>
75-
</div>
76-
77-
<div className="ml-auto font-medium text-gray-600">
23+
<div className="mb-4 flex w-full items-center justify-end">
24+
<div className="font-medium text-gray-600">
7825
Status:
7926
<span className={`ml-2 font-bold ${statusClass}`}>{statusText}</span>
8027
</div>
8128
</div>
8229

8330
<div className="flex w-full items-center justify-center">
8431
{array.map((value, index) => {
85-
// Determine the element's state for styling
86-
let stateClass = "bg-blue-400"; // Default state
87-
32+
let stateClass = "bg-blue-400";
8833
if (found && index === current) {
89-
stateClass = "bg-green-500"; // Found state
34+
stateClass = "bg-green-500";
9035
} else if (index === current && !found) {
91-
stateClass = "bg-yellow-400"; // Current examining state
36+
stateClass = "bg-yellow-400";
9237
} else if (visited.includes(index) && !found) {
93-
stateClass = "bg-gray-400"; // Already visited state
38+
stateClass = "bg-gray-400";
9439
}
9540

9641
return (

‎components/primitives/CodeView.tsx‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
interface CodeViewProps {
2+
code: string[];
3+
currentLine?: number;
4+
}
5+
6+
export default function CodeView({ code, currentLine }: CodeViewProps) {
7+
return (
8+
<div className="card">
9+
<div className="border-b bg-gray-100 px-4 py-3">
10+
<h3 className="heading-sm">Pseudocode</h3>
11+
</div>
12+
13+
<div className="overflow-auto bg-white p-4">
14+
<pre className="font-mono text-sm whitespace-pre-wrap text-gray-800">
15+
{code.map((line, index) => {
16+
const indentMatch = line.match(/^\s*/);
17+
const indentLevel = indentMatch ? indentMatch[0].length / 4 : 0;
18+
const paddingLeft = `${indentLevel * 1.5}rem`;
19+
const isActive = currentLine === index;
20+
const lineClasses = isActive
21+
? "border-yellow-400 bg-yellow-100"
22+
: "border-transparent hover:border-blue-400 hover:bg-blue-50";
23+
24+
return (
25+
<div
26+
key={index}
27+
className={`border-l-2 py-1 transition-colors ${lineClasses}`}
28+
style={{ paddingLeft }}
29+
>
30+
{line}
31+
</div>
32+
);
33+
})}
34+
</pre>
35+
</div>
36+
</div>
37+
);
38+
}

components/visualizer/GraphVisualization.tsx renamed to components/primitives/Graph2D.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"use client";
2+
13
import type { GraphStep } from "@/lib/types";
24
import { useState, useEffect } from "react";
35
import type { ReactNode } from "react";
@@ -17,11 +19,11 @@ function getStatusMessage(
1719
return <p>Traversal will start from a selected vertex.</p>;
1820
}
1921

20-
interface GraphVisualizationProps {
22+
interface Graph2DProps {
2123
step: GraphStep;
2224
}
2325

24-
export default function GraphVisualization({ step }: GraphVisualizationProps) {
26+
export default function Graph2D({ step }: Graph2DProps) {
2527
const { graph, current, visited, stack, path, treeEdges } = step;
2628
const [graphSize, setGraphSize] = useState({ width: 0, height: 0 });
2729

‎components/primitives/index.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { ReactElement } from "react";
2+
import { createElement } from "react";
3+
import type { AlgorithmVisualization } from "@/lib/types";
4+
import ArrayBars from "./ArrayBars";
5+
import ArrayCells from "./ArrayCells";
6+
import Graph2D from "./Graph2D";
7+
8+
export { default as ArrayBars } from "./ArrayBars";
9+
export { default as ArrayCells } from "./ArrayCells";
10+
export { default as Graph2D } from "./Graph2D";
11+
export { default as CodeView } from "./CodeView";
12+
13+
/**
14+
* Renders the visualization for the current step using the primitive
15+
* declared on the AlgorithmVisualization. The discriminated union on
16+
* `viz.primitive` makes the switch exhaustive at the type level — adding
17+
* a new primitive surfaces as a compile error here.
18+
*/
19+
export function renderPrimitive(
20+
viz: AlgorithmVisualization,
21+
currentStep: number
22+
): ReactElement {
23+
switch (viz.primitive) {
24+
case "array-bars": {
25+
const step = viz.steps[currentStep]!;
26+
const maxValue = step.array.length > 0 ? Math.max(...step.array) : 100;
27+
return createElement(ArrayBars, { step, maxValue });
28+
}
29+
case "array-cells": {
30+
const step = viz.steps[currentStep]!;
31+
return createElement(ArrayCells, { step });
32+
}
33+
case "graph-2d": {
34+
const step = viz.steps[currentStep]!;
35+
return createElement(Graph2D, { step });
36+
}
37+
}
38+
}

‎components/visualizer/AlgorithmPseudocode.tsx‎

Lines changed: 0 additions & 36 deletions
This file was deleted.

‎components/visualizer/AlgorithmVisualizer.tsx‎

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
"use client";
22

3-
import SortingVisualization from "./SortingVisualization";
4-
import SearchVisualization from "./SearchVisualization";
5-
import GraphVisualization from "./GraphVisualization";
63
import VisualizerControls from "./VisualizerControls";
74
import AlgorithmInfo from "./AlgorithmInfo";
8-
import AlgorithmPseudocode from "./AlgorithmPseudocode";
95
import ColorLegend from "./ColorLegend";
106
import ArrayInputPanel from "./ArrayInputPanel";
7+
import { CodeView, renderPrimitive } from "@/components/primitives";
118
import { useAlgorithm } from "@/context/AlgorithmContext";
129
import {
1310
getGraphAlgorithm,
@@ -17,12 +14,7 @@ import {
1714
} from "@/lib/algorithms";
1815
import { defaultGraphFor } from "@/lib/algorithms/graph/sampleGraphs";
1916
import { generateRandomArray, getRandomValueFromArray } from "@/lib/utils";
20-
import type {
21-
AlgorithmVisualization,
22-
GraphStep,
23-
SearchStep,
24-
SortingStep,
25-
} from "@/lib/types";
17+
import type { AlgorithmVisualization, GraphStep } from "@/lib/types";
2618

2719
function regenerateGraph(
2820
algorithm: string,
@@ -81,7 +73,7 @@ export default function AlgorithmVisualizer() {
8173
const category = visualizationData?.category ?? "";
8274

8375
const numVertices =
84-
category === "graph" && visualizationData
76+
visualizationData?.primitive === "graph-2d"
8577
? (visualizationData.steps[0] as GraphStep).graph.numVertices
8678
: 6;
8779

@@ -133,8 +125,6 @@ export default function AlgorithmVisualizer() {
133125
if (viz) dispatch({ type: "GENERATE_VISUALIZATION", payload: viz });
134126
};
135127

136-
const maxValue = data.length > 0 ? Math.max(...data) : 100;
137-
138128
if (!visualizationData) {
139129
return (
140130
<div className="flex h-64 items-center justify-center">
@@ -146,30 +136,16 @@ export default function AlgorithmVisualizer() {
146136
);
147137
}
148138

139+
const currentLine = visualizationData.steps[currentStep]?.lineNumber;
140+
149141
return (
150142
<div className="space-y-8">
151143
<AlgorithmInfo algorithm={visualizationData} />
152144

153145
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
154146
<div className="space-y-6 lg:col-span-2">
155147
<div className="card">
156-
{category === "sorting" && (
157-
<SortingVisualization
158-
step={visualizationData.steps[currentStep] as SortingStep}
159-
maxValue={maxValue}
160-
/>
161-
)}
162-
{category === "searching" && (
163-
<SearchVisualization
164-
step={visualizationData.steps[currentStep] as SearchStep}
165-
maxValue={maxValue}
166-
/>
167-
)}
168-
{category === "graph" && (
169-
<GraphVisualization
170-
step={visualizationData.steps[currentStep] as GraphStep}
171-
/>
172-
)}
148+
{renderPrimitive(visualizationData, currentStep)}
173149
</div>
174150

175151
<ArrayInputPanel
@@ -193,7 +169,10 @@ export default function AlgorithmVisualizer() {
193169
</div>
194170

195171
<div className="lg:col-span-1">
196-
<AlgorithmPseudocode code={visualizationData.pseudoCode} />
172+
<CodeView
173+
code={visualizationData.pseudoCode}
174+
{...(currentLine !== undefined ? { currentLine } : {})}
175+
/>
197176
</div>
198177
</div>
199178
</div>

‎lib/algorithms/graph/bellmanFord.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function bellmanFord(
7070
path: [...state.path],
7171
});
7272

73-
return createVisualization("bellmanFord", state.steps, {
73+
return createVisualization("bellmanFord", "graph-2d", state.steps, {
7474
timeComplexity: "O(V·E)",
7575
spaceComplexity: "O(V)",
7676
reference: "https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm",

0 commit comments

Comments
 (0)