diff --git a/apps/web/src/app/create/worksheets/addition/components/config-panel/OverallDifficultySlider.tsx b/apps/web/src/app/create/worksheets/addition/components/config-panel/OverallDifficultySlider.tsx new file mode 100644 index 00000000..246a359d --- /dev/null +++ b/apps/web/src/app/create/worksheets/addition/components/config-panel/OverallDifficultySlider.tsx @@ -0,0 +1,255 @@ +'use client' + +import * as Slider from '@radix-ui/react-slider' +import { css } from '../../../../../../../styled-system/css' +import { + DIFFICULTY_PROFILES, + DIFFICULTY_PROGRESSION, + calculateOverallDifficulty, + calculateRegroupingIntensity, + calculateScaffoldingLevel, + REGROUPING_PROGRESSION, + SCAFFOLDING_PROGRESSION, + findNearestValidState, + getProfileFromConfig, + type DifficultyLevel, +} from '../../difficultyProfiles' +import type { DisplayRules } from '../../displayRules' + +export interface OverallDifficultySliderProps { + currentDifficulty: number + onChange: (updates: { + pAnyStart: number + pAllStart: number + displayRules: DisplayRules + difficultyProfile?: DifficultyLevel + }) => void + isDark?: boolean +} + +export function OverallDifficultySlider({ + currentDifficulty, + onChange, + isDark = false, +}: OverallDifficultySliderProps) { + const handleValueChange = (value: number[]) => { + const targetDifficulty = value[0] / 10 + + // Calculate preset positions in 2D space + const presetPoints = DIFFICULTY_PROGRESSION.map((presetName) => { + const preset = DIFFICULTY_PROFILES[presetName] + const regrouping = calculateRegroupingIntensity( + preset.regrouping.pAnyStart, + preset.regrouping.pAllStart + ) + const scaffolding = calculateScaffoldingLevel(preset.displayRules, regrouping) + const difficulty = calculateOverallDifficulty( + preset.regrouping.pAnyStart, + preset.regrouping.pAllStart, + preset.displayRules + ) + return { + regrouping, + scaffolding, + difficulty, + name: presetName, + } + }) + + // Find which path segment we're on and interpolate + let idealRegrouping = 0 + let idealScaffolding = 10 + + for (let i = 0; i < presetPoints.length - 1; i++) { + const start = presetPoints[i] + const end = presetPoints[i + 1] + + if (targetDifficulty >= start.difficulty && targetDifficulty <= end.difficulty) { + // Interpolate between start and end + const t = (targetDifficulty - start.difficulty) / (end.difficulty - start.difficulty) + idealRegrouping = start.regrouping + t * (end.regrouping - start.regrouping) + idealScaffolding = start.scaffolding + t * (end.scaffolding - start.scaffolding) + console.log('[Slider] Interpolating between', start.name, 'and', end.name, { + t, + idealRegrouping, + idealScaffolding, + }) + break + } + } + + // Handle edge cases (before first or after last preset) + if (targetDifficulty < presetPoints[0].difficulty) { + idealRegrouping = presetPoints[0].regrouping + idealScaffolding = presetPoints[0].scaffolding + } else if (targetDifficulty > presetPoints[presetPoints.length - 1].difficulty) { + idealRegrouping = presetPoints[presetPoints.length - 1].regrouping + idealScaffolding = presetPoints[presetPoints.length - 1].scaffolding + } + + // Find valid configuration closest to ideal point on path + let closestConfig: { + pAnyStart: number + pAllStart: number + displayRules: any + distance: number + } | null = null + + for (let regIdx = 0; regIdx < REGROUPING_PROGRESSION.length; regIdx++) { + for (let scaffIdx = 0; scaffIdx < SCAFFOLDING_PROGRESSION.length; scaffIdx++) { + const validState = findNearestValidState(regIdx, scaffIdx) + if (validState.regroupingIdx !== regIdx || validState.scaffoldingIdx !== scaffIdx) { + continue + } + + const regrouping = REGROUPING_PROGRESSION[regIdx] + const displayRules = SCAFFOLDING_PROGRESSION[scaffIdx] + + const actualRegrouping = calculateRegroupingIntensity( + regrouping.pAnyStart, + regrouping.pAllStart + ) + const actualScaffolding = calculateScaffoldingLevel(displayRules, actualRegrouping) + + // Euclidean distance to ideal point on pedagogical path + const distance = Math.sqrt( + (actualRegrouping - idealRegrouping) ** 2 + (actualScaffolding - idealScaffolding) ** 2 + ) + + if (closestConfig === null || distance < closestConfig.distance) { + closestConfig = { + pAnyStart: regrouping.pAnyStart, + pAllStart: regrouping.pAllStart, + displayRules, + distance, + } + } + } + } + + if (closestConfig) { + console.log('[Slider] Closest config:', { + ...closestConfig, + regrouping: calculateRegroupingIntensity(closestConfig.pAnyStart, closestConfig.pAllStart), + scaffolding: calculateScaffoldingLevel( + closestConfig.displayRules, + calculateRegroupingIntensity(closestConfig.pAnyStart, closestConfig.pAllStart) + ), + }) + const matchedProfile = getProfileFromConfig( + closestConfig.pAllStart, + closestConfig.pAnyStart, + closestConfig.displayRules + ) + onChange({ + pAnyStart: closestConfig.pAnyStart, + pAllStart: closestConfig.pAllStart, + displayRules: closestConfig.displayRules, + difficultyProfile: matchedProfile !== 'custom' ? matchedProfile : undefined, + }) + } + } + + return ( +
+
+ Overall Difficulty: {currentDifficulty.toFixed(1)} / 10 +
+ + {/* Difficulty Slider */} +
+ + + + + {/* Preset markers on track */} + {DIFFICULTY_PROGRESSION.map((profileName) => { + const p = DIFFICULTY_PROFILES[profileName] + const presetDifficulty = calculateOverallDifficulty( + p.regrouping.pAnyStart, + p.regrouping.pAllStart, + p.displayRules + ) + const position = (presetDifficulty / 10) * 100 + + return ( +
+ ) + })} + + + + +
+
+ ) +} diff --git a/apps/web/src/app/create/worksheets/addition/components/config-panel/SmartModeControls.tsx b/apps/web/src/app/create/worksheets/addition/components/config-panel/SmartModeControls.tsx index e8561428..5c6f2b35 100644 --- a/apps/web/src/app/create/worksheets/addition/components/config-panel/SmartModeControls.tsx +++ b/apps/web/src/app/create/worksheets/addition/components/config-panel/SmartModeControls.tsx @@ -27,6 +27,9 @@ import type { DisplayRules } from '../../displayRules' import { getScaffoldingSummary } from './utils' import { RegroupingFrequencyPanel } from './RegroupingFrequencyPanel' import { DigitRangeSection } from './DigitRangeSection' +import { DifficultyPresetDropdown } from './DifficultyPresetDropdown' +import { MakeEasierHarderButtons } from './MakeEasierHarderButtons' +import { OverallDifficultySlider } from './OverallDifficultySlider' export interface SmartModeControlsProps { formState: WorksheetFormState @@ -273,277 +276,17 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart return ( <> {/* Preset Selector Dropdown */} -
-
- Difficulty Preset -
- - - - - - - - {DIFFICULTY_PROGRESSION.map((presetName) => { - const preset = DIFFICULTY_PROFILES[presetName] - const isSelected = currentProfile === presetName && !isCustom - - // Generate preset description - const regroupingPercent = Math.round( - calculateRegroupingIntensity( - preset.regrouping.pAnyStart, - preset.regrouping.pAllStart - ) * 10 - ) - const scaffoldingSummary = getScaffoldingSummary( - preset.displayRules, - formState.operator - ) - const presetDescription = ( - <> -
{regroupingPercent}% regrouping
- {scaffoldingSummary} - - ) - - return ( - { - // Apply preset configuration - onChange({ - difficultyProfile: presetName, - pAnyStart: preset.regrouping.pAnyStart, - pAllStart: preset.regrouping.pAllStart, - displayRules: preset.displayRules, - }) - }} - className={css({ - display: 'flex', - flexDirection: 'column', - gap: '1', - px: '3', - py: '2.5', - rounded: 'md', - cursor: 'pointer', - outline: 'none', - bg: isSelected ? 'brand.50' : 'transparent', - _hover: { - bg: 'brand.50', - }, - _focus: { - bg: 'brand.100', - }, - })} - > -
- {preset.label} -
-
- {presetDescription} -
-
- ) - })} -
-
-
-
+ {/* Make Easier/Harder buttons with preview */}