feat(worksheets): restore mastery progression UI with 3-way mode selector

Restore the mastery progression worksheet mode that was previously working
but got disconnected during recent development.

Changes:
- Add 'mastery' mode option to ModeSelector (Smart/Manual/Mastery Progression)
- Wire ProgressionModePanel into ConfigPanel's mode selection logic
- Add currentStepId field to WorksheetFormState for tracking progression step
- Mastery mode displays 6-step scaffolding fade progression slider

The mastery progression system uses a 1D slider mapped to a 6-step path
through 3D difficulty space (digit count × regrouping complexity × scaffolding).
Scaffolding cycles back when moving to higher digit counts, following a
pedagogically sound pattern.

Components progressionPath.ts and ProgressionModePanel.tsx already existed
(dated Nov 9) but were not integrated into the UI flow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-10 09:49:30 -06:00
parent 0d66c54991
commit 26a08859d7
3 changed files with 219 additions and 209 deletions

View File

@ -1,44 +1,40 @@
"use client"; 'use client'
import { stack } from "../../../../../../styled-system/patterns"; import { stack } from '../../../../../../styled-system/patterns'
import type { WorksheetFormState } from "../types"; import type { WorksheetFormState } from '../types'
import { defaultAdditionConfig } from "@/app/create/worksheets/config-schemas"; import { defaultAdditionConfig } from '@/app/create/worksheets/config-schemas'
import { ModeSelector } from "./ModeSelector"; import { ModeSelector } from './ModeSelector'
import { StudentNameInput } from "./config-panel/StudentNameInput"; import { StudentNameInput } from './config-panel/StudentNameInput'
import { DigitRangeSection } from "./config-panel/DigitRangeSection"; import { DigitRangeSection } from './config-panel/DigitRangeSection'
import { OperatorSection } from "./config-panel/OperatorSection"; import { OperatorSection } from './config-panel/OperatorSection'
import { ProgressiveDifficultyToggle } from "./config-panel/ProgressiveDifficultyToggle"; import { ProgressiveDifficultyToggle } from './config-panel/ProgressiveDifficultyToggle'
import { SmartModeControls } from "./config-panel/SmartModeControls"; import { SmartModeControls } from './config-panel/SmartModeControls'
import { ManualModeControls } from "./config-panel/ManualModeControls"; import { ManualModeControls } from './config-panel/ManualModeControls'
import { ProgressionModePanel } from './config-panel/ProgressionModePanel'
interface ConfigPanelProps { interface ConfigPanelProps {
formState: WorksheetFormState; formState: WorksheetFormState
onChange: (updates: Partial<WorksheetFormState>) => void; onChange: (updates: Partial<WorksheetFormState>) => void
isDark?: boolean; isDark?: boolean
} }
export function ConfigPanel({ export function ConfigPanel({ formState, onChange, isDark = false }: ConfigPanelProps) {
formState,
onChange,
isDark = false,
}: ConfigPanelProps) {
// Handler for mode switching // Handler for mode switching
const handleModeChange = (newMode: "smart" | "manual") => { const handleModeChange = (newMode: 'smart' | 'manual' | 'mastery') => {
if (formState.mode === newMode) { if (formState.mode === newMode) {
return; // No change needed return // No change needed
} }
if (newMode === "smart") { if (newMode === 'smart') {
// Switching to Smart mode // Switching to Smart mode
// Use current displayRules if available, otherwise default to earlyLearner // Use current displayRules if available, otherwise default to earlyLearner
const displayRules = const displayRules = formState.displayRules ?? defaultAdditionConfig.displayRules
formState.displayRules ?? defaultAdditionConfig.displayRules;
onChange({ onChange({
mode: "smart", mode: 'smart',
displayRules, displayRules,
difficultyProfile: "earlyLearner", difficultyProfile: 'earlyLearner',
} as unknown as Partial<WorksheetFormState>); } as unknown as Partial<WorksheetFormState>)
} else { } else if (newMode === 'manual') {
// Switching to Manual mode // Switching to Manual mode
// Convert current displayRules to boolean flags if available // Convert current displayRules to boolean flags if available
let booleanFlags = { let booleanFlags = {
@ -49,32 +45,38 @@ export function ConfigPanel({
showProblemNumbers: true, showProblemNumbers: true,
showCellBorder: true, showCellBorder: true,
showTenFramesForAll: false, showTenFramesForAll: false,
}; }
if (formState.displayRules) { if (formState.displayRules) {
// Convert 'always' to true, everything else to false // Convert 'always' to true, everything else to false
booleanFlags = { booleanFlags = {
showCarryBoxes: formState.displayRules.carryBoxes === "always", showCarryBoxes: formState.displayRules.carryBoxes === 'always',
showAnswerBoxes: formState.displayRules.answerBoxes === "always", showAnswerBoxes: formState.displayRules.answerBoxes === 'always',
showPlaceValueColors: showPlaceValueColors: formState.displayRules.placeValueColors === 'always',
formState.displayRules.placeValueColors === "always", showTenFrames: formState.displayRules.tenFrames === 'always',
showTenFrames: formState.displayRules.tenFrames === "always", showProblemNumbers: formState.displayRules.problemNumbers === 'always',
showProblemNumbers: showCellBorder: formState.displayRules.cellBorders === 'always',
formState.displayRules.problemNumbers === "always",
showCellBorder: formState.displayRules.cellBorders === "always",
showTenFramesForAll: false, showTenFramesForAll: false,
}; }
} }
onChange({ onChange({
mode: "manual", mode: 'manual',
...booleanFlags, ...booleanFlags,
} as unknown as Partial<WorksheetFormState>); } as unknown as Partial<WorksheetFormState>)
} else {
// Switching to Mastery mode
// Mastery mode uses Smart mode under the hood with skill-based configuration
const displayRules = formState.displayRules ?? defaultAdditionConfig.displayRules
onChange({
mode: 'mastery',
displayRules,
} as unknown as Partial<WorksheetFormState>)
}
} }
};
return ( return (
<div data-component="config-panel" className={stack({ gap: "3" })}> <div data-component="config-panel" className={stack({ gap: '3' })}>
{/* Student Name */} {/* Student Name */}
<StudentNameInput <StudentNameInput
value={formState.name} value={formState.name}
@ -98,7 +100,7 @@ export function ConfigPanel({
{/* Mode Selector */} {/* Mode Selector */}
<ModeSelector <ModeSelector
currentMode={formState.mode ?? "smart"} currentMode={formState.mode ?? 'smart'}
onChange={handleModeChange} onChange={handleModeChange}
isDark={isDark} isDark={isDark}
/> />
@ -111,22 +113,19 @@ export function ConfigPanel({
/> />
{/* Smart Mode Controls */} {/* Smart Mode Controls */}
{(!formState.mode || formState.mode === "smart") && ( {(!formState.mode || formState.mode === 'smart') && (
<SmartModeControls <SmartModeControls formState={formState} onChange={onChange} isDark={isDark} />
formState={formState}
onChange={onChange}
isDark={isDark}
/>
)} )}
{/* Manual Mode Controls */} {/* Manual Mode Controls */}
{formState.mode === "manual" && ( {formState.mode === 'manual' && (
<ManualModeControls <ManualModeControls formState={formState} onChange={onChange} isDark={isDark} />
formState={formState} )}
onChange={onChange}
isDark={isDark} {/* Mastery Mode Controls */}
/> {formState.mode === 'mastery' && (
<ProgressionModePanel formState={formState} onChange={onChange} isDark={isDark} />
)} )}
</div> </div>
); )
} }

View File

@ -1,42 +1,38 @@
"use client"; 'use client'
import { css } from "../../../../../../styled-system/css"; import { css } from '../../../../../../styled-system/css'
interface ModeSelectorProps { interface ModeSelectorProps {
currentMode: "smart" | "manual"; currentMode: 'smart' | 'manual' | 'mastery'
onChange: (mode: "smart" | "manual") => void; onChange: (mode: 'smart' | 'manual' | 'mastery') => void
isDark?: boolean; isDark?: boolean
} }
/** /**
* Mode selector for worksheet generation * Mode selector for worksheet generation
* Allows switching between Smart Difficulty and Manual Control modes * Allows switching between Smart Difficulty, Manual Control, and Mastery Progression modes
*/ */
export function ModeSelector({ export function ModeSelector({ currentMode, onChange, isDark = false }: ModeSelectorProps) {
currentMode,
onChange,
isDark = false,
}: ModeSelectorProps) {
return ( return (
<div <div
data-component="mode-selector" data-component="mode-selector"
className={css({ className={css({
marginBottom: "1.5rem", marginBottom: '1.5rem',
padding: "1rem", padding: '1rem',
backgroundColor: isDark ? "gray.700" : "gray.50", backgroundColor: isDark ? 'gray.700' : 'gray.50',
borderRadius: "8px", borderRadius: '8px',
border: "1px solid", border: '1px solid',
borderColor: isDark ? "gray.600" : "gray.200", borderColor: isDark ? 'gray.600' : 'gray.200',
})} })}
> >
<h3 <h3
className={css({ className={css({
fontSize: "0.875rem", fontSize: '0.875rem',
fontWeight: "600", fontWeight: '600',
color: isDark ? "gray.200" : "gray.700", color: isDark ? 'gray.200' : 'gray.700',
marginBottom: "0.75rem", marginBottom: '0.75rem',
textTransform: "uppercase", textTransform: 'uppercase',
letterSpacing: "0.05em", letterSpacing: '0.05em',
})} })}
> >
Worksheet Mode Worksheet Mode
@ -45,67 +41,52 @@ export function ModeSelector({
<div <div
data-element="mode-buttons" data-element="mode-buttons"
className={css({ className={css({
display: "flex", display: 'grid',
gap: "0.75rem", gridTemplateColumns: 'repeat(3, 1fr)',
gap: '0.75rem',
})} })}
> >
{/* Smart Difficulty Mode Button */} {/* Smart Difficulty Mode Button */}
<button <button
type="button" type="button"
data-action="select-smart-mode" data-action="select-smart-mode"
data-selected={currentMode === "smart"} data-selected={currentMode === 'smart'}
onClick={() => onChange("smart")} onClick={() => onChange('smart')}
className={css({ className={css({
flex: 1, padding: '1rem',
padding: "1rem", borderRadius: '6px',
borderRadius: "6px", border: '2px solid',
border: "2px solid", borderColor: currentMode === 'smart' ? 'blue.500' : isDark ? 'gray.500' : 'gray.300',
borderColor: backgroundColor: currentMode === 'smart' ? 'blue.50' : isDark ? 'gray.600' : 'white',
currentMode === "smart" cursor: 'pointer',
? "blue.500" transition: 'all 0.2s',
: isDark textAlign: 'left',
? "gray.500"
: "gray.300",
backgroundColor:
currentMode === "smart"
? "blue.50"
: isDark
? "gray.600"
: "white",
cursor: "pointer",
transition: "all 0.2s",
textAlign: "left",
_hover: { _hover: {
borderColor: "blue.400", borderColor: 'blue.400',
backgroundColor: "blue.50", backgroundColor: 'blue.50',
}, },
})} })}
> >
<div <div
className={css({ className={css({
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
gap: "0.5rem", gap: '0.5rem',
marginBottom: "0.5rem", marginBottom: '0.5rem',
})} })}
> >
<span <span
className={css({ className={css({
fontSize: "1.25rem", fontSize: '1.25rem',
})} })}
> >
🎯 🎯
</span> </span>
<span <span
className={css({ className={css({
fontSize: "0.875rem", fontSize: '0.875rem',
fontWeight: "600", fontWeight: '600',
color: color: currentMode === 'smart' ? 'blue.700' : isDark ? 'gray.200' : 'gray.700',
currentMode === "smart"
? "blue.700"
: isDark
? "gray.200"
: "gray.700",
})} })}
> >
Smart Difficulty Smart Difficulty
@ -113,18 +94,12 @@ export function ModeSelector({
</div> </div>
<p <p
className={css({ className={css({
fontSize: "0.75rem", fontSize: '0.75rem',
color: color: currentMode === 'smart' ? 'blue.600' : isDark ? 'gray.400' : 'gray.600',
currentMode === "smart" lineHeight: '1.4',
? "blue.600"
: isDark
? "gray.400"
: "gray.600",
lineHeight: "1.4",
})} })}
> >
Research-backed progressive difficulty with adaptive scaffolding per Research-backed progressive difficulty with adaptive scaffolding per problem
problem
</p> </p>
</button> </button>
@ -132,59 +107,43 @@ export function ModeSelector({
<button <button
type="button" type="button"
data-action="select-manual-mode" data-action="select-manual-mode"
data-selected={currentMode === "manual"} data-selected={currentMode === 'manual'}
onClick={() => onChange("manual")} onClick={() => onChange('manual')}
className={css({ className={css({
flex: 1, padding: '1rem',
padding: "1rem", borderRadius: '6px',
borderRadius: "6px", border: '2px solid',
border: "2px solid", borderColor: currentMode === 'manual' ? 'blue.500' : isDark ? 'gray.500' : 'gray.300',
borderColor: backgroundColor: currentMode === 'manual' ? 'blue.50' : isDark ? 'gray.600' : 'white',
currentMode === "manual" cursor: 'pointer',
? "blue.500" transition: 'all 0.2s',
: isDark textAlign: 'left',
? "gray.500"
: "gray.300",
backgroundColor:
currentMode === "manual"
? "blue.50"
: isDark
? "gray.600"
: "white",
cursor: "pointer",
transition: "all 0.2s",
textAlign: "left",
_hover: { _hover: {
borderColor: "blue.400", borderColor: 'blue.400',
backgroundColor: "blue.50", backgroundColor: 'blue.50',
}, },
})} })}
> >
<div <div
className={css({ className={css({
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
gap: "0.5rem", gap: '0.5rem',
marginBottom: "0.5rem", marginBottom: '0.5rem',
})} })}
> >
<span <span
className={css({ className={css({
fontSize: "1.25rem", fontSize: '1.25rem',
})} })}
> >
🎛 🎛
</span> </span>
<span <span
className={css({ className={css({
fontSize: "0.875rem", fontSize: '0.875rem',
fontWeight: "600", fontWeight: '600',
color: color: currentMode === 'manual' ? 'blue.700' : isDark ? 'gray.200' : 'gray.700',
currentMode === "manual"
? "blue.700"
: isDark
? "gray.200"
: "gray.700",
})} })}
> >
Manual Control Manual Control
@ -192,21 +151,72 @@ export function ModeSelector({
</div> </div>
<p <p
className={css({ className={css({
fontSize: "0.75rem", fontSize: '0.75rem',
color: color: currentMode === 'manual' ? 'blue.600' : isDark ? 'gray.400' : 'gray.600',
currentMode === "manual" lineHeight: '1.4',
? "blue.600"
: isDark
? "gray.400"
: "gray.600",
lineHeight: "1.4",
})} })}
> >
Full control over display options with uniform scaffolding across Full control over display options with uniform scaffolding across all problems
all problems </p>
</button>
{/* Mastery Progression Mode Button */}
<button
type="button"
data-action="select-mastery-mode"
data-selected={currentMode === 'mastery'}
onClick={() => onChange('mastery')}
className={css({
padding: '1rem',
borderRadius: '6px',
border: '2px solid',
borderColor: currentMode === 'mastery' ? 'blue.500' : isDark ? 'gray.500' : 'gray.300',
backgroundColor: currentMode === 'mastery' ? 'blue.50' : isDark ? 'gray.600' : 'white',
cursor: 'pointer',
transition: 'all 0.2s',
textAlign: 'left',
_hover: {
borderColor: 'blue.400',
backgroundColor: 'blue.50',
},
})}
>
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
marginBottom: '0.5rem',
})}
>
<span
className={css({
fontSize: '1.25rem',
})}
>
🎓
</span>
<span
className={css({
fontSize: '0.875rem',
fontWeight: '600',
color: currentMode === 'mastery' ? 'blue.700' : isDark ? 'gray.200' : 'gray.700',
})}
>
Mastery Progression
</span>
</div>
<p
className={css({
fontSize: '0.75rem',
color: currentMode === 'mastery' ? 'blue.600' : isDark ? 'gray.400' : 'gray.600',
lineHeight: '1.4',
})}
>
Skill-based progression with automatic review mixing for pedagogical practice
</p> </p>
</button> </button>
</div> </div>
</div> </div>
); )
} }

View File

@ -4,7 +4,7 @@ import type {
AdditionConfigV4, AdditionConfigV4,
AdditionConfigV4Smart, AdditionConfigV4Smart,
AdditionConfigV4Manual, AdditionConfigV4Manual,
} from "../config-schemas"; } from '../config-schemas'
/** /**
* Complete, validated configuration for worksheet generation * Complete, validated configuration for worksheet generation
@ -18,25 +18,25 @@ import type {
*/ */
export type WorksheetConfig = AdditionConfigV4 & { export type WorksheetConfig = AdditionConfigV4 & {
// Problem set - DERIVED state // Problem set - DERIVED state
total: number; // total = problemsPerPage * pages total: number // total = problemsPerPage * pages
rows: number; // rows = (problemsPerPage / cols) * pages rows: number // rows = (problemsPerPage / cols) * pages
// Personalization // Personalization
date: string; date: string
seed: number; seed: number
// Layout // Layout
page: { page: {
wIn: number; wIn: number
hIn: number; hIn: number
}; }
margins: { margins: {
left: number; left: number
right: number; right: number
top: number; top: number
bottom: number; bottom: number
}; }
}; }
/** /**
* Partial form state - user may be editing, fields optional * Partial form state - user may be editing, fields optional
@ -52,55 +52,56 @@ export type WorksheetConfig = AdditionConfigV4 & {
* This type is intentionally permissive during form editing to allow fields from * This type is intentionally permissive during form editing to allow fields from
* both modes to exist temporarily. Validation will enforce mode consistency. * both modes to exist temporarily. Validation will enforce mode consistency.
*/ */
export type WorksheetFormState = Partial< export type WorksheetFormState = Partial<Omit<AdditionConfigV4Smart, 'version'>> &
Omit<AdditionConfigV4Smart, "version"> Partial<Omit<AdditionConfigV4Manual, 'version'>> & {
> &
Partial<Omit<AdditionConfigV4Manual, "version">> & {
// DERIVED state (calculated from primary state) // DERIVED state (calculated from primary state)
rows?: number; rows?: number
total?: number; total?: number
date?: string; date?: string
seed?: number; seed?: number
};
// Mastery progression mode
currentStepId?: string // Current step in progression path
}
/** /**
* Worksheet operator type * Worksheet operator type
*/ */
export type WorksheetOperator = "addition" | "subtraction" | "mixed"; export type WorksheetOperator = 'addition' | 'subtraction' | 'mixed'
/** /**
* A single addition problem * A single addition problem
*/ */
export interface AdditionProblem { export interface AdditionProblem {
a: number; a: number
b: number; b: number
operator: "+"; operator: '+'
} }
/** /**
* A single subtraction problem * A single subtraction problem
*/ */
export interface SubtractionProblem { export interface SubtractionProblem {
minuend: number; minuend: number
subtrahend: number; subtrahend: number
operator: ""; // Proper minus sign (U+2212) operator: '' // Proper minus sign (U+2212)
} }
/** /**
* Unified problem type (addition or subtraction) * Unified problem type (addition or subtraction)
*/ */
export type WorksheetProblem = AdditionProblem | SubtractionProblem; export type WorksheetProblem = AdditionProblem | SubtractionProblem
/** /**
* Validation result * Validation result
*/ */
export interface ValidationResult { export interface ValidationResult {
isValid: boolean; isValid: boolean
config?: WorksheetConfig; config?: WorksheetConfig
errors?: string[]; errors?: string[]
} }
/** /**
* Problem category for difficulty control * Problem category for difficulty control
*/ */
export type ProblemCategory = "non" | "onesOnly" | "both"; export type ProblemCategory = 'non' | 'onesOnly' | 'both'