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 */}

List Comprehension Visualizer

Python's syntax can be terse. This tool expands the one-line syntax into visual steps to help your students understand the logic flow, especially for nested loops.

{/* 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 }) => ( ); // --- 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 */}

{title}

{description}

{/* Controls */}
{/* Code Block */}
Python
{/* Visualization Area */}
{/* LEFT: Inputs */}
{isNested && (
)}
{/* CENTER: Processing Stage */}
{currentStep ? (
) : ( stepIndex === -1 ? (

Click Next to step through

) : (

Finished!

) )}
{/* 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 && (
y
{inner}
)}

{log}

); 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;