import React, { useState, useMemo, useEffect } from 'react';
import { RefreshCw, ChevronRight, ChevronLeft, Info, BookOpen, Layers, Play } from 'lucide-react';
const COLORS = {
outer: 'bg-blue-100 border-blue-500 text-blue-900',
inner: 'bg-purple-100 border-purple-500 text-purple-900',
success: 'bg-green-100 border-green-500 text-green-900',
fail: 'bg-red-50 border-red-200 text-red-300 opacity-50',
neutral: 'bg-gray-100 border-gray-300 text-gray-700',
highlight: 'bg-yellow-100 border-yellow-500 text-yellow-900'
};
const App = () => {
const [activeTab, setActiveTab] = useState('basic');
return (
{/* Header */}
{/* Navigation Tabs */}
setActiveTab('basic')}
icon={}
title="1. The Basics"
desc="Single loop mapping"
/>
setActiveTab('filter')}
icon={}
title="2. Filtering"
desc="Adding conditions (if)"
/>
setActiveTab('nested')}
icon={}
title="3. Nested Loops"
desc="The 'Multiplier' Effect"
/>
{/* Main Content Area */}
{activeTab === 'basic' && }
{activeTab === 'filter' && }
{activeTab === 'nested' && }
);
};
const TabButton = ({ active, onClick, icon, title, desc }) => (
{icon}
);
// --- LESSON COMPONENTS ---
const BasicLesson = () => {
const data = useMemo(() => ["apple", "banana", "cherry"], []);
return (
({
logic: (item) => ({
result: item.toUpperCase(),
kept: true,
desc: `Convert "${item}" to uppercase`
})
}), [])}
/>
);
};
const FilterLesson = () => {
const data = useMemo(() => ["apple", "banana", "kiwi", "mango"], []);
return (
({
logic: (item) => {
const hasN = item.includes('n');
return {
result: item,
kept: hasN,
desc: hasN ? `"${item}" contains 'n'. Keep it.` : `"${item}" has no 'n'. Discard.`
};
}
}), [])}
/>
);
};
const NestedLesson = () => {
const fruits = useMemo(() => ["apple", "kiwi"], []);
const duplicate = useMemo(() => [1, 2, 3], []);
return (
({
logic: (x, y) => {
const hasA = x.includes('a');
return {
result: x,
kept: hasA,
desc: `Outer: "${x}", Inner: ${y}. Check "a" in "${x}"? ${hasA ? 'Yes' : 'No'}.`
};
}
}), [])}
/>
);
};
// --- CORE ENGINE ---
const VisualizerEngine = ({ title, description, code, data, loopConfig, isNested = false }) => {
const [stepIndex, setStepIndex] = useState(-1);
// Extract Data Lists
const outerKey = Object.keys(data)[0];
const outerList = data[outerKey];
const innerKey = isNested ? Object.keys(data)[1] : null;
const rawInnerList = isNested ? data[innerKey] : null;
// Pre-calculate the entire simulation timeline
// This ensures stability: we don't calculate on the fly, avoiding "infinite add" bugs
const timeline = useMemo(() => {
const steps = [];
const innerListToUse = isNested ? rawInnerList : [null];
outerList.forEach((itemX, idxX) => {
innerListToUse.forEach((itemY, idxY) => {
const outcome = isNested
? loopConfig.logic(itemX, itemY)
: loopConfig.logic(itemX);
steps.push({
outerItem: itemX,
innerItem: itemY,
outerIdx: idxX,
innerIdx: idxY,
...outcome
});
});
});
return steps;
}, [outerList, rawInnerList, isNested, loopConfig]);
const totalSteps = timeline.length;
// Derived state for the UI based on current stepIndex
const currentStep = stepIndex >= 0 && stepIndex < totalSteps ? timeline[stepIndex] : null;
const currentLog = currentStep ? currentStep.desc : "";
// Calculate results up to the current step
const currentResults = useMemo(() => {
if (stepIndex === -1) return [];
// Slice timeline up to current step, filter only kept items
return timeline.slice(0, stepIndex + 1)
.filter(step => step.kept)
.map(step => step.result);
}, [timeline, stepIndex]);
const progress = Math.max(0, ((stepIndex + 1) / totalSteps) * 100);
// Controls
const handleNext = () => {
if (stepIndex < totalSteps - 1) setStepIndex(prev => prev + 1);
};
const handlePrev = () => {
if (stepIndex > -1) setStepIndex(prev => prev - 1);
};
const handleReset = () => {
setStepIndex(-1);
};
// Auto-reset when tab changes (data changes)
useEffect(() => {
setStepIndex(-1);
}, [title]);
return (
{/* Top Bar */}
{/* Controls */}
Prev
= totalSteps - 1}
className={`flex items-center gap-1 px-6 py-2 rounded-lg font-bold text-sm transition-all shadow-sm ${
stepIndex >= totalSteps - 1
? 'bg-slate-100 text-slate-400 cursor-not-allowed shadow-none'
: 'bg-blue-600 text-white hover:bg-blue-700 shadow-blue-200'
}`}
>
{stepIndex === -1 ? 'Start' : 'Next'}
{/* Code Block */}
{/* Visualization Area */}
{/* LEFT: Inputs */}
{/* CENTER: Processing Stage */}
{currentStep ? (
) : (
stepIndex === -1 ? (
Click Next to step through
) : (
)
)}
{/* RIGHT: Output */}
newlist = []
{currentResults.length} items
{currentResults.length === 0 && stepIndex > -1 && stepIndex < totalSteps - 1 && (
Collecting items...
)}
{currentResults.map((res, idx) => (
{typeof res === 'string' ? `'${res}'` : res}
))}
);
};
// --- HELPER COMPONENTS ---
const ListBox = ({ title, items, activeIndex, color, label }) => {
const isBlue = color === 'blue';
return (
{title}
{label}
{items.map((item, idx) => (
{typeof item === 'string' ? `"${item}"` : item}
{idx === activeIndex && }
))}
);
};
const ProcessingCard = ({ outer, inner, isNested, log }) => (
Current Iteration
x
{typeof outer === 'string' ? `"${outer}"` : outer}
{isNested && (
)}
);
const CodeColorizer = ({ code }) => {
const parts = code.split(/(\[|\]|for | in | if |"|'|\(|\))/g);
return (
{parts.map((part, i) => {
if (part === 'for ' || part === ' in ' || part === ' if ') return {part} ;
if (part === '"' || part === "'") return {part} ;
if (part === '[' || part === ']') return {part} ;
if (part.match(/^[0-9]+$/)) return {part} ;
if (part.trim() === 'x' || part.trim() === 'fruit') return {part} ;
if (part.trim() === 'y') return {part} ;
return {part} ;
})}
);
};
export default App;