feat: add 'auto' option for scaffolding to defer to mastery progression
Add new 'auto' display rule value that allows users to defer scaffolding decisions to the mastery progression's skill recommendations while still being able to override individual settings.
**Changes:**
1. **Type System (config-schemas.ts)**
- Add 'auto' to displayRuleValues constant
- Update all V4 schemas (Smart, Manual, Mastery) to include 'auto'
- Document 'auto' as "defer to mastery progression's skill recommendation"
2. **UI (RuleThermometer.tsx, ScaffoldingTab.tsx)**
- Add 'Auto' option to scaffolding thermometer controls
- Add 'Auto' preset button (only shown in mastery mode)
- Style Auto button with green theme to indicate recommended default
3. **Validation Logic (validation.ts)**
- Add mergeDisplayRulesWithAuto() helper function
- Resolve 'auto' values to skill's recommendedScaffolding
- Support both single-operator and mixed-operator mastery modes
- Add debug logging to trace auto resolution
**How it works:**
- When value is 'auto' → use skill's recommendation
- When value is anything else → use user's manual override
- When value is undefined → use skill's recommendation (fallback)
**Example:**
```typescript
// User sets answerBoxes to 'auto', carryBoxes to 'never'
userRules = {
answerBoxes: 'auto', // → Resolves to skill.recommendedScaffolding.answerBoxes
carryBoxes: 'never' // → Manual override, uses 'never'
}
```
This resolves the conflict between mastery progression recommendations and manual scaffolding controls, allowing users to defer to skill expertise while retaining the ability to override specific settings.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
77ea70bff5
commit
a945a620c4
|
|
@ -12,6 +12,7 @@ export interface RuleThermometerProps {
|
|||
}
|
||||
|
||||
const RULE_OPTIONS: Array<{ value: RuleMode; label: string; short: string }> = [
|
||||
{ value: 'auto', label: 'Auto (Use Mastery Progression)', short: 'Auto' },
|
||||
{ value: 'always', label: 'Always', short: 'Always' },
|
||||
{ value: 'whenRegrouping', label: 'When Regrouping', short: 'Regroup' },
|
||||
{ value: 'whenMultipleRegroups', label: 'Multiple Regroups', short: '2+' },
|
||||
|
|
|
|||
|
|
@ -69,6 +69,48 @@ export function ScaffoldingTab() {
|
|||
Quick Presets
|
||||
</div>
|
||||
<div className={css({ display: 'flex', gap: '1.5' })}>
|
||||
{/* Auto preset - defer to mastery progression (only show in mastery mode) */}
|
||||
{formState.mode === 'mastery' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const newDisplayRules = {
|
||||
...displayRules,
|
||||
carryBoxes: 'auto' as const,
|
||||
answerBoxes: 'auto' as const,
|
||||
placeValueColors: 'auto' as const,
|
||||
tenFrames: 'auto' as const,
|
||||
borrowNotation: 'auto' as const,
|
||||
borrowingHints: 'auto' as const,
|
||||
}
|
||||
// In mastery+mixed mode, update operator-specific rules too
|
||||
if (isMasteryMixed) {
|
||||
onChange({
|
||||
displayRules: newDisplayRules,
|
||||
additionDisplayRules: newDisplayRules,
|
||||
subtractionDisplayRules: newDisplayRules,
|
||||
})
|
||||
} else {
|
||||
onChange({
|
||||
displayRules: newDisplayRules,
|
||||
})
|
||||
}
|
||||
}}
|
||||
className={css({
|
||||
px: '2',
|
||||
py: '0.5',
|
||||
fontSize: '2xs',
|
||||
color: isDark ? 'green.300' : 'green.600',
|
||||
bg: isDark ? 'green.900/30' : 'green.50',
|
||||
border: '1px solid',
|
||||
borderColor: isDark ? 'green.700' : 'green.300',
|
||||
rounded: 'md',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: isDark ? 'green.800/40' : 'green.100' },
|
||||
})}
|
||||
>
|
||||
Auto
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
const newDisplayRules = {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,29 @@ import { WORKSHEET_LIMITS } from './constants/validation'
|
|||
* 3. Export parseXXXConfig() helper
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// SHARED DISPLAY RULE TYPES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Display rule values for conditional scaffolding
|
||||
*
|
||||
* - "auto": Defer to mastery progression's skill recommendation (mastery mode only)
|
||||
* - "always": Always show this scaffolding element
|
||||
* - "never": Never show this scaffolding element
|
||||
* - "whenRegrouping": Show only when problem requires regrouping
|
||||
* - "whenMultipleRegroups": Show only when problem has multiple regroups
|
||||
* - "when3PlusDigits": Show only when problem has 3+ digits
|
||||
*/
|
||||
const displayRuleValues = [
|
||||
'auto',
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
] as const
|
||||
|
||||
// =============================================================================
|
||||
// ADDITION WORKSHEETS
|
||||
// =============================================================================
|
||||
|
|
@ -336,64 +359,16 @@ const additionConfigV4BaseSchema = z.object({
|
|||
const additionConfigV4SmartSchema = additionConfigV4BaseSchema.extend({
|
||||
mode: z.literal('smart'),
|
||||
|
||||
// Conditional display rules
|
||||
// Conditional display rules (with 'auto' for deferring to smart difficulty)
|
||||
displayRules: z.object({
|
||||
carryBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
answerBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
placeValueColors: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
tenFrames: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
problemNumbers: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
cellBorders: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowNotation: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowingHints: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
carryBoxes: z.enum(displayRuleValues),
|
||||
answerBoxes: z.enum(displayRuleValues),
|
||||
placeValueColors: z.enum(displayRuleValues),
|
||||
tenFrames: z.enum(displayRuleValues),
|
||||
problemNumbers: z.enum(displayRuleValues),
|
||||
cellBorders: z.enum(displayRuleValues),
|
||||
borrowNotation: z.enum(displayRuleValues),
|
||||
borrowingHints: z.enum(displayRuleValues),
|
||||
}),
|
||||
|
||||
// Optional: Which smart difficulty profile is selected
|
||||
|
|
@ -406,63 +381,16 @@ const additionConfigV4ManualSchema = additionConfigV4BaseSchema.extend({
|
|||
mode: z.literal('manual'),
|
||||
|
||||
// Manual mode now uses conditional display rules (same as Smart/Mastery)
|
||||
// 'auto' is available but typically not used in manual mode
|
||||
displayRules: z.object({
|
||||
carryBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
answerBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
placeValueColors: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
tenFrames: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
problemNumbers: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
cellBorders: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowNotation: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowingHints: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
carryBoxes: z.enum(displayRuleValues),
|
||||
answerBoxes: z.enum(displayRuleValues),
|
||||
placeValueColors: z.enum(displayRuleValues),
|
||||
tenFrames: z.enum(displayRuleValues),
|
||||
problemNumbers: z.enum(displayRuleValues),
|
||||
cellBorders: z.enum(displayRuleValues),
|
||||
borrowNotation: z.enum(displayRuleValues),
|
||||
borrowingHints: z.enum(displayRuleValues),
|
||||
}),
|
||||
|
||||
// Optional: Which manual preset is selected
|
||||
|
|
@ -473,187 +401,44 @@ const additionConfigV4ManualSchema = additionConfigV4BaseSchema.extend({
|
|||
const additionConfigV4MasterySchema = additionConfigV4BaseSchema.extend({
|
||||
mode: z.literal('mastery'),
|
||||
|
||||
// Mastery mode uses displayRules like smart mode (conditional scaffolding)
|
||||
// Mastery mode uses displayRules with 'auto' for deferring to skill recommendations
|
||||
displayRules: z.object({
|
||||
carryBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
answerBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
placeValueColors: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
tenFrames: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
problemNumbers: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
cellBorders: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowNotation: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowingHints: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
carryBoxes: z.enum(displayRuleValues),
|
||||
answerBoxes: z.enum(displayRuleValues),
|
||||
placeValueColors: z.enum(displayRuleValues),
|
||||
tenFrames: z.enum(displayRuleValues),
|
||||
problemNumbers: z.enum(displayRuleValues),
|
||||
cellBorders: z.enum(displayRuleValues),
|
||||
borrowNotation: z.enum(displayRuleValues),
|
||||
borrowingHints: z.enum(displayRuleValues),
|
||||
}),
|
||||
|
||||
// Optional: Separate display rules for mixed mode (operator-specific scaffolding)
|
||||
// When operator='mixed', additionDisplayRules applies to addition problems,
|
||||
// subtractionDisplayRules applies to subtraction problems
|
||||
// Each can independently use 'auto' to defer to the operator-specific skill recommendation
|
||||
additionDisplayRules: z
|
||||
.object({
|
||||
carryBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
answerBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
placeValueColors: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
tenFrames: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
problemNumbers: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
cellBorders: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowNotation: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowingHints: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
carryBoxes: z.enum(displayRuleValues),
|
||||
answerBoxes: z.enum(displayRuleValues),
|
||||
placeValueColors: z.enum(displayRuleValues),
|
||||
tenFrames: z.enum(displayRuleValues),
|
||||
problemNumbers: z.enum(displayRuleValues),
|
||||
cellBorders: z.enum(displayRuleValues),
|
||||
borrowNotation: z.enum(displayRuleValues),
|
||||
borrowingHints: z.enum(displayRuleValues),
|
||||
})
|
||||
.optional(),
|
||||
subtractionDisplayRules: z
|
||||
.object({
|
||||
carryBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
answerBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
placeValueColors: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
tenFrames: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
problemNumbers: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
cellBorders: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowNotation: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowingHints: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
carryBoxes: z.enum(displayRuleValues),
|
||||
answerBoxes: z.enum(displayRuleValues),
|
||||
placeValueColors: z.enum(displayRuleValues),
|
||||
tenFrames: z.enum(displayRuleValues),
|
||||
problemNumbers: z.enum(displayRuleValues),
|
||||
cellBorders: z.enum(displayRuleValues),
|
||||
borrowNotation: z.enum(displayRuleValues),
|
||||
borrowingHints: z.enum(displayRuleValues),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,34 @@ function getDefaultDate(): string {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge user display rules with skill recommendations, resolving "auto" values
|
||||
*
|
||||
* For each display rule field:
|
||||
* - If user value is "auto" → use skill's recommendation
|
||||
* - If user value is undefined → use skill's recommendation
|
||||
* - Otherwise → use user's explicit value (manual override)
|
||||
*
|
||||
* @param skillRules - The skill's recommended scaffolding settings
|
||||
* @param userRules - The user's custom scaffolding settings (may contain "auto")
|
||||
* @returns Fully resolved display rules with no "auto" values
|
||||
*/
|
||||
function mergeDisplayRulesWithAuto(
|
||||
skillRules: DisplayRules,
|
||||
userRules: Partial<DisplayRules>
|
||||
): DisplayRules {
|
||||
const result: Record<string, any> = {}
|
||||
|
||||
for (const key of Object.keys(skillRules) as Array<keyof DisplayRules>) {
|
||||
const userValue = userRules[key]
|
||||
// If user value is "auto" or undefined, use skill's recommendation
|
||||
// Otherwise, use user's explicit value (manual override)
|
||||
result[key] = userValue === 'auto' || userValue === undefined ? skillRules[key] : userValue
|
||||
}
|
||||
|
||||
return result as DisplayRules
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and create complete config from partial form state
|
||||
*/
|
||||
|
|
@ -260,10 +288,22 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
|||
}
|
||||
}
|
||||
|
||||
const displayRules: DisplayRules = {
|
||||
...baseDisplayRules,
|
||||
...((formState.displayRules as any) ?? {}), // Override with provided rules if any
|
||||
}
|
||||
// Merge user's display rules with skill recommendations, resolving "auto" values
|
||||
const userDisplayRules = (formState.displayRules as any) ?? {}
|
||||
const displayRules: DisplayRules =
|
||||
mode === 'mastery'
|
||||
? mergeDisplayRulesWithAuto(baseDisplayRules, userDisplayRules)
|
||||
: {
|
||||
...baseDisplayRules,
|
||||
...userDisplayRules, // Smart mode: direct override (no "auto" resolution)
|
||||
}
|
||||
|
||||
console.log('[MASTERY MODE] Display rules resolved:', {
|
||||
mode,
|
||||
baseDisplayRules,
|
||||
userDisplayRules,
|
||||
resolvedDisplayRules: displayRules,
|
||||
})
|
||||
|
||||
// Build config with operator-specific display rules for mixed mode
|
||||
const operator = formState.operator ?? 'addition'
|
||||
|
|
@ -287,32 +327,36 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
|||
|
||||
if (addSkill?.recommendedScaffolding && subSkill?.recommendedScaffolding) {
|
||||
// Merge user's operator-specific displayRules with skill's recommended scaffolding
|
||||
// User's rules (if set) take precedence over skill's recommendations
|
||||
// Fall back to general displayRules if operator-specific rules don't exist
|
||||
// Resolves "auto" values to skill recommendations
|
||||
// Falls back to general displayRules if operator-specific rules don't exist
|
||||
const userAdditionRules: Partial<DisplayRules> =
|
||||
(formState as any).additionDisplayRules || formState.displayRules || {}
|
||||
const userSubtractionRules: Partial<DisplayRules> =
|
||||
(formState as any).subtractionDisplayRules || formState.displayRules || {}
|
||||
|
||||
console.log('[MIXED MODE SCAFFOLDING] User rules:', {
|
||||
console.log('[MIXED MODE SCAFFOLDING] User rules (may contain "auto"):', {
|
||||
additionRules: userAdditionRules,
|
||||
subtractionRules: userSubtractionRules,
|
||||
generalRules: formState.displayRules,
|
||||
})
|
||||
|
||||
// Resolve "auto" values to skill recommendations
|
||||
const resolvedAdditionRules = mergeDisplayRulesWithAuto(
|
||||
addSkill.recommendedScaffolding,
|
||||
userAdditionRules
|
||||
)
|
||||
const resolvedSubtractionRules = mergeDisplayRulesWithAuto(
|
||||
subSkill.recommendedScaffolding,
|
||||
userSubtractionRules
|
||||
)
|
||||
|
||||
config = {
|
||||
...baseConfig,
|
||||
additionDisplayRules: {
|
||||
...addSkill.recommendedScaffolding,
|
||||
...userAdditionRules, // User's custom rules override skill's recommendations
|
||||
},
|
||||
subtractionDisplayRules: {
|
||||
...subSkill.recommendedScaffolding,
|
||||
...userSubtractionRules, // User's custom rules override skill's recommendations
|
||||
},
|
||||
additionDisplayRules: resolvedAdditionRules,
|
||||
subtractionDisplayRules: resolvedSubtractionRules,
|
||||
} as any
|
||||
|
||||
console.log('[MIXED MODE SCAFFOLDING] Final config:', {
|
||||
console.log('[MIXED MODE SCAFFOLDING] Final config (after resolving "auto"):', {
|
||||
additionDisplayRules: config.additionDisplayRules,
|
||||
subtractionDisplayRules: config.subtractionDisplayRules,
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue