Add visual debug mode and documentation for flowchart system
- Add DebugMermaidDiagram component to visualize flowchart with current node highlighted in debug mode - Add DebugStepTimeline for undo/redo navigation through flowchart steps - Add rawMermaid field to ExecutableFlowchart schema for debug rendering - Add comprehensive README.md documenting the flowchart walker system - Add JSDoc comments to parser.ts, loader.ts, definitions/index.ts - Add flowchart section to .claude/CLAUDE.md - Add PageWithNav hamburger menu to flowchart page - Fix MathDisplay to show implicit coefficient of 1 (1x → x) - Install mermaid dependency for flowchart visualization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f67b39f315
commit
5ebd3008f6
|
|
@ -1382,6 +1382,48 @@ If you find yourself:
|
|||
3. [ ] Am I using `mutation.isPending` instead of manual loading state?
|
||||
4. [ ] Am I NOT using `router.refresh()` for cache updates?
|
||||
|
||||
## Flowchart Walker System
|
||||
|
||||
When working on the interactive flowchart walker system, refer to:
|
||||
|
||||
- **[`src/lib/flowcharts/README.md`](../src/lib/flowcharts/README.md)** - Complete system documentation
|
||||
- Architecture overview (JSON definitions + Mermaid content)
|
||||
- Where to find files for each flowchart
|
||||
- Node types and their behavior
|
||||
- Data flow and key functions
|
||||
- Adding new flowcharts
|
||||
|
||||
**CRITICAL: Finding Mermaid Content**
|
||||
|
||||
Mermaid content is **NOT always in separate `.mmd` files!** Many flowcharts embed their mermaid content directly in `definitions/index.ts`.
|
||||
|
||||
| Flowchart ID | JSON Definition | Mermaid Content |
|
||||
|--------------|-----------------|-----------------|
|
||||
| `subtraction-regrouping` | `definitions/subtraction-regrouping.flow.json` | `definitions/subtraction-regrouping-flowchart.mmd` |
|
||||
| `fraction-add-sub` | `definitions/fraction-add-sub.flow.json` | **EMBEDDED** in `definitions/index.ts` as `FRACTION_MERMAID` |
|
||||
| `linear-equations` | `definitions/linear-equations.flow.json` | **EMBEDDED** in `definitions/index.ts` as `LINEAR_EQUATIONS_MERMAID` |
|
||||
|
||||
**To find node content for a flowchart:**
|
||||
1. **First check `definitions/index.ts`** - search for the node ID (e.g., `READY1`)
|
||||
2. If not embedded, check the `.mmd` file referenced in the JSON's `mermaidFile` field
|
||||
|
||||
**Key Files:**
|
||||
|
||||
- `src/lib/flowcharts/definitions/index.ts` - **Registry + EMBEDDED MERMAID CONTENT**
|
||||
- `src/lib/flowcharts/definitions/*.flow.json` - JSON behavior definitions
|
||||
- `src/lib/flowcharts/loader.ts` - Merges JSON + Mermaid into ExecutableFlowchart
|
||||
- `src/lib/flowcharts/parser.ts` - Parses Mermaid content into nodes/edges/phases
|
||||
- `src/lib/flowcharts/evaluator.ts` - Expression evaluation engine
|
||||
- `src/components/flowchart/FlowchartWalker.tsx` - Main UI component
|
||||
|
||||
**Two-File Architecture:**
|
||||
|
||||
Each flowchart has two parts:
|
||||
1. **JSON definition** (`.flow.json`): Node types, validation logic, variables, constraints
|
||||
2. **Mermaid content** (`.mmd` or embedded): Visual presentation, node text, phases
|
||||
|
||||
The loader merges these into an `ExecutableFlowchart` at runtime.
|
||||
|
||||
## Daily Practice System
|
||||
|
||||
When working on the curriculum-based daily practice system, refer to:
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@
|
|||
"lib0": "^0.2.114",
|
||||
"lucide-react": "^0.294.0",
|
||||
"make-plural": "^7.4.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "^14.2.32",
|
||||
"next-auth": "5.0.0-beta.29",
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import type { ExecutableFlowchart, ProblemValue } from '@/lib/flowcharts/schema'
|
|||
import { loadFlowchart } from '@/lib/flowcharts/loader'
|
||||
import { getFlowchart } from '@/lib/flowcharts/definitions'
|
||||
import { FlowchartWalker, FlowchartProblemInput } from '@/components/flowchart'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { css } from '../../../../styled-system/css'
|
||||
import { vstack, hstack } from '../../../../styled-system/patterns'
|
||||
import { vstack } from '../../../../styled-system/patterns'
|
||||
|
||||
type PageState =
|
||||
| { type: 'loading' }
|
||||
|
|
@ -101,11 +102,8 @@ export default function FlowchartPage() {
|
|||
router.push('/flowchart')
|
||||
}, [router])
|
||||
|
||||
// Render based on state
|
||||
return (
|
||||
<div className={vstack({ gap: '4', padding: '4', minHeight: '100vh' })}>
|
||||
{/* Header */}
|
||||
<header className={hstack({ width: '100%', justifyContent: 'flex-start' })}>
|
||||
// Nav slot content - Back to flowcharts link
|
||||
const navSlot = (
|
||||
<Link
|
||||
href="/flowchart"
|
||||
className={css({
|
||||
|
|
@ -117,8 +115,12 @@ export default function FlowchartPage() {
|
|||
>
|
||||
← Back to flowcharts
|
||||
</Link>
|
||||
</header>
|
||||
)
|
||||
|
||||
// Render based on state
|
||||
return (
|
||||
<PageWithNav navSlot={navSlot}>
|
||||
<div className={vstack({ gap: '4', padding: '4', minHeight: '100vh' })}>
|
||||
{/* Main content */}
|
||||
<main
|
||||
className={css({
|
||||
|
|
@ -195,5 +197,6 @@ export default function FlowchartPage() {
|
|||
)}
|
||||
</main>
|
||||
</div>
|
||||
</PageWithNav>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ export function AnimatedMathDisplay({
|
|||
}: AnimatedMathDisplayProps) {
|
||||
const prevExpressionRef = useRef<string>(expression)
|
||||
|
||||
const [layers, setLayers] = useState<
|
||||
Array<{ expression: string; opacity: number; id: number }>
|
||||
>([{ expression, opacity: 1, id: 0 }])
|
||||
const [layers, setLayers] = useState<Array<{ expression: string; opacity: number; id: number }>>([
|
||||
{ expression, opacity: 1, id: 0 },
|
||||
])
|
||||
|
||||
const idCounter = useRef(1)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { css } from '../../../styled-system/css'
|
||||
|
||||
interface DebugMermaidDiagramProps {
|
||||
/** Raw mermaid content */
|
||||
mermaidContent: string
|
||||
/** Current node ID to highlight */
|
||||
currentNodeId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* DebugMermaidDiagram - Renders a mermaid flowchart with the current node highlighted.
|
||||
*
|
||||
* Only rendered when visual debug mode is enabled.
|
||||
* Uses mermaid.js to render the flowchart SVG with custom styling for the current node.
|
||||
*/
|
||||
export function DebugMermaidDiagram({ mermaidContent, currentNodeId }: DebugMermaidDiagramProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
|
||||
async function renderDiagram() {
|
||||
if (!containerRef.current) return
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Dynamic import to avoid SSR issues
|
||||
const mermaid = (await import('mermaid')).default
|
||||
|
||||
// Initialize mermaid with custom config
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
securityLevel: 'loose',
|
||||
flowchart: {
|
||||
useMaxWidth: true,
|
||||
htmlLabels: true,
|
||||
curve: 'basis',
|
||||
},
|
||||
})
|
||||
|
||||
// Add style definition to highlight the current node
|
||||
// We append this to the mermaid content
|
||||
const highlightStyle = `
|
||||
style ${currentNodeId} fill:#fbbf24,stroke:#d97706,stroke-width:4px,color:#000
|
||||
`
|
||||
// Insert the highlight style before the last closing style or at the end
|
||||
const contentWithHighlight = mermaidContent + '\n' + highlightStyle
|
||||
|
||||
// Generate unique ID for this render
|
||||
const id = `mermaid-debug-${Date.now()}`
|
||||
|
||||
// Render the diagram
|
||||
const { svg } = await mermaid.render(id, contentWithHighlight)
|
||||
|
||||
if (mounted && containerRef.current) {
|
||||
containerRef.current.innerHTML = svg
|
||||
|
||||
// Make the SVG responsive
|
||||
const svgElement = containerRef.current.querySelector('svg')
|
||||
if (svgElement) {
|
||||
svgElement.style.maxWidth = '100%'
|
||||
svgElement.style.height = 'auto'
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Mermaid render error:', err)
|
||||
if (mounted) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to render diagram')
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderDiagram()
|
||||
|
||||
return () => {
|
||||
mounted = false
|
||||
}
|
||||
}, [mermaidContent, currentNodeId])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
padding: '4',
|
||||
backgroundColor: { base: 'red.50', _dark: 'red.900/30' },
|
||||
borderRadius: 'lg',
|
||||
color: { base: 'red.700', _dark: 'red.300' },
|
||||
fontSize: 'sm',
|
||||
})}
|
||||
>
|
||||
Failed to render flowchart: {error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="debug-mermaid-diagram"
|
||||
className={css({
|
||||
padding: '4',
|
||||
backgroundColor: { base: 'white', _dark: 'gray.800' },
|
||||
borderRadius: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: { base: 'gray.200', _dark: 'gray.700' },
|
||||
overflow: 'auto',
|
||||
maxHeight: '400px',
|
||||
})}
|
||||
>
|
||||
{isLoading && (
|
||||
<div
|
||||
className={css({
|
||||
textAlign: 'center',
|
||||
padding: '4',
|
||||
color: { base: 'gray.500', _dark: 'gray.400' },
|
||||
fontSize: 'sm',
|
||||
})}
|
||||
>
|
||||
Loading flowchart...
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={css({
|
||||
display: isLoading ? 'none' : 'block',
|
||||
'& svg': {
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
'use client'
|
||||
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { hstack } from '../../../styled-system/patterns'
|
||||
import type { FlowchartState } from '@/lib/flowcharts/schema'
|
||||
|
||||
interface TimelineStep {
|
||||
state: FlowchartState
|
||||
nodeTitle: string
|
||||
}
|
||||
|
||||
interface DebugStepTimelineProps {
|
||||
/** All states in the timeline (history + current + redo) */
|
||||
steps: TimelineStep[]
|
||||
/** Index of current step */
|
||||
currentIndex: number
|
||||
/** Navigate to specific step */
|
||||
onNavigate: (index: number) => void
|
||||
/** Can go back */
|
||||
canGoBack: boolean
|
||||
/** Can go forward (redo stack has items) */
|
||||
canGoForward: boolean
|
||||
/** Can skip (not at terminal) */
|
||||
canSkip: boolean
|
||||
onBack: () => void
|
||||
onForward: () => void
|
||||
/** Skip/auto-advance through current node */
|
||||
onSkip: () => void
|
||||
/** Whether auto-advance is paused */
|
||||
autoAdvancePaused: boolean
|
||||
/** Toggle auto-advance pause */
|
||||
onToggleAutoAdvance: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* DebugStepTimeline - A horizontal timeline showing all steps when visual debug is enabled.
|
||||
*
|
||||
* Shows:
|
||||
* - Past steps (green) - from stateHistory
|
||||
* - Current step (blue, highlighted)
|
||||
* - Future steps (gray) - from redoStack
|
||||
*
|
||||
* Users can click any step to jump directly to it.
|
||||
*/
|
||||
export function DebugStepTimeline({
|
||||
steps,
|
||||
currentIndex,
|
||||
onNavigate,
|
||||
canGoBack,
|
||||
canGoForward,
|
||||
canSkip,
|
||||
onBack,
|
||||
onForward,
|
||||
onSkip,
|
||||
autoAdvancePaused,
|
||||
onToggleAutoAdvance,
|
||||
}: DebugStepTimelineProps) {
|
||||
if (steps.length === 0) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="debug-step-timeline"
|
||||
data-current-index={currentIndex}
|
||||
data-step-count={steps.length}
|
||||
className={css({
|
||||
padding: '3',
|
||||
backgroundColor: { base: 'purple.50', _dark: 'purple.900/30' },
|
||||
borderRadius: 'lg',
|
||||
border: '2px dashed',
|
||||
borderColor: { base: 'purple.300', _dark: 'purple.700' },
|
||||
})}
|
||||
>
|
||||
{/* Header with nav buttons */}
|
||||
<div
|
||||
className={hstack({
|
||||
gap: '3',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '2',
|
||||
})}
|
||||
>
|
||||
{/* Back button */}
|
||||
<button
|
||||
data-testid="debug-back-button"
|
||||
onClick={onBack}
|
||||
disabled={!canGoBack}
|
||||
className={css({
|
||||
padding: '1 2',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: { base: 'purple.300', _dark: 'purple.600' },
|
||||
backgroundColor: { base: 'white', _dark: 'purple.900' },
|
||||
color: canGoBack
|
||||
? { base: 'purple.700', _dark: 'purple.300' }
|
||||
: { base: 'gray.400', _dark: 'gray.600' },
|
||||
cursor: canGoBack ? 'pointer' : 'not-allowed',
|
||||
opacity: canGoBack ? 1 : 0.5,
|
||||
transition: 'all 0.15s',
|
||||
_hover: canGoBack
|
||||
? {
|
||||
backgroundColor: { base: 'purple.100', _dark: 'purple.800' },
|
||||
}
|
||||
: {},
|
||||
})}
|
||||
>
|
||||
← Prev
|
||||
</button>
|
||||
|
||||
{/* Title and auto-advance toggle */}
|
||||
<div className={hstack({ gap: '3', alignItems: 'center' })}>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'semibold',
|
||||
color: { base: 'purple.700', _dark: 'purple.300' },
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 'wide',
|
||||
})}
|
||||
>
|
||||
Debug ({currentIndex + 1}/{steps.length})
|
||||
</span>
|
||||
|
||||
{/* Auto-advance toggle */}
|
||||
<button
|
||||
data-testid="debug-auto-advance-toggle"
|
||||
onClick={onToggleAutoAdvance}
|
||||
title={autoAdvancePaused ? 'Auto-advance is paused' : 'Auto-advance is enabled'}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1',
|
||||
padding: '1 2',
|
||||
fontSize: '2xs',
|
||||
fontWeight: 'medium',
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
borderColor: autoAdvancePaused
|
||||
? { base: 'orange.400', _dark: 'orange.600' }
|
||||
: { base: 'gray.300', _dark: 'gray.600' },
|
||||
backgroundColor: autoAdvancePaused
|
||||
? { base: 'orange.100', _dark: 'orange.900/50' }
|
||||
: { base: 'gray.100', _dark: 'gray.800' },
|
||||
color: autoAdvancePaused
|
||||
? { base: 'orange.700', _dark: 'orange.300' }
|
||||
: { base: 'gray.600', _dark: 'gray.400' },
|
||||
_hover: {
|
||||
backgroundColor: autoAdvancePaused
|
||||
? { base: 'orange.200', _dark: 'orange.800/50' }
|
||||
: { base: 'gray.200', _dark: 'gray.700' },
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span>{autoAdvancePaused ? '⏸' : '▶'}</span>
|
||||
<span>Auto</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<div className={hstack({ gap: '2' })}>
|
||||
{/* Forward button (redo) */}
|
||||
<button
|
||||
data-testid="debug-forward-button"
|
||||
onClick={onForward}
|
||||
disabled={!canGoForward}
|
||||
title="Redo - go forward to previously visited step"
|
||||
className={css({
|
||||
padding: '1 2',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: { base: 'purple.300', _dark: 'purple.600' },
|
||||
backgroundColor: { base: 'white', _dark: 'purple.900' },
|
||||
color: canGoForward
|
||||
? { base: 'purple.700', _dark: 'purple.300' }
|
||||
: { base: 'gray.400', _dark: 'gray.600' },
|
||||
cursor: canGoForward ? 'pointer' : 'not-allowed',
|
||||
opacity: canGoForward ? 1 : 0.5,
|
||||
transition: 'all 0.15s',
|
||||
_hover: canGoForward
|
||||
? {
|
||||
backgroundColor: { base: 'purple.100', _dark: 'purple.800' },
|
||||
}
|
||||
: {},
|
||||
})}
|
||||
>
|
||||
Redo
|
||||
</button>
|
||||
|
||||
{/* Skip button (auto-advance) */}
|
||||
<button
|
||||
data-testid="debug-skip-button"
|
||||
onClick={onSkip}
|
||||
disabled={!canSkip}
|
||||
title="Skip - auto-answer and advance to next step"
|
||||
className={css({
|
||||
padding: '1 2',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'semibold',
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: { base: 'green.400', _dark: 'green.600' },
|
||||
backgroundColor: canSkip
|
||||
? { base: 'green.500', _dark: 'green.600' }
|
||||
: { base: 'gray.200', _dark: 'gray.700' },
|
||||
color: canSkip ? 'white' : { base: 'gray.400', _dark: 'gray.600' },
|
||||
cursor: canSkip ? 'pointer' : 'not-allowed',
|
||||
opacity: canSkip ? 1 : 0.5,
|
||||
transition: 'all 0.15s',
|
||||
_hover: canSkip
|
||||
? {
|
||||
backgroundColor: { base: 'green.600', _dark: 'green.500' },
|
||||
}
|
||||
: {},
|
||||
})}
|
||||
>
|
||||
Skip →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step timeline */}
|
||||
<div
|
||||
data-testid="debug-timeline-steps"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '1',
|
||||
overflowX: 'auto',
|
||||
paddingY: '2',
|
||||
scrollbarWidth: 'thin',
|
||||
})}
|
||||
>
|
||||
{steps.map((step, idx) => {
|
||||
const isPast = idx < currentIndex
|
||||
const isCurrent = idx === currentIndex
|
||||
const isFuture = idx > currentIndex
|
||||
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
data-testid={`debug-step-${idx}`}
|
||||
data-step-status={isCurrent ? 'current' : isPast ? 'past' : 'future'}
|
||||
onClick={() => onNavigate(idx)}
|
||||
title={`Step ${idx + 1}: ${step.nodeTitle}`}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1',
|
||||
padding: '1 2',
|
||||
borderRadius: 'md',
|
||||
border: '2px solid',
|
||||
cursor: isCurrent ? 'default' : 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
flexShrink: 0,
|
||||
maxWidth: '150px',
|
||||
|
||||
// Colors based on status
|
||||
backgroundColor: isCurrent
|
||||
? { base: 'blue.100', _dark: 'blue.800' }
|
||||
: isPast
|
||||
? { base: 'green.50', _dark: 'green.900/50' }
|
||||
: { base: 'gray.100', _dark: 'gray.800' },
|
||||
borderColor: isCurrent
|
||||
? { base: 'blue.500', _dark: 'blue.400' }
|
||||
: isPast
|
||||
? { base: 'green.300', _dark: 'green.700' }
|
||||
: { base: 'gray.300', _dark: 'gray.600' },
|
||||
color: isCurrent
|
||||
? { base: 'blue.800', _dark: 'blue.200' }
|
||||
: isPast
|
||||
? { base: 'green.700', _dark: 'green.300' }
|
||||
: { base: 'gray.500', _dark: 'gray.400' },
|
||||
|
||||
// Hover states (only for non-current)
|
||||
_hover: isCurrent
|
||||
? {}
|
||||
: {
|
||||
backgroundColor: isPast
|
||||
? { base: 'green.100', _dark: 'green.800' }
|
||||
: { base: 'gray.200', _dark: 'gray.700' },
|
||||
borderColor: isPast
|
||||
? { base: 'green.400', _dark: 'green.600' }
|
||||
: { base: 'gray.400', _dark: 'gray.500' },
|
||||
transform: 'translateY(-1px)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{/* Step number / icon */}
|
||||
<span
|
||||
className={css({
|
||||
fontSize: '2xs',
|
||||
fontWeight: 'bold',
|
||||
})}
|
||||
>
|
||||
{isPast ? '✓' : isCurrent ? '📍' : '○'}
|
||||
</span>
|
||||
|
||||
{/* Step title (truncated) */}
|
||||
<span
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
fontWeight: isCurrent ? 'semibold' : 'medium',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
})}
|
||||
>
|
||||
{step.nodeTitle.length > 12 ? `${step.nodeTitle.slice(0, 10)}…` : step.nodeTitle}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
DecisionNode,
|
||||
CheckpointNode,
|
||||
} from '@/lib/flowcharts/schema'
|
||||
import { useVisualDebugSafe } from '@/contexts/VisualDebugContext'
|
||||
import {
|
||||
initializeState,
|
||||
getNextNode,
|
||||
|
|
@ -28,6 +29,8 @@ import { FlowchartDecision } from './FlowchartDecision'
|
|||
import { FlowchartCheckpoint } from './FlowchartCheckpoint'
|
||||
import { FlowchartPhaseRail } from './FlowchartPhaseRail'
|
||||
import { MathDisplay } from './MathDisplay'
|
||||
import { DebugStepTimeline } from './DebugStepTimeline'
|
||||
import { DebugMermaidDiagram } from './DebugMermaidDiagram'
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
|
|
@ -86,13 +89,20 @@ export function FlowchartWalker({
|
|||
const [wrongDecision, setWrongDecision] = useState<WrongDecisionState | null>(null)
|
||||
// History stack for back navigation (stores full state snapshots)
|
||||
const [stateHistory, setStateHistory] = useState<FlowchartState[]>([])
|
||||
// Redo stack for forward navigation (when user goes back)
|
||||
const [redoStack, setRedoStack] = useState<FlowchartState[]>([])
|
||||
// Track checked checklist items for the current node
|
||||
const [checkedItems, setCheckedItems] = useState<Set<number>>(new Set())
|
||||
// Debug mode: pause auto-advance to inspect nodes
|
||||
const [autoAdvancePaused, setAutoAdvancePaused] = useState(false)
|
||||
// Track browser history depth for this walker session
|
||||
const historyDepthRef = useRef(0)
|
||||
// Flag to prevent double-handling when we programmatically go back
|
||||
const isNavigatingBackRef = useRef(false)
|
||||
|
||||
// Visual debug mode
|
||||
const { isVisualDebugEnabled } = useVisualDebugSafe()
|
||||
|
||||
// Current node
|
||||
const currentNode = useMemo(
|
||||
() => flowchart.nodes[state.currentNode],
|
||||
|
|
@ -106,6 +116,39 @@ export function FlowchartWalker({
|
|||
currentChecklist &&
|
||||
currentChecklist.length > 0
|
||||
|
||||
// Build timeline steps for visual debug mode
|
||||
const timelineSteps = useMemo(() => {
|
||||
if (!isVisualDebugEnabled) return []
|
||||
|
||||
const steps: Array<{ state: FlowchartState; nodeTitle: string }> = []
|
||||
|
||||
// Add states from history (past steps)
|
||||
for (const s of stateHistory) {
|
||||
const node = flowchart.nodes[s.currentNode]
|
||||
steps.push({
|
||||
state: s,
|
||||
nodeTitle: node?.content?.title || s.currentNode,
|
||||
})
|
||||
}
|
||||
|
||||
// Add current state
|
||||
steps.push({
|
||||
state,
|
||||
nodeTitle: currentNode?.content?.title || state.currentNode,
|
||||
})
|
||||
|
||||
// Add states from redo stack (future steps) - reversed so oldest redo is first
|
||||
for (const s of redoStack.slice().reverse()) {
|
||||
const node = flowchart.nodes[s.currentNode]
|
||||
steps.push({
|
||||
state: s,
|
||||
nodeTitle: node?.content?.title || s.currentNode,
|
||||
})
|
||||
}
|
||||
|
||||
return steps
|
||||
}, [isVisualDebugEnabled, stateHistory, state, redoStack, flowchart.nodes, currentNode])
|
||||
|
||||
// Reset checked items when node changes
|
||||
useEffect(() => {
|
||||
setCheckedItems(new Set())
|
||||
|
|
@ -119,6 +162,12 @@ export function FlowchartWalker({
|
|||
historyDepthRef.current--
|
||||
isNavigatingBackRef.current = true
|
||||
|
||||
// Push current state to redo stack before going back
|
||||
setState((currentState) => {
|
||||
setRedoStack((prev) => [...prev, currentState])
|
||||
return currentState
|
||||
})
|
||||
|
||||
const previousState = stateHistory[stateHistory.length - 1]
|
||||
setStateHistory((prev) => prev.slice(0, -1))
|
||||
setState(previousState)
|
||||
|
|
@ -171,6 +220,9 @@ export function FlowchartWalker({
|
|||
// Save current state to history before advancing
|
||||
setStateHistory((prev) => [...prev, state])
|
||||
|
||||
// Clear redo stack - user has made a new choice, branching invalidates redo
|
||||
setRedoStack([])
|
||||
|
||||
// Push browser history entry so back button works
|
||||
historyDepthRef.current++
|
||||
window.history.pushState({ flowchartStep: historyDepthRef.current }, '')
|
||||
|
|
@ -194,13 +246,16 @@ export function FlowchartWalker({
|
|||
|
||||
// Check if new node is terminal
|
||||
if (isTerminal(flowchart, nextNodeId)) {
|
||||
// If auto-advance is paused, don't auto-complete - let user inspect the terminal node
|
||||
if (!autoAdvancePaused) {
|
||||
setTimeout(() => {
|
||||
setPhase({ type: 'complete' })
|
||||
onComplete?.(newState)
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
},
|
||||
[flowchart, state, currentNode, onComplete]
|
||||
[flowchart, state, currentNode, onComplete, autoAdvancePaused]
|
||||
)
|
||||
|
||||
// Go back to the previous step (uses browser history so back button stays in sync)
|
||||
|
|
@ -215,6 +270,140 @@ export function FlowchartWalker({
|
|||
window.history.back()
|
||||
}, [stateHistory, onChangeProblem])
|
||||
|
||||
// Go forward to the next step (from redo stack) - for visual debug mode
|
||||
const goForward = useCallback(() => {
|
||||
if (redoStack.length === 0) return
|
||||
|
||||
// Push current state to history
|
||||
setStateHistory((prev) => [...prev, state])
|
||||
|
||||
// Pop next state from redo stack
|
||||
const nextState = redoStack[redoStack.length - 1]
|
||||
setRedoStack((prev) => prev.slice(0, -1))
|
||||
setState(nextState)
|
||||
setPhase({ type: 'showingNode' })
|
||||
setWrongAttempts(0)
|
||||
setWrongDecision(null)
|
||||
setCheckedItems(new Set())
|
||||
|
||||
historyDepthRef.current++
|
||||
window.history.pushState({ flowchartStep: historyDepthRef.current }, '')
|
||||
}, [redoStack, state])
|
||||
|
||||
// Navigate to a specific step in the full timeline (for visual debug mode)
|
||||
// The timeline is: stateHistory + [current] + redoStack (reversed)
|
||||
const navigateToHistoryStep = useCallback(
|
||||
(targetIndex: number) => {
|
||||
// Build full timeline: stateHistory + [current] + redoStack (reversed)
|
||||
const fullTimeline = [...stateHistory, state, ...redoStack.slice().reverse()]
|
||||
const currentIndex = stateHistory.length
|
||||
|
||||
if (targetIndex === currentIndex) return // Already there
|
||||
if (targetIndex < 0 || targetIndex >= fullTimeline.length) return
|
||||
|
||||
const targetState = fullTimeline[targetIndex]
|
||||
if (!targetState) return
|
||||
|
||||
// Calculate new stateHistory (everything before target)
|
||||
const newHistory = fullTimeline.slice(0, targetIndex)
|
||||
// Calculate new redoStack (everything after target, reversed back)
|
||||
const newRedo = fullTimeline.slice(targetIndex + 1).reverse()
|
||||
|
||||
setStateHistory(newHistory)
|
||||
setRedoStack(newRedo)
|
||||
setState(targetState)
|
||||
setPhase({ type: 'showingNode' })
|
||||
setWrongAttempts(0)
|
||||
setWrongDecision(null)
|
||||
setCheckedItems(new Set())
|
||||
|
||||
// Update browser history depth to match
|
||||
historyDepthRef.current = targetIndex
|
||||
window.history.replaceState({ flowchartStep: historyDepthRef.current }, '')
|
||||
},
|
||||
[stateHistory, state, redoStack]
|
||||
)
|
||||
|
||||
// Debug skip - auto-advance through the current node (for visual debug mode)
|
||||
// For decision nodes: picks the first option
|
||||
// For checkpoint nodes: computes and submits the correct answer
|
||||
// For instruction nodes: just advances
|
||||
const debugSkipStep = useCallback(() => {
|
||||
if (!currentNode) return
|
||||
|
||||
const def = currentNode.definition
|
||||
|
||||
switch (def.type) {
|
||||
case 'instruction':
|
||||
case 'milestone':
|
||||
// Just advance
|
||||
advanceToNext()
|
||||
break
|
||||
|
||||
case 'decision': {
|
||||
// Pick the first option
|
||||
const decisionDef = def as DecisionNode
|
||||
if (decisionDef.options.length > 0) {
|
||||
const firstOption = decisionDef.options[0]
|
||||
advanceToNext(firstOption.value, firstOption.value, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'checkpoint': {
|
||||
// Compute the correct answer and submit it
|
||||
const checkpointDef = def as CheckpointNode
|
||||
try {
|
||||
const context = createContextFromState(state)
|
||||
|
||||
if (checkpointDef.inputType === 'two-numbers' && Array.isArray(checkpointDef.expected)) {
|
||||
// Two-number checkpoint
|
||||
const twoNumberAnswer: [number, number] = [
|
||||
evaluate(checkpointDef.expected[0], context) as number,
|
||||
evaluate(checkpointDef.expected[1], context) as number,
|
||||
]
|
||||
const newState = applyStateUpdate(
|
||||
state,
|
||||
state.currentNode,
|
||||
flowchart,
|
||||
twoNumberAnswer as unknown as ProblemValue
|
||||
)
|
||||
setState(newState)
|
||||
advanceToNext(undefined, twoNumberAnswer as unknown as ProblemValue, true)
|
||||
} else if (typeof checkpointDef.expected === 'string') {
|
||||
// Single value checkpoint - expected is a string expression
|
||||
const correctAnswer = evaluate(checkpointDef.expected, context) as number
|
||||
const newState = applyStateUpdate(state, state.currentNode, flowchart, correctAnswer)
|
||||
setState(newState)
|
||||
advanceToNext(undefined, correctAnswer, true)
|
||||
} else {
|
||||
// Shouldn't reach here, but just advance if we do
|
||||
advanceToNext()
|
||||
}
|
||||
} catch {
|
||||
// If we can't compute the answer, just try to advance
|
||||
advanceToNext()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'terminal':
|
||||
// Complete
|
||||
setPhase({ type: 'complete' })
|
||||
onComplete?.(state)
|
||||
break
|
||||
|
||||
default:
|
||||
advanceToNext()
|
||||
}
|
||||
}, [currentNode, state, flowchart, advanceToNext, onComplete])
|
||||
|
||||
// Check if we can skip (not at terminal)
|
||||
const canDebugSkip = useMemo(() => {
|
||||
if (!currentNode) return false
|
||||
return !isTerminal(flowchart, state.currentNode)
|
||||
}, [currentNode, flowchart, state.currentNode])
|
||||
|
||||
// Navigate to a specific step in the working problem history
|
||||
// Clicking on ledger entry i takes you to the state right after that entry was created
|
||||
const navigateToStep = useCallback(
|
||||
|
|
@ -322,10 +511,12 @@ export function FlowchartWalker({
|
|||
expected: result?.expected ?? value,
|
||||
userAnswer: value,
|
||||
})
|
||||
// Auto-advance after short delay
|
||||
// Auto-advance after short delay (unless auto-advance is paused)
|
||||
if (!autoAdvancePaused) {
|
||||
setTimeout(() => {
|
||||
advanceToNext(undefined, value as ProblemValue, true)
|
||||
}, 1000)
|
||||
}
|
||||
} else {
|
||||
// Wrong answer
|
||||
setWrongAttempts((prev) => prev + 1)
|
||||
|
|
@ -338,7 +529,7 @@ export function FlowchartWalker({
|
|||
})
|
||||
}
|
||||
},
|
||||
[flowchart, state, advanceToNext]
|
||||
[flowchart, state, advanceToNext, autoAdvancePaused]
|
||||
)
|
||||
|
||||
const handleChecklistToggle = useCallback(
|
||||
|
|
@ -351,9 +542,9 @@ export function FlowchartWalker({
|
|||
next.add(index)
|
||||
}
|
||||
|
||||
// Check if all items are now checked - if so, auto-advance
|
||||
// Check if all items are now checked - if so, auto-advance (unless auto-advance is paused)
|
||||
const totalItems = currentChecklist?.length ?? 0
|
||||
if (next.size === totalItems && totalItems > 0) {
|
||||
if (next.size === totalItems && totalItems > 0 && !autoAdvancePaused) {
|
||||
// Small delay so the user sees the final checkbox check
|
||||
setTimeout(() => {
|
||||
advanceToNext()
|
||||
|
|
@ -363,7 +554,7 @@ export function FlowchartWalker({
|
|||
return next
|
||||
})
|
||||
},
|
||||
[currentChecklist, advanceToNext]
|
||||
[currentChecklist, advanceToNext, autoAdvancePaused]
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -557,8 +748,10 @@ export function FlowchartWalker({
|
|||
}
|
||||
|
||||
case 'milestone':
|
||||
// Auto-advance milestones
|
||||
// Auto-advance milestones (unless auto-advance is paused)
|
||||
if (!autoAdvancePaused) {
|
||||
setTimeout(() => advanceToNext(), 500)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
data-testid="milestone-display"
|
||||
|
|
@ -723,6 +916,31 @@ export function FlowchartWalker({
|
|||
)}
|
||||
</nav>
|
||||
|
||||
{/* Debug step timeline - only visible when visual debug mode is enabled */}
|
||||
{isVisualDebugEnabled && timelineSteps.length > 0 && (
|
||||
<DebugStepTimeline
|
||||
steps={timelineSteps}
|
||||
currentIndex={stateHistory.length}
|
||||
onNavigate={navigateToHistoryStep}
|
||||
canGoBack={stateHistory.length > 0}
|
||||
canGoForward={redoStack.length > 0}
|
||||
canSkip={canDebugSkip}
|
||||
onBack={goBack}
|
||||
onForward={goForward}
|
||||
onSkip={debugSkipStep}
|
||||
autoAdvancePaused={autoAdvancePaused}
|
||||
onToggleAutoAdvance={() => setAutoAdvancePaused((prev) => !prev)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Debug mermaid diagram - shows flowchart with current node highlighted */}
|
||||
{isVisualDebugEnabled && flowchart.rawMermaid && (
|
||||
<DebugMermaidDiagram
|
||||
mermaidContent={flowchart.rawMermaid}
|
||||
currentNodeId={state.currentNode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Phase rail with flowchart navigation */}
|
||||
<FlowchartPhaseRail flowchart={flowchart} state={state} />
|
||||
|
||||
|
|
@ -883,7 +1101,8 @@ export function FlowchartWalker({
|
|||
},
|
||||
})}
|
||||
>
|
||||
{/* Node content */}
|
||||
{/* Node content - skip for milestone nodes (they show emoji in interaction area) */}
|
||||
{currentNode?.definition.type !== 'milestone' && (
|
||||
<div
|
||||
data-testid="node-content-container"
|
||||
data-node-type={currentNode?.definition.type}
|
||||
|
|
@ -904,6 +1123,7 @@ export function FlowchartWalker({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Interaction area */}
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -111,9 +111,10 @@ function parseExpression(expr: string): Token[] {
|
|||
const termMatch = part.match(/^(-?\d*)([a-zA-Z])$/)
|
||||
if (termMatch) {
|
||||
const [, coef, varName] = termMatch
|
||||
if (coef === '' || coef === '-') {
|
||||
// Just "x" or "-x"
|
||||
tokens.push({ type: 'variable', name: coef === '-' ? `-${varName}` : varName })
|
||||
if (coef === '' || coef === '-' || coef === '1' || coef === '-1') {
|
||||
// Coefficient of 1 is implicit: "1x" → "x", "-1x" → "-x"
|
||||
const isNegative = coef === '-' || coef === '-1'
|
||||
tokens.push({ type: 'variable', name: isNegative ? `-${varName}` : varName })
|
||||
} else {
|
||||
tokens.push({ type: 'term', coefficient: coef, variable: varName })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,231 @@
|
|||
# Flowchart Walker System
|
||||
|
||||
This directory contains the **interactive flowchart walker** system, which guides users through multi-step math procedures using visual flowcharts.
|
||||
|
||||
## Quick Reference: Finding Flowchart Files
|
||||
|
||||
**CRITICAL: Mermaid content is NOT always in separate `.mmd` files!**
|
||||
|
||||
| Flowchart ID | JSON Definition | Mermaid Content Location |
|
||||
|--------------|-----------------|--------------------------|
|
||||
| `subtraction-regrouping` | `definitions/subtraction-regrouping.flow.json` | `definitions/subtraction-regrouping-flowchart.mmd` |
|
||||
| `fraction-add-sub` | `definitions/fraction-add-sub.flow.json` | **EMBEDDED** in `definitions/index.ts` as `FRACTION_MERMAID` |
|
||||
| `linear-equations` | `definitions/linear-equations.flow.json` | **EMBEDDED** in `definitions/index.ts` as `LINEAR_EQUATIONS_MERMAID` |
|
||||
|
||||
**To find mermaid content for a flowchart:**
|
||||
1. First check `definitions/index.ts` - look for `const <NAME>_MERMAID = ...`
|
||||
2. If not embedded, check the `mermaidFile` field in the `.flow.json` file
|
||||
3. The actual `.mmd` file will be in `definitions/` directory
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Each flowchart consists of **two parts**:
|
||||
|
||||
### 1. JSON Definition (`.flow.json`)
|
||||
Defines **behavior and interactivity**:
|
||||
- Node types (instruction, decision, checkpoint, milestone, terminal)
|
||||
- Problem input schema (what values users can enter)
|
||||
- Variables and computed values
|
||||
- Validation rules and expected answers
|
||||
- Working problem transformations
|
||||
|
||||
### 2. Mermaid Content (`.mmd` or embedded in `index.ts`)
|
||||
Defines **visual presentation**:
|
||||
- Node content (title, body text, examples, warnings, checklists)
|
||||
- Visual layout and styling
|
||||
- Phase/subgraph organization
|
||||
- Edge labels and styling
|
||||
|
||||
### Why Two Files?
|
||||
- **Separation of concerns**: Content authors can edit mermaid without touching logic
|
||||
- **Mermaid compatibility**: Flowcharts render in standard Mermaid viewers
|
||||
- **Reusability**: Same definition structure across different topics
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
lib/flowcharts/
|
||||
├── README.md # This file
|
||||
├── schema.ts # TypeScript types for all structures
|
||||
├── parser.ts # Mermaid file parsing (extracts nodes, edges, phases)
|
||||
├── loader.ts # Combines JSON + Mermaid into ExecutableFlowchart
|
||||
├── evaluator.ts # Expression evaluation engine
|
||||
├── constraint-parser.ts # Parses generation constraints
|
||||
├── example-generator-client.ts # Client-side problem generation
|
||||
├── example-generator.worker.ts # Web worker for generation
|
||||
├── index.ts # Public exports
|
||||
├── benchmark.ts # Performance benchmarks
|
||||
├── __tests__/ # Tests
|
||||
└── definitions/ # Flowchart definitions
|
||||
├── index.ts # Registry + EMBEDDED MERMAID CONTENT
|
||||
├── *.flow.json # JSON behavior definitions
|
||||
└── *.mmd # Standalone mermaid files (if not embedded)
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Node Types
|
||||
|
||||
| Type | Purpose | User Interaction |
|
||||
|------|---------|------------------|
|
||||
| `instruction` | Show content | Tap to continue |
|
||||
| `decision` | Ask yes/no or multiple choice | Tap choice button |
|
||||
| `checkpoint` | Validate user answer | Enter value, app validates |
|
||||
| `milestone` | Success marker | Auto-advances (shows emoji briefly) |
|
||||
| `terminal` | End state | Shows completion screen |
|
||||
|
||||
### ExecutableFlowchart
|
||||
|
||||
The final merged structure used at runtime:
|
||||
|
||||
```typescript
|
||||
interface ExecutableFlowchart {
|
||||
definition: FlowchartDefinition // From .flow.json
|
||||
mermaid: ParsedMermaid // Parsed from mermaid content
|
||||
nodes: Record<string, ExecutableNode> // Merged nodes
|
||||
}
|
||||
|
||||
interface ExecutableNode {
|
||||
id: string
|
||||
definition: FlowchartNode // Behavior from JSON
|
||||
content: ParsedNodeContent // Display content from mermaid
|
||||
}
|
||||
```
|
||||
|
||||
### Node Content Parsing
|
||||
|
||||
Mermaid nodes use special formatting parsed by `parser.ts`:
|
||||
|
||||
```mermaid
|
||||
NODE["<b>Title Here</b><br/>───────<br/>Body text line 1<br/>Body text line 2<br/>📝 Example text<br/>⚠️ Warning text"]
|
||||
```
|
||||
|
||||
Parsed into:
|
||||
```typescript
|
||||
{
|
||||
title: "Title Here",
|
||||
body: ["Body text line 1", "Body text line 2"],
|
||||
example: "Example text",
|
||||
warning: "Warning text",
|
||||
checklist: undefined,
|
||||
raw: "..."
|
||||
}
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ .flow.json │ │ Mermaid content │
|
||||
│ (behavior) │ │ (.mmd or embedded) │
|
||||
└─────────┬───────────┘ └─────────┬───────────┘
|
||||
│ │
|
||||
│ loadFlowchart() │
|
||||
└───────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────┐
|
||||
│ ExecutableFlowchart │
|
||||
│ (merged structure) │
|
||||
└───────────┬───────────┘
|
||||
│
|
||||
│ initializeState()
|
||||
▼
|
||||
┌───────────────────────┐
|
||||
│ FlowchartState │
|
||||
│ (runtime state) │
|
||||
└───────────┬───────────┘
|
||||
│
|
||||
│ FlowchartWalker component
|
||||
▼
|
||||
┌───────────────────────┐
|
||||
│ User Interface │
|
||||
└───────────────────────┘
|
||||
```
|
||||
|
||||
## Key Functions
|
||||
|
||||
### `definitions/index.ts`
|
||||
- `getFlowchart(id)` - Get a flowchart by ID (returns definition + mermaid)
|
||||
- `getFlowchartList()` - Get metadata for all flowcharts
|
||||
- `FLOWCHARTS` - Registry mapping IDs to definitions
|
||||
|
||||
### `loader.ts`
|
||||
- `loadFlowchart(definition, mermaid)` - Merge JSON and mermaid into ExecutableFlowchart
|
||||
- `initializeState(flowchart, problemInput)` - Create initial runtime state
|
||||
- `advanceState(state, nextNode, ...)` - Move to next node
|
||||
- `validateCheckpoint(flowchart, node, state, input)` - Check user answer
|
||||
- `isDecisionCorrect(flowchart, node, state, choice)` - Check decision choice
|
||||
- `formatProblemDisplay(flowchart, problem)` - Format problem for display
|
||||
|
||||
### `parser.ts`
|
||||
- `parseMermaidFile(content)` - Parse mermaid into nodes, edges, phases
|
||||
- `parseNodeContent(raw)` - Parse node label into structured content
|
||||
- `getNextNodes(mermaid, nodeId)` - Get successor nodes
|
||||
- `findNodePhase(mermaid, nodeId)` - Find which phase a node belongs to
|
||||
|
||||
### `evaluator.ts`
|
||||
- `evaluate(expression, context)` - Evaluate math/logic expressions
|
||||
- Supports: arithmetic, comparisons, boolean logic, ternary, functions (gcd, lcm, floor, etc.)
|
||||
|
||||
## Adding a New Flowchart
|
||||
|
||||
1. **Create the JSON definition** (`definitions/my-flowchart.flow.json`):
|
||||
```json
|
||||
{
|
||||
"id": "my-flowchart",
|
||||
"title": "My Flowchart",
|
||||
"mermaidFile": "my-flowchart.mmd",
|
||||
"problemInput": { ... },
|
||||
"variables": { ... },
|
||||
"entryNode": "START",
|
||||
"nodes": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
2. **Create the mermaid content** - either:
|
||||
- Standalone file: `definitions/my-flowchart.mmd`
|
||||
- OR embed in `definitions/index.ts` as `const MY_FLOWCHART_MERMAID = \`...\``
|
||||
|
||||
3. **Register in `definitions/index.ts`**:
|
||||
```typescript
|
||||
import myDefinition from './my-flowchart.flow.json'
|
||||
|
||||
export const FLOWCHARTS = {
|
||||
'my-flowchart': {
|
||||
definition: myDefinition as FlowchartDefinition,
|
||||
mermaid: MY_FLOWCHART_MERMAID, // or read from .mmd file
|
||||
meta: { id: 'my-flowchart', title: '...', ... }
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Finding why a node looks wrong
|
||||
|
||||
1. **Find the node ID** - Look at `data-current-node` attribute in browser DevTools
|
||||
2. **Check the mermaid content** - Look in `definitions/index.ts` for embedded mermaid
|
||||
3. **Check the JSON definition** - Look in `.flow.json` for node type and behavior
|
||||
4. **Check the parsed content** - Log `currentNode.content` in FlowchartWalker
|
||||
|
||||
### Common issues
|
||||
|
||||
| Issue | Likely Cause |
|
||||
|-------|--------------|
|
||||
| Node shows only emoji | Mermaid node has no `<b>` title, just emoji like `(("👍"))` |
|
||||
| Content shows twice | Node content rendered by both container and interaction area |
|
||||
| Wrong answer marked correct | Check `expected` expression in checkpoint definition |
|
||||
| Node not advancing | Check `next` field or `edges` in JSON definition |
|
||||
|
||||
## Related Components
|
||||
|
||||
- `components/flowchart/FlowchartWalker.tsx` - Main walker UI component
|
||||
- `components/flowchart/FlowchartNodeContent.tsx` - Renders parsed node content
|
||||
- `components/flowchart/FlowchartDecision.tsx` - Decision button UI
|
||||
- `components/flowchart/FlowchartCheckpoint.tsx` - Checkpoint input UI
|
||||
- `components/flowchart/FlowchartPhaseRail.tsx` - Phase progress indicator
|
||||
- `components/flowchart/DebugStepTimeline.tsx` - Visual debug timeline
|
||||
- `app/flowchart/page.tsx` - Flowchart picker page
|
||||
- `app/flowchart/[flowchartId]/page.tsx` - Flowchart walker page
|
||||
|
|
@ -1,7 +1,36 @@
|
|||
/**
|
||||
* Flowchart Definitions Index
|
||||
* Flowchart Definitions Registry
|
||||
*
|
||||
* Exports all available flowchart definitions with their Mermaid content.
|
||||
* **THIS FILE CONTAINS EMBEDDED MERMAID CONTENT!**
|
||||
*
|
||||
* Each flowchart has two parts:
|
||||
* 1. **JSON definition** (`.flow.json`): Behavior, validation, variables
|
||||
* 2. **Mermaid content**: Visual presentation, node text, phases
|
||||
*
|
||||
* ## Where is the Mermaid content?
|
||||
*
|
||||
* | Flowchart | Mermaid Location |
|
||||
* |-----------|------------------|
|
||||
* | subtraction-regrouping | Embedded below as `SUBTRACTION_MERMAID` |
|
||||
* | fraction-add-sub | Embedded below as `FRACTION_MERMAID` |
|
||||
* | linear-equations | Embedded below as `LINEAR_EQUATIONS_MERMAID` |
|
||||
*
|
||||
* **To find node content**: Search this file for the node ID (e.g., `READY1`) in
|
||||
* the appropriate `*_MERMAID` constant.
|
||||
*
|
||||
* ## Why Embed Mermaid?
|
||||
*
|
||||
* Next.js doesn't support `?raw` imports for loading text files.
|
||||
* Embedding the mermaid content as template strings is the simplest solution.
|
||||
*
|
||||
* ## Adding a New Flowchart
|
||||
*
|
||||
* 1. Create `my-flowchart.flow.json` in this directory
|
||||
* 2. Add `const MY_FLOWCHART_MERMAID = \`...\`` below
|
||||
* 3. Import the JSON and add to `FLOWCHARTS` registry
|
||||
*
|
||||
* @see {@link ../README.md} for complete system documentation
|
||||
* @module flowcharts/definitions
|
||||
*/
|
||||
|
||||
import type { FlowchartDefinition } from '../schema'
|
||||
|
|
@ -9,7 +38,17 @@ import subtractionDefinition from './subtraction-regrouping.flow.json'
|
|||
import fractionDefinition from './fraction-add-sub.flow.json'
|
||||
import linearEquationsDefinition from './linear-equations.flow.json'
|
||||
|
||||
// Mermaid content embedded as strings (since Next.js doesn't support ?raw imports)
|
||||
// =============================================================================
|
||||
// EMBEDDED MERMAID CONTENT
|
||||
// =============================================================================
|
||||
// These constants contain the visual content for each flowchart.
|
||||
// Search for node IDs (e.g., "READY1", "STEP0") to find their content.
|
||||
|
||||
/**
|
||||
* Mermaid content for subtraction-regrouping flowchart.
|
||||
* Nodes: START, COMPARE, HAPPY, SAD, CHECK1, CHECK1B, NEEDIT, SKIP, TENS,
|
||||
* TAKEONE, BREAK, ADDTEN, CHECK2, DOONES, DOTENS, DONE
|
||||
*/
|
||||
const SUBTRACTION_MERMAID = `%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '18px', 'primaryColor': '#e3f2fd', 'primaryTextColor': '#1a1a1a', 'primaryBorderColor': '#90caf9', 'lineColor': '#444444'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 20}}}%%
|
||||
flowchart TB
|
||||
subgraph PHASE1["<b>1. 👀 LOOK</b>"]
|
||||
|
|
@ -68,6 +107,16 @@ flowchart TB
|
|||
style DONE fill:#66bb6a,stroke:#2e7d32,stroke-width:2px
|
||||
`
|
||||
|
||||
/**
|
||||
* Mermaid content for fraction-add-sub flowchart.
|
||||
* Nodes: STEP0, STEP1, READY1, READY2, READY3, STEP2, CONV1A, CONV1B, CONV1C,
|
||||
* STEP3, STEP3B, CHECK1, REMIND, ADDSUB, GOSTEP4, GOSTEP4B, GOSTEP4C,
|
||||
* BORROWCHECK, BORROW, CHECK2, STEP4, SIMPLIFY_Q, SIMPLIFY_HOW,
|
||||
* IMPROPER_Q, MIXED_HOW, CHECK3, DONE
|
||||
*
|
||||
* NOTE: Milestone nodes (READY1, READY2, READY3, GOSTEP4, etc.) only contain
|
||||
* emoji like (("👍")) - they display briefly before auto-advancing.
|
||||
*/
|
||||
const FRACTION_MERMAID = `%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '14px', 'primaryColor': '#e3f2fd', 'primaryTextColor': '#1a1a1a', 'primaryBorderColor': '#90caf9', 'lineColor': '#444444'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 25, 'rankSpacing': 40, 'padding': 15}}}%%
|
||||
flowchart TB
|
||||
subgraph PHASE1["<b>1. 🔍 MAKE THE BOTTOMS MATCH</b>"]
|
||||
|
|
@ -122,6 +171,12 @@ flowchart TB
|
|||
style PHASE3 fill:#e8f5e9,stroke:#388e3c,stroke-width:3px
|
||||
`
|
||||
|
||||
/**
|
||||
* Mermaid content for linear-equations flowchart.
|
||||
* Nodes: INTRO, BALANCE, FIND_OP, STUCK_ADD, STUCK_MUL, CHECK1, GOAL,
|
||||
* HOWSTUCK, ZERO, ONE, MAKEZ, MAKEONE, EX_ADD, EX_MUL, REMIND,
|
||||
* CHECK2, PLUG, MATCH, DONE, RETRY
|
||||
*/
|
||||
const LINEAR_EQUATIONS_MERMAID = `%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '14px', 'primaryColor': '#e3f2fd', 'primaryTextColor': '#1a1a1a', 'primaryBorderColor': '#90caf9', 'lineColor': '#444444'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 25, 'rankSpacing': 40, 'padding': 15}}}%%
|
||||
flowchart TB
|
||||
subgraph PHASE1["<b>1. 🔍 UNDERSTAND THE EQUATION</b>"]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,41 @@
|
|||
/**
|
||||
* Flowchart Loader
|
||||
*
|
||||
* Loads and merges .mmd and .flow.json files into an executable flowchart.
|
||||
* Combines JSON definitions (`.flow.json`) with Mermaid content (`.mmd` or embedded)
|
||||
* to create executable flowcharts, and manages runtime state as users walk through them.
|
||||
*
|
||||
* ## Key Functions
|
||||
*
|
||||
* - {@link loadFlowchart} - Merge JSON definition + Mermaid into ExecutableFlowchart
|
||||
* - {@link initializeState} - Create initial runtime state from problem input
|
||||
* - {@link advanceState} - Move to next node in the flowchart
|
||||
* - {@link validateCheckpoint} - Check if user's answer is correct
|
||||
* - {@link isDecisionCorrect} - Check if user chose the correct option
|
||||
* - {@link formatProblemDisplay} - Format problem values for display
|
||||
*
|
||||
* ## Data Flow
|
||||
*
|
||||
* ```
|
||||
* FlowchartDefinition + Mermaid content
|
||||
* ↓
|
||||
* loadFlowchart()
|
||||
* ↓
|
||||
* ExecutableFlowchart
|
||||
* ↓
|
||||
* initializeState(flowchart, problemInput)
|
||||
* ↓
|
||||
* FlowchartState
|
||||
* ↓
|
||||
* advanceState(), validateCheckpoint(), etc.
|
||||
* ```
|
||||
*
|
||||
* ## Where to Find Mermaid Content
|
||||
*
|
||||
* **IMPORTANT**: Mermaid content is NOT always in separate `.mmd` files!
|
||||
* Check `definitions/index.ts` first - many flowcharts embed their mermaid as constants.
|
||||
*
|
||||
* @see {@link ./README.md} for complete system documentation
|
||||
* @module flowcharts/loader
|
||||
*/
|
||||
|
||||
import type {
|
||||
|
|
@ -32,7 +66,33 @@ import {
|
|||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Load and merge a flowchart definition with its Mermaid content
|
||||
* Load and merge a flowchart definition with its Mermaid content.
|
||||
*
|
||||
* This is the main entry point for creating an executable flowchart.
|
||||
* It combines:
|
||||
* - **JSON definition** (`.flow.json`): Node types, validation logic, variables
|
||||
* - **Mermaid content**: Node display content, phases, visual structure
|
||||
*
|
||||
* ## Node Merging
|
||||
*
|
||||
* For each node ID:
|
||||
* 1. If in JSON definition: uses that node type/behavior
|
||||
* 2. If only in Mermaid: creates default `instruction` node
|
||||
* 3. Content always comes from Mermaid (parsed via `parseNodeContent`)
|
||||
*
|
||||
* ## Common Usage
|
||||
*
|
||||
* ```typescript
|
||||
* import { getFlowchart } from './definitions'
|
||||
* import { loadFlowchart } from './loader'
|
||||
*
|
||||
* const data = getFlowchart('fraction-add-sub')
|
||||
* const flowchart = await loadFlowchart(data.definition, data.mermaid)
|
||||
* ```
|
||||
*
|
||||
* @param definition - The JSON definition from `.flow.json`
|
||||
* @param mermaidContent - The Mermaid content (from `.mmd` file or embedded string)
|
||||
* @returns Promise resolving to executable flowchart ready for FlowchartWalker
|
||||
*/
|
||||
export async function loadFlowchart(
|
||||
definition: FlowchartDefinition,
|
||||
|
|
@ -78,6 +138,7 @@ export async function loadFlowchart(
|
|||
return {
|
||||
definition,
|
||||
mermaid,
|
||||
rawMermaid: mermaidContent,
|
||||
nodes,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,36 @@
|
|||
/**
|
||||
* Mermaid Flowchart Parser
|
||||
*
|
||||
* Extracts structure from .mmd files:
|
||||
* - Node IDs and content
|
||||
* - Edge connections
|
||||
* - Subgraph (phase) definitions
|
||||
* Extracts structure from Mermaid flowchart content (from .mmd files or embedded strings).
|
||||
*
|
||||
* ## Key Concepts
|
||||
*
|
||||
* - **Node content** is stored in Mermaid node labels using special formatting
|
||||
* - **Phases** are Mermaid subgraphs that group related nodes
|
||||
* - **Edges** connect nodes and may have labels
|
||||
*
|
||||
* ## Content Formatting
|
||||
*
|
||||
* Mermaid node labels use HTML-like formatting:
|
||||
* - `<b>...</b>` - Title (extracted separately)
|
||||
* - `<br/>` - Line breaks
|
||||
* - `<i>...</i>` - Italic (used for examples)
|
||||
* - `───────` - Dividers (ignored)
|
||||
* - `📝` - Example marker
|
||||
* - `⚠️` - Warning marker
|
||||
* - `☐` / `☑` - Checklist items
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { parseMermaidFile, parseNodeContent } from './parser'
|
||||
*
|
||||
* const mermaid = parseMermaidFile(mermaidContent)
|
||||
* const nodeContent = mermaid.nodes['START']
|
||||
* const parsed = parseNodeContent(nodeContent)
|
||||
* console.log(parsed.title, parsed.body)
|
||||
* ```
|
||||
*
|
||||
* @module flowcharts/parser
|
||||
*/
|
||||
|
||||
import type { ParsedMermaid, ParsedNodeContent, ParsedEdge } from './schema'
|
||||
|
|
@ -14,14 +40,49 @@ import type { ParsedMermaid, ParsedNodeContent, ParsedEdge } from './schema'
|
|||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Parse the raw content string from a Mermaid node label.
|
||||
* Parse the raw content string from a Mermaid node label into structured content.
|
||||
*
|
||||
* Mermaid node content uses:
|
||||
* - <b>...</b> for bold (title)
|
||||
* - <br/> for line breaks
|
||||
* - <i>...</i> for italic (examples)
|
||||
* - ─────── for dividers
|
||||
* - Emojis throughout
|
||||
* ## Input Format
|
||||
*
|
||||
* Mermaid node content uses HTML-like formatting:
|
||||
* - `<b>...</b>` - Bold text becomes the **title**
|
||||
* - `<br/>` - Line breaks separate content
|
||||
* - `<i>...</i>` - Italic text (treated as examples)
|
||||
* - `───────` - Divider lines (ignored)
|
||||
* - `📝` - Marks example text
|
||||
* - `⚠️` - Marks warning text
|
||||
* - `☐` / `☑` - Checklist items
|
||||
*
|
||||
* ## Output Structure
|
||||
*
|
||||
* ```typescript
|
||||
* {
|
||||
* title: "The title text", // From <b>...</b> or first line
|
||||
* body: ["Line 1", "Line 2"], // Main content lines
|
||||
* example: "Example text", // Lines after 📝 or <i>
|
||||
* warning: "Warning text", // Lines with ⚠️
|
||||
* checklist: ["☐ Item 1"], // Lines with checkboxes
|
||||
* raw: "original content" // Original for fallback
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ## Edge Cases
|
||||
*
|
||||
* - **Emoji-only nodes** (like milestone `(("👍"))`): Title becomes the emoji, body is empty
|
||||
* - **No `<b>` tags**: First line becomes the title
|
||||
* - **Multi-line titles**: `<b>Line 1<br/>Line 2</b>` becomes a single-line title
|
||||
*
|
||||
* @param raw - The raw content string from a Mermaid node label
|
||||
* @returns Parsed and structured node content
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const content = parseNodeContent('<b>Step 1</b><br/>Do this thing<br/>📝 Example: 3 + 4 = 7')
|
||||
* // { title: "Step 1", body: ["Do this thing"], example: "Example: 3 + 4 = 7", ... }
|
||||
*
|
||||
* const emoji = parseNodeContent('👍')
|
||||
* // { title: "👍", body: [], ... }
|
||||
* ```
|
||||
*/
|
||||
export function parseNodeContent(raw: string): ParsedNodeContent {
|
||||
// Decode HTML entities that might be in the content
|
||||
|
|
@ -131,7 +192,43 @@ function stripHtml(str: string): string {
|
|||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Parse a complete Mermaid flowchart file
|
||||
* Parse a complete Mermaid flowchart into nodes, edges, and phases.
|
||||
*
|
||||
* ## What It Extracts
|
||||
*
|
||||
* - **Nodes**: ID → raw content mapping (content is NOT parsed here, use `parseNodeContent`)
|
||||
* - **Edges**: From → To connections with optional labels
|
||||
* - **Phases**: Subgraph groupings with title and contained node IDs
|
||||
*
|
||||
* ## Node Shapes Supported
|
||||
*
|
||||
* - `ID["content"]` - Rectangle
|
||||
* - `ID{"content"}` - Diamond (decision)
|
||||
* - `ID(["content"])` - Stadium (rounded rectangle)
|
||||
* - `ID(("content"))` - Circle (milestones, often emoji-only)
|
||||
*
|
||||
* ## Edge Format
|
||||
*
|
||||
* - `A --> B` - Simple edge
|
||||
* - `A -->|"label"| B` - Edge with label
|
||||
*
|
||||
* @param content - The complete Mermaid flowchart content (from .mmd file or embedded string)
|
||||
* @returns Parsed structure with nodes, edges, and phases
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const mermaid = parseMermaidFile(`
|
||||
* flowchart TB
|
||||
* subgraph PHASE1["Step 1"]
|
||||
* START["<b>Begin</b>"] --> DECISION{"<b>Continue?</b>"}
|
||||
* DECISION -->|"Yes"| DONE(("👍"))
|
||||
* end
|
||||
* `)
|
||||
*
|
||||
* mermaid.nodes['START'] // '<b>Begin</b>'
|
||||
* mermaid.edges[0] // { from: 'START', to: 'DECISION' }
|
||||
* mermaid.phases[0] // { id: 'PHASE1', title: 'Step 1', nodes: ['START', 'DECISION', 'DONE'] }
|
||||
* ```
|
||||
*/
|
||||
export function parseMermaidFile(content: string): ParsedMermaid {
|
||||
const nodes: Record<string, string> = {}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,53 @@
|
|||
/**
|
||||
* Flowchart Walker Schema Types
|
||||
*
|
||||
* Defines the structure of .flow.json companion files that add
|
||||
* interactivity metadata to .mmd Mermaid flowcharts.
|
||||
* This module defines all TypeScript types for the flowchart walker system.
|
||||
* These types correspond to the structure of `.flow.json` files and the
|
||||
* runtime state used during flowchart execution.
|
||||
*
|
||||
* ## Architecture Overview
|
||||
*
|
||||
* ```
|
||||
* FlowchartDefinition (from .flow.json)
|
||||
* ├── problemInput: ProblemInputSchema # User input form definition
|
||||
* ├── variables: Record<string, VariableDefinition> # Computed values
|
||||
* ├── nodes: Record<string, FlowchartNode> # Node behavior definitions
|
||||
* ├── workingProblem?: WorkingProblemConfig # Evolving problem display
|
||||
* └── constraints?: GenerationConstraints # Problem generation rules
|
||||
*
|
||||
* ParsedMermaid (from .mmd or embedded)
|
||||
* ├── nodes: Record<string, string> # Raw node content
|
||||
* ├── edges: ParsedEdge[] # Connections between nodes
|
||||
* └── phases: Phase[] # Subgraph groupings
|
||||
*
|
||||
* ExecutableFlowchart = FlowchartDefinition + ParsedMermaid merged
|
||||
* └── nodes: Record<string, ExecutableNode> # Ready for display
|
||||
*
|
||||
* FlowchartState (runtime)
|
||||
* ├── problem: user input values
|
||||
* ├── computed: calculated variables
|
||||
* ├── currentNode: where we are
|
||||
* └── history: actions taken
|
||||
* ```
|
||||
*
|
||||
* ## Node Types
|
||||
*
|
||||
* | Type | Purpose | User Action |
|
||||
* |------|---------|-------------|
|
||||
* | `instruction` | Show content | Tap to continue |
|
||||
* | `decision` | Yes/No or multiple choice | Tap option |
|
||||
* | `checkpoint` | Validate user answer | Enter value |
|
||||
* | `milestone` | Success marker | Auto-advances |
|
||||
* | `terminal` | End state | Shows completion |
|
||||
*
|
||||
* ## File Locations
|
||||
*
|
||||
* - JSON definitions: `lib/flowcharts/definitions/*.flow.json`
|
||||
* - Mermaid content: `lib/flowcharts/definitions/index.ts` (embedded) or `*.mmd`
|
||||
* - This file: Type definitions only, no runtime logic
|
||||
*
|
||||
* @see {@link ../README.md} for complete system documentation
|
||||
* @module flowcharts/schema
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -434,5 +479,7 @@ export interface ExecutableNode {
|
|||
export interface ExecutableFlowchart {
|
||||
definition: FlowchartDefinition
|
||||
mermaid: ParsedMermaid
|
||||
/** Raw mermaid content string (for debug rendering) */
|
||||
rawMermaid: string
|
||||
nodes: Record<string, ExecutableNode>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,8 +84,7 @@ describe('A/B Performance Benchmark', () => {
|
|||
console.log(` Run ${run + 1}: ${elapsed.toFixed(1)}ms`)
|
||||
}
|
||||
|
||||
const nonMemoizedMean =
|
||||
nonMemoizedTimes.reduce((a, b) => a + b, 0) / nonMemoizedTimes.length
|
||||
const nonMemoizedMean = nonMemoizedTimes.reduce((a, b) => a + b, 0) / nonMemoizedTimes.length
|
||||
|
||||
// ============= MEMOIZED =============
|
||||
console.log(`\n🚀 Testing MEMOIZED...`)
|
||||
|
|
|
|||
|
|
@ -166,7 +166,9 @@ describe('Session Generation Benchmark', () => {
|
|||
console.log(` Per problem: ${(finalTime / 60).toFixed(1)}ms`)
|
||||
console.log(` Cache entries: ${finalStats.size}`)
|
||||
console.log(` Cache hits: ${finalStats.hits.toLocaleString()}`)
|
||||
console.log(` Cache hit rate: ${((finalStats.hits / (finalStats.hits + finalStats.misses)) * 100).toFixed(1)}%`)
|
||||
console.log(
|
||||
` Cache hit rate: ${((finalStats.hits / (finalStats.hits + finalStats.misses)) * 100).toFixed(1)}%`
|
||||
)
|
||||
|
||||
// Phase 2 recommendation
|
||||
console.log(`\n🤔 PHASE 2 RECOMMENDATION:`)
|
||||
|
|
|
|||
|
|
@ -72,9 +72,10 @@ describe('analyzeStepSkills memoization', () => {
|
|||
{ currentValue: 100, term: -1, description: '100-1=99 borrow from hundreds' },
|
||||
]
|
||||
|
||||
it.each(testCases)(
|
||||
'$description produces identical results memoized vs non-memoized',
|
||||
({ currentValue, term }) => {
|
||||
it.each(testCases)('$description produces identical results memoized vs non-memoized', ({
|
||||
currentValue,
|
||||
term,
|
||||
}) => {
|
||||
const newValue = currentValue + term
|
||||
|
||||
// First call (cache miss) - should compute fresh
|
||||
|
|
@ -85,8 +86,7 @@ describe('analyzeStepSkills memoization', () => {
|
|||
|
||||
// Results must be identical
|
||||
expect(memoizedResult).toEqual(directResult)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('repeated calls return identical arrays', () => {
|
||||
// Call multiple times with same inputs
|
||||
|
|
@ -273,7 +273,9 @@ describe('performance benchmark', () => {
|
|||
expect(stats.hits).toBeGreaterThanOrEqual(operations.length * 2)
|
||||
|
||||
// Log for manual verification
|
||||
console.log(`Memoized: ${memoizedTime.toFixed(2)}ms for ${repeatedOperations.length} operations`)
|
||||
console.log(
|
||||
`Memoized: ${memoizedTime.toFixed(2)}ms for ${repeatedOperations.length} operations`
|
||||
)
|
||||
console.log(`Cache stats: ${stats.size} entries, ${stats.hits} hits, ${stats.misses} misses`)
|
||||
console.log(`Hit rate: ${((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(1)}%`)
|
||||
})
|
||||
|
|
|
|||
816
pnpm-lock.yaml
816
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue