Add interactive checkboxes that must all be checked to proceed
- Checklist items in instruction nodes now render as clickable checkboxes - All items must be checked before auto-advancing to next step - "I did it" button is hidden when node has interactive checklist - Update checklist text to "I wrote the new fractions down" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
efc98b8d4d
commit
74da416ab4
|
|
@ -2,19 +2,29 @@
|
|||
|
||||
import type { ParsedNodeContent } from '@/lib/flowcharts/schema'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { vstack } from '../../../styled-system/patterns'
|
||||
import { vstack, hstack } from '../../../styled-system/patterns'
|
||||
|
||||
interface FlowchartNodeContentProps {
|
||||
content: ParsedNodeContent
|
||||
/** Whether to show in compact mode */
|
||||
compact?: boolean
|
||||
/** For interactive checklists: which items are checked (by index) */
|
||||
checkedItems?: Set<number>
|
||||
/** Callback when a checklist item is toggled */
|
||||
onChecklistToggle?: (index: number) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders parsed node content with proper formatting.
|
||||
* Handles title, body, examples, warnings, and checklists.
|
||||
*/
|
||||
export function FlowchartNodeContent({ content, compact = false }: FlowchartNodeContentProps) {
|
||||
export function FlowchartNodeContent({
|
||||
content,
|
||||
compact = false,
|
||||
checkedItems,
|
||||
onChecklistToggle,
|
||||
}: FlowchartNodeContentProps) {
|
||||
const isInteractiveChecklist = checkedItems !== undefined && onChecklistToggle !== undefined
|
||||
return (
|
||||
<div
|
||||
data-testid="node-content"
|
||||
|
|
@ -92,30 +102,81 @@ export function FlowchartNodeContent({ content, compact = false }: FlowchartNode
|
|||
|
||||
{/* Checklist */}
|
||||
{content.checklist && content.checklist.length > 0 && (
|
||||
<ul
|
||||
<div
|
||||
data-testid="node-content-checklist"
|
||||
data-item-count={content.checklist.length}
|
||||
data-interactive={isInteractiveChecklist}
|
||||
className={css({
|
||||
listStyle: 'none',
|
||||
padding: '3',
|
||||
backgroundColor: { base: 'green.50', _dark: 'green.900' },
|
||||
borderRadius: 'md',
|
||||
})}
|
||||
>
|
||||
{content.checklist.map((item, i) => (
|
||||
<li
|
||||
key={i}
|
||||
data-testid={`checklist-item-${i}`}
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: { base: 'green.800', _dark: 'green.200' },
|
||||
marginBottom: '1',
|
||||
})}
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{content.checklist.map((item, i) => {
|
||||
// Strip the ☐ or ☑ prefix from the item text (we'll use real checkboxes)
|
||||
const itemText = item.replace(/^[☐☑]\s*/, '')
|
||||
const isChecked = checkedItems?.has(i) ?? false
|
||||
|
||||
return isInteractiveChecklist ? (
|
||||
<label
|
||||
key={i}
|
||||
data-testid={`checklist-item-${i}`}
|
||||
data-checked={isChecked}
|
||||
className={hstack({
|
||||
gap: '3',
|
||||
cursor: 'pointer',
|
||||
padding: '2',
|
||||
marginBottom: '1',
|
||||
borderRadius: 'md',
|
||||
transition: 'all 0.15s ease-out',
|
||||
backgroundColor: isChecked
|
||||
? { base: 'green.200', _dark: 'green.800' }
|
||||
: 'transparent',
|
||||
_hover: {
|
||||
backgroundColor: isChecked
|
||||
? { base: 'green.200', _dark: 'green.800' }
|
||||
: { base: 'green.100', _dark: 'green.800/50' },
|
||||
},
|
||||
})}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={() => onChecklistToggle?.(i)}
|
||||
className={css({
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
accentColor: 'green',
|
||||
cursor: 'pointer',
|
||||
})}
|
||||
/>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: { base: 'green.800', _dark: 'green.200' },
|
||||
textDecoration: isChecked ? 'line-through' : 'none',
|
||||
opacity: isChecked ? 0.8 : 1,
|
||||
})}
|
||||
>
|
||||
{itemText}
|
||||
</span>
|
||||
</label>
|
||||
) : (
|
||||
<div
|
||||
key={i}
|
||||
data-testid={`checklist-item-${i}`}
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: { base: 'green.800', _dark: 'green.200' },
|
||||
marginBottom: '1',
|
||||
paddingLeft: '2',
|
||||
})}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useCallback, useMemo } from 'react'
|
||||
import { useState, useCallback, useMemo, useEffect } from 'react'
|
||||
import type {
|
||||
ExecutableFlowchart,
|
||||
FlowchartState,
|
||||
|
|
@ -84,6 +84,8 @@ export function FlowchartWalker({
|
|||
const [wrongDecision, setWrongDecision] = useState<WrongDecisionState | null>(null)
|
||||
// History stack for back navigation (stores full state snapshots)
|
||||
const [stateHistory, setStateHistory] = useState<FlowchartState[]>([])
|
||||
// Track checked checklist items for the current node
|
||||
const [checkedItems, setCheckedItems] = useState<Set<number>>(new Set())
|
||||
|
||||
// Current node
|
||||
const currentNode = useMemo(
|
||||
|
|
@ -91,6 +93,18 @@ export function FlowchartWalker({
|
|||
[flowchart.nodes, state.currentNode]
|
||||
)
|
||||
|
||||
// Check if current node has an interactive checklist
|
||||
const currentChecklist = currentNode?.content?.checklist
|
||||
const hasInteractiveChecklist =
|
||||
currentNode?.definition.type === 'instruction' &&
|
||||
currentChecklist &&
|
||||
currentChecklist.length > 0
|
||||
|
||||
// Reset checked items when node changes
|
||||
useEffect(() => {
|
||||
setCheckedItems(new Set())
|
||||
}, [state.currentNode])
|
||||
|
||||
// Problem display
|
||||
const problemDisplay = formatProblemDisplay(flowchart, state.problem)
|
||||
|
||||
|
|
@ -290,6 +304,31 @@ export function FlowchartWalker({
|
|||
setPhase({ type: 'awaitingCheckpoint' })
|
||||
}, [])
|
||||
|
||||
const handleChecklistToggle = useCallback(
|
||||
(index: number) => {
|
||||
setCheckedItems((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(index)) {
|
||||
next.delete(index)
|
||||
} else {
|
||||
next.add(index)
|
||||
}
|
||||
|
||||
// Check if all items are now checked - if so, auto-advance
|
||||
const totalItems = currentChecklist?.length ?? 0
|
||||
if (next.size === totalItems && totalItems > 0) {
|
||||
// Small delay so the user sees the final checkbox check
|
||||
setTimeout(() => {
|
||||
advanceToNext()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
return next
|
||||
})
|
||||
},
|
||||
[currentChecklist, advanceToNext]
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Determine what to show based on node type and phase
|
||||
// =============================================================================
|
||||
|
|
@ -301,6 +340,10 @@ export function FlowchartWalker({
|
|||
|
||||
switch (def.type) {
|
||||
case 'instruction':
|
||||
// If there's an interactive checklist, don't show the button - checking all items advances
|
||||
if (hasInteractiveChecklist) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<button
|
||||
data-testid="instruction-advance-button"
|
||||
|
|
@ -717,7 +760,13 @@ export function FlowchartWalker({
|
|||
borderColor: { base: 'gray.200', _dark: 'gray.700' },
|
||||
})}
|
||||
>
|
||||
{currentNode && <FlowchartNodeContent content={currentNode.content} />}
|
||||
{currentNode && (
|
||||
<FlowchartNodeContent
|
||||
content={currentNode.content}
|
||||
checkedItems={hasInteractiveChecklist ? checkedItems : undefined}
|
||||
onChecklistToggle={hasInteractiveChecklist ? handleChecklistToggle : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Interaction area */}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ flowchart TB
|
|||
CONV1C --> READY2(("👍"))
|
||||
STEP2 -->|"NO"| STEP3["<b>CROSS MULTIPLY BOTTOMS</b><br/>──────────────────<br/>New bottom = left × right"]
|
||||
STEP3 --> STEP3B["<b>CONVERT BOTH FRACTIONS</b><br/>────────────────────<br/>For EACH fraction:<br/>What × old bottom = LCD?"] --> READY3(("👍"))
|
||||
READY1 --> CHECK1["<b>✅ READY CHECK</b><br/>──────────────<br/>☐ Both bottoms are<br/> the SAME number<br/>☐ I wrote the fractions down"]
|
||||
READY1 --> CHECK1["<b>✅ READY CHECK</b><br/>──────────────<br/>☐ Both bottoms are<br/> the SAME number<br/>☐ I wrote the new fractions down"]
|
||||
READY2 --> CHECK1
|
||||
READY3 --> CHECK1
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue