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 type { WorksheetFormState } from "../types";
import { defaultAdditionConfig } from "@/app/create/worksheets/config-schemas";
import { ModeSelector } from "./ModeSelector";
import { StudentNameInput } from "./config-panel/StudentNameInput";
import { DigitRangeSection } from "./config-panel/DigitRangeSection";
import { OperatorSection } from "./config-panel/OperatorSection";
import { ProgressiveDifficultyToggle } from "./config-panel/ProgressiveDifficultyToggle";
import { SmartModeControls } from "./config-panel/SmartModeControls";
import { ManualModeControls } from "./config-panel/ManualModeControls";
import { stack } from '../../../../../../styled-system/patterns'
import type { WorksheetFormState } from '../types'
import { defaultAdditionConfig } from '@/app/create/worksheets/config-schemas'
import { ModeSelector } from './ModeSelector'
import { StudentNameInput } from './config-panel/StudentNameInput'
import { DigitRangeSection } from './config-panel/DigitRangeSection'
import { OperatorSection } from './config-panel/OperatorSection'
import { ProgressiveDifficultyToggle } from './config-panel/ProgressiveDifficultyToggle'
import { SmartModeControls } from './config-panel/SmartModeControls'
import { ManualModeControls } from './config-panel/ManualModeControls'
import { ProgressionModePanel } from './config-panel/ProgressionModePanel'
interface ConfigPanelProps {
formState: WorksheetFormState;
onChange: (updates: Partial<WorksheetFormState>) => void;
isDark?: boolean;
formState: WorksheetFormState
onChange: (updates: Partial<WorksheetFormState>) => void
isDark?: boolean
}
export function ConfigPanel({
formState,
onChange,
isDark = false,
}: ConfigPanelProps) {
export function ConfigPanel({ formState, onChange, isDark = false }: ConfigPanelProps) {
// Handler for mode switching
const handleModeChange = (newMode: "smart" | "manual") => {
const handleModeChange = (newMode: 'smart' | 'manual' | 'mastery') => {
if (formState.mode === newMode) {
return; // No change needed
return // No change needed
}
if (newMode === "smart") {
if (newMode === 'smart') {
// Switching to Smart mode
// Use current displayRules if available, otherwise default to earlyLearner
const displayRules =
formState.displayRules ?? defaultAdditionConfig.displayRules;
const displayRules = formState.displayRules ?? defaultAdditionConfig.displayRules
onChange({
mode: "smart",
mode: 'smart',
displayRules,
difficultyProfile: "earlyLearner",
} as unknown as Partial<WorksheetFormState>);
} else {
difficultyProfile: 'earlyLearner',
} as unknown as Partial<WorksheetFormState>)
} else if (newMode === 'manual') {
// Switching to Manual mode
// Convert current displayRules to boolean flags if available
let booleanFlags = {
@ -49,32 +45,38 @@ export function ConfigPanel({
showProblemNumbers: true,
showCellBorder: true,
showTenFramesForAll: false,
};
}
if (formState.displayRules) {
// Convert 'always' to true, everything else to false
booleanFlags = {
showCarryBoxes: formState.displayRules.carryBoxes === "always",
showAnswerBoxes: formState.displayRules.answerBoxes === "always",
showPlaceValueColors:
formState.displayRules.placeValueColors === "always",
showTenFrames: formState.displayRules.tenFrames === "always",
showProblemNumbers:
formState.displayRules.problemNumbers === "always",
showCellBorder: formState.displayRules.cellBorders === "always",
showCarryBoxes: formState.displayRules.carryBoxes === 'always',
showAnswerBoxes: formState.displayRules.answerBoxes === 'always',
showPlaceValueColors: formState.displayRules.placeValueColors === 'always',
showTenFrames: formState.displayRules.tenFrames === 'always',
showProblemNumbers: formState.displayRules.problemNumbers === 'always',
showCellBorder: formState.displayRules.cellBorders === 'always',
showTenFramesForAll: false,
};
}
}
onChange({
mode: "manual",
mode: 'manual',
...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 (
<div data-component="config-panel" className={stack({ gap: "3" })}>
<div data-component="config-panel" className={stack({ gap: '3' })}>
{/* Student Name */}
<StudentNameInput
value={formState.name}
@ -98,7 +100,7 @@ export function ConfigPanel({
{/* Mode Selector */}
<ModeSelector
currentMode={formState.mode ?? "smart"}
currentMode={formState.mode ?? 'smart'}
onChange={handleModeChange}
isDark={isDark}
/>
@ -111,22 +113,19 @@ export function ConfigPanel({
/>
{/* Smart Mode Controls */}
{(!formState.mode || formState.mode === "smart") && (
<SmartModeControls
formState={formState}
onChange={onChange}
isDark={isDark}
/>
{(!formState.mode || formState.mode === 'smart') && (
<SmartModeControls formState={formState} onChange={onChange} isDark={isDark} />
)}
{/* Manual Mode Controls */}
{formState.mode === "manual" && (
<ManualModeControls
formState={formState}
onChange={onChange}
isDark={isDark}
/>
{formState.mode === 'manual' && (
<ManualModeControls formState={formState} onChange={onChange} isDark={isDark} />
)}
{/* Mastery Mode Controls */}
{formState.mode === 'mastery' && (
<ProgressionModePanel formState={formState} onChange={onChange} isDark={isDark} />
)}
</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 {
currentMode: "smart" | "manual";
onChange: (mode: "smart" | "manual") => void;
isDark?: boolean;
currentMode: 'smart' | 'manual' | 'mastery'
onChange: (mode: 'smart' | 'manual' | 'mastery') => void
isDark?: boolean
}
/**
* 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({
currentMode,
onChange,
isDark = false,
}: ModeSelectorProps) {
export function ModeSelector({ currentMode, onChange, isDark = false }: ModeSelectorProps) {
return (
<div
data-component="mode-selector"
className={css({
marginBottom: "1.5rem",
padding: "1rem",
backgroundColor: isDark ? "gray.700" : "gray.50",
borderRadius: "8px",
border: "1px solid",
borderColor: isDark ? "gray.600" : "gray.200",
marginBottom: '1.5rem',
padding: '1rem',
backgroundColor: isDark ? 'gray.700' : 'gray.50',
borderRadius: '8px',
border: '1px solid',
borderColor: isDark ? 'gray.600' : 'gray.200',
})}
>
<h3
className={css({
fontSize: "0.875rem",
fontWeight: "600",
color: isDark ? "gray.200" : "gray.700",
marginBottom: "0.75rem",
textTransform: "uppercase",
letterSpacing: "0.05em",
fontSize: '0.875rem',
fontWeight: '600',
color: isDark ? 'gray.200' : 'gray.700',
marginBottom: '0.75rem',
textTransform: 'uppercase',
letterSpacing: '0.05em',
})}
>
Worksheet Mode
@ -45,67 +41,52 @@ export function ModeSelector({
<div
data-element="mode-buttons"
className={css({
display: "flex",
gap: "0.75rem",
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '0.75rem',
})}
>
{/* Smart Difficulty Mode Button */}
<button
type="button"
data-action="select-smart-mode"
data-selected={currentMode === "smart"}
onClick={() => onChange("smart")}
data-selected={currentMode === 'smart'}
onClick={() => onChange('smart')}
className={css({
flex: 1,
padding: "1rem",
borderRadius: "6px",
border: "2px solid",
borderColor:
currentMode === "smart"
? "blue.500"
: isDark
? "gray.500"
: "gray.300",
backgroundColor:
currentMode === "smart"
? "blue.50"
: isDark
? "gray.600"
: "white",
cursor: "pointer",
transition: "all 0.2s",
textAlign: "left",
padding: '1rem',
borderRadius: '6px',
border: '2px solid',
borderColor: currentMode === 'smart' ? 'blue.500' : isDark ? 'gray.500' : 'gray.300',
backgroundColor: currentMode === 'smart' ? 'blue.50' : isDark ? 'gray.600' : 'white',
cursor: 'pointer',
transition: 'all 0.2s',
textAlign: 'left',
_hover: {
borderColor: "blue.400",
backgroundColor: "blue.50",
borderColor: 'blue.400',
backgroundColor: 'blue.50',
},
})}
>
<div
className={css({
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginBottom: "0.5rem",
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
marginBottom: '0.5rem',
})}
>
<span
className={css({
fontSize: "1.25rem",
fontSize: '1.25rem',
})}
>
🎯
</span>
<span
className={css({
fontSize: "0.875rem",
fontWeight: "600",
color:
currentMode === "smart"
? "blue.700"
: isDark
? "gray.200"
: "gray.700",
fontSize: '0.875rem',
fontWeight: '600',
color: currentMode === 'smart' ? 'blue.700' : isDark ? 'gray.200' : 'gray.700',
})}
>
Smart Difficulty
@ -113,18 +94,12 @@ export function ModeSelector({
</div>
<p
className={css({
fontSize: "0.75rem",
color:
currentMode === "smart"
? "blue.600"
: isDark
? "gray.400"
: "gray.600",
lineHeight: "1.4",
fontSize: '0.75rem',
color: currentMode === 'smart' ? 'blue.600' : isDark ? 'gray.400' : 'gray.600',
lineHeight: '1.4',
})}
>
Research-backed progressive difficulty with adaptive scaffolding per
problem
Research-backed progressive difficulty with adaptive scaffolding per problem
</p>
</button>
@ -132,59 +107,43 @@ export function ModeSelector({
<button
type="button"
data-action="select-manual-mode"
data-selected={currentMode === "manual"}
onClick={() => onChange("manual")}
data-selected={currentMode === 'manual'}
onClick={() => onChange('manual')}
className={css({
flex: 1,
padding: "1rem",
borderRadius: "6px",
border: "2px solid",
borderColor:
currentMode === "manual"
? "blue.500"
: isDark
? "gray.500"
: "gray.300",
backgroundColor:
currentMode === "manual"
? "blue.50"
: isDark
? "gray.600"
: "white",
cursor: "pointer",
transition: "all 0.2s",
textAlign: "left",
padding: '1rem',
borderRadius: '6px',
border: '2px solid',
borderColor: currentMode === 'manual' ? 'blue.500' : isDark ? 'gray.500' : 'gray.300',
backgroundColor: currentMode === 'manual' ? 'blue.50' : isDark ? 'gray.600' : 'white',
cursor: 'pointer',
transition: 'all 0.2s',
textAlign: 'left',
_hover: {
borderColor: "blue.400",
backgroundColor: "blue.50",
borderColor: 'blue.400',
backgroundColor: 'blue.50',
},
})}
>
<div
className={css({
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginBottom: "0.5rem",
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
marginBottom: '0.5rem',
})}
>
<span
className={css({
fontSize: "1.25rem",
fontSize: '1.25rem',
})}
>
🎛
</span>
<span
className={css({
fontSize: "0.875rem",
fontWeight: "600",
color:
currentMode === "manual"
? "blue.700"
: isDark
? "gray.200"
: "gray.700",
fontSize: '0.875rem',
fontWeight: '600',
color: currentMode === 'manual' ? 'blue.700' : isDark ? 'gray.200' : 'gray.700',
})}
>
Manual Control
@ -192,21 +151,72 @@ export function ModeSelector({
</div>
<p
className={css({
fontSize: "0.75rem",
color:
currentMode === "manual"
? "blue.600"
: isDark
? "gray.400"
: "gray.600",
lineHeight: "1.4",
fontSize: '0.75rem',
color: currentMode === 'manual' ? 'blue.600' : isDark ? 'gray.400' : 'gray.600',
lineHeight: '1.4',
})}
>
Full control over display options with uniform scaffolding across
all problems
Full control over display options with uniform scaffolding across 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>
</button>
</div>
</div>
);
)
}

View File

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