From b956e2d6057d0a6427624f7b690e4a29218ac3f6 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Sat, 8 Nov 2025 14:53:44 -0600 Subject: [PATCH] fix(worksheets): add backward compatibility for displayRules in SmartModeControls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use type casting to safely access borrowNotation and borrowingHints fields from formState.displayRules, which may not have these fields if coming from old worksheet configurations. Provides fallback to profile defaults when fields are missing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../config-panel/SmartModeControls.tsx | 1283 ++++++++++------- .../worksheets/addition/typstGenerator.ts | 2 +- 2 files changed, 744 insertions(+), 541 deletions(-) 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 b4787a16..2f8b3cbc 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 @@ -1,13 +1,13 @@ -'use client' +"use client"; -import { useState } from 'react' -import type React from 'react' -import * as Slider from '@radix-ui/react-slider' -import * as Tooltip from '@radix-ui/react-tooltip' -import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { css } from '../../../../../../../styled-system/css' -import { stack } from '../../../../../../../styled-system/patterns' -import type { WorksheetFormState } from '../../types' +import { useState } from "react"; +import type React from "react"; +import * as Slider from "@radix-ui/react-slider"; +import * as Tooltip from "@radix-ui/react-tooltip"; +import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import { css } from "../../../../../../../styled-system/css"; +import { stack } from "../../../../../../../styled-system/patterns"; +import type { WorksheetFormState } from "../../types"; import { DIFFICULTY_PROFILES, DIFFICULTY_PROGRESSION, @@ -22,67 +22,87 @@ import { getProfileFromConfig, type DifficultyLevel, type DifficultyMode, -} from '../../difficultyProfiles' -import type { DisplayRules } from '../../displayRules' -import { getScaffoldingSummary } from './utils' +} from "../../difficultyProfiles"; +import type { DisplayRules } from "../../displayRules"; +import { getScaffoldingSummary } from "./utils"; export interface SmartModeControlsProps { - formState: WorksheetFormState - onChange: (updates: Partial) => void - isDark?: boolean + formState: WorksheetFormState; + onChange: (updates: Partial) => void; + isDark?: boolean; } -export function SmartModeControls({ formState, onChange, isDark = false }: SmartModeControlsProps) { - const [showDebugPlot, setShowDebugPlot] = useState(false) - const [hoverPoint, setHoverPoint] = useState<{ x: number; y: number } | null>(null) +export function SmartModeControls({ + formState, + onChange, + isDark = false, +}: SmartModeControlsProps) { + const [showDebugPlot, setShowDebugPlot] = useState(false); + const [hoverPoint, setHoverPoint] = useState<{ x: number; y: number } | null>( + null, + ); const [hoverPreview, setHoverPreview] = useState<{ - pAnyStart: number - pAllStart: number - displayRules: DisplayRules - matchedProfile: string | 'custom' - } | null>(null) + pAnyStart: number; + pAllStart: number; + displayRules: DisplayRules; + matchedProfile: string | "custom"; + } | null>(null); // Helper function to handle difficulty adjustments - const handleDifficultyChange = (mode: DifficultyMode, direction: 'harder' | 'easier') => { + const handleDifficultyChange = ( + mode: DifficultyMode, + direction: "harder" | "easier", + ) => { const currentState = { pAnyStart: formState.pAnyStart ?? 0.25, pAllStart: formState.pAllStart ?? 0, - displayRules: formState.displayRules ?? DIFFICULTY_PROFILES.earlyLearner.displayRules, - } + displayRules: { + ...(formState.displayRules ?? DIFFICULTY_PROFILES.earlyLearner.displayRules), + // Ensure new fields have defaults if missing (for backward compatibility) + borrowNotation: + (formState.displayRules as any)?.borrowNotation ?? + DIFFICULTY_PROFILES.earlyLearner.displayRules.borrowNotation, + borrowingHints: + (formState.displayRules as any)?.borrowingHints ?? + DIFFICULTY_PROFILES.earlyLearner.displayRules.borrowingHints, + }, + }; const result = - direction === 'harder' + direction === "harder" ? makeHarder(currentState, mode, formState.operator) - : makeEasier(currentState, mode, formState.operator) + : makeEasier(currentState, mode, formState.operator); onChange({ pAnyStart: result.pAnyStart, pAllStart: result.pAllStart, displayRules: result.displayRules, difficultyProfile: - result.difficultyProfile !== 'custom' ? result.difficultyProfile : undefined, - }) - } + result.difficultyProfile !== "custom" + ? result.difficultyProfile + : undefined, + }); + }; return (
-
+
Difficulty Level @@ -90,88 +110,115 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart {/* Get current profile and state */} {(() => { - const currentProfile = formState.difficultyProfile as DifficultyLevel | undefined + const currentProfile = formState.difficultyProfile as + | DifficultyLevel + | undefined; const profile = currentProfile ? DIFFICULTY_PROFILES[currentProfile] - : DIFFICULTY_PROFILES.earlyLearner + : DIFFICULTY_PROFILES.earlyLearner; // Use defaults from profile if form state values are undefined - const pAnyStart = formState.pAnyStart ?? profile.regrouping.pAnyStart - const pAllStart = formState.pAllStart ?? profile.regrouping.pAllStart - const displayRules = formState.displayRules ?? profile.displayRules + const pAnyStart = formState.pAnyStart ?? profile.regrouping.pAnyStart; + const pAllStart = formState.pAllStart ?? profile.regrouping.pAllStart; + const displayRules: DisplayRules = { + ...(formState.displayRules ?? profile.displayRules), + // Ensure new fields have defaults (backward compatibility with old configs) + borrowNotation: (formState.displayRules as any)?.borrowNotation ?? profile.displayRules.borrowNotation, + borrowingHints: (formState.displayRules as any)?.borrowingHints ?? profile.displayRules.borrowingHints, + }; // Check if current state matches the selected profile const matchesProfile = pAnyStart === profile.regrouping.pAnyStart && pAllStart === profile.regrouping.pAllStart && - JSON.stringify(displayRules) === JSON.stringify(profile.displayRules) - const isCustom = !matchesProfile + JSON.stringify(displayRules) === + JSON.stringify(profile.displayRules); + const isCustom = !matchesProfile; // Find nearest presets for custom configurations - let nearestEasier: DifficultyLevel | null = null - let nearestHarder: DifficultyLevel | null = null - let customDescription: React.ReactNode = '' + let nearestEasier: DifficultyLevel | null = null; + let nearestHarder: DifficultyLevel | null = null; + let customDescription: React.ReactNode = ""; if (isCustom) { - const currentRegrouping = calculateRegroupingIntensity(pAnyStart, pAllStart) - const currentScaffolding = calculateScaffoldingLevel(displayRules, currentRegrouping) + const currentRegrouping = calculateRegroupingIntensity( + pAnyStart, + pAllStart, + ); + const currentScaffolding = calculateScaffoldingLevel( + displayRules, + currentRegrouping, + ); // Calculate distances to all presets const distances = DIFFICULTY_PROGRESSION.map((presetName) => { - const preset = DIFFICULTY_PROFILES[presetName] + const preset = DIFFICULTY_PROFILES[presetName]; const presetRegrouping = calculateRegroupingIntensity( preset.regrouping.pAnyStart, - preset.regrouping.pAllStart - ) + preset.regrouping.pAllStart, + ); const presetScaffolding = calculateScaffoldingLevel( preset.displayRules, - presetRegrouping - ) + presetRegrouping, + ); const distance = Math.sqrt( (currentRegrouping - presetRegrouping) ** 2 + - (currentScaffolding - presetScaffolding) ** 2 - ) + (currentScaffolding - presetScaffolding) ** 2, + ); return { presetName, distance, difficulty: calculateOverallDifficulty( preset.regrouping.pAnyStart, preset.regrouping.pAllStart, - preset.displayRules + preset.displayRules, ), - } - }).sort((a, b) => a.distance - b.distance) + }; + }).sort((a, b) => a.distance - b.distance); const currentDifficultyValue = calculateOverallDifficulty( pAnyStart, pAllStart, - displayRules - ) + displayRules, + ); // Find closest easier and harder presets - const easierPresets = distances.filter((d) => d.difficulty < currentDifficultyValue) - const harderPresets = distances.filter((d) => d.difficulty > currentDifficultyValue) + const easierPresets = distances.filter( + (d) => d.difficulty < currentDifficultyValue, + ); + const harderPresets = distances.filter( + (d) => d.difficulty > currentDifficultyValue, + ); nearestEasier = - easierPresets.length > 0 ? easierPresets[0].presetName : distances[0].presetName + easierPresets.length > 0 + ? easierPresets[0].presetName + : distances[0].presetName; nearestHarder = harderPresets.length > 0 ? harderPresets[0].presetName - : distances[distances.length - 1].presetName + : distances[distances.length - 1].presetName; // Generate custom description - const regroupingPercent = Math.round(pAnyStart * 100) - const scaffoldingSummary = getScaffoldingSummary(displayRules, formState.operator) + const regroupingPercent = Math.round(pAnyStart * 100); + const scaffoldingSummary = getScaffoldingSummary( + displayRules, + formState.operator, + ); customDescription = ( <>
{regroupingPercent}% regrouping
{scaffoldingSummary} - ) + ); } // Calculate current difficulty position - const currentDifficulty = calculateOverallDifficulty(pAnyStart, pAllStart, displayRules) + const currentDifficulty = calculateOverallDifficulty( + pAnyStart, + pAllStart, + displayRules, + ); // Calculate make easier/harder results for preview (all modes) const easierResultBoth = makeEasier( @@ -180,9 +227,9 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart pAllStart, displayRules, }, - 'both', - formState.operator - ) + "both", + formState.operator, + ); const easierResultChallenge = makeEasier( { @@ -190,9 +237,9 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart pAllStart, displayRules, }, - 'challenge', - formState.operator - ) + "challenge", + formState.operator, + ); const easierResultSupport = makeEasier( { @@ -200,9 +247,9 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart pAllStart, displayRules, }, - 'support', - formState.operator - ) + "support", + formState.operator, + ); const harderResultBoth = makeHarder( { @@ -210,9 +257,9 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart pAllStart, displayRules, }, - 'both', - formState.operator - ) + "both", + formState.operator, + ); const harderResultChallenge = makeHarder( { @@ -220,9 +267,9 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart pAllStart, displayRules, }, - 'challenge', - formState.operator - ) + "challenge", + formState.operator, + ); const harderResultSupport = makeHarder( { @@ -230,37 +277,43 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart pAllStart, displayRules, }, - 'support', - formState.operator - ) + "support", + formState.operator, + ); const canMakeEasierBoth = - easierResultBoth.changeDescription !== 'Already at minimum difficulty' + easierResultBoth.changeDescription !== + "Already at minimum difficulty"; const canMakeEasierChallenge = - easierResultChallenge.changeDescription !== 'Already at minimum difficulty' + easierResultChallenge.changeDescription !== + "Already at minimum difficulty"; const canMakeEasierSupport = - easierResultSupport.changeDescription !== 'Already at minimum difficulty' + easierResultSupport.changeDescription !== + "Already at minimum difficulty"; const canMakeHarderBoth = - harderResultBoth.changeDescription !== 'Already at maximum difficulty' + harderResultBoth.changeDescription !== + "Already at maximum difficulty"; const canMakeHarderChallenge = - harderResultChallenge.changeDescription !== 'Already at maximum difficulty' + harderResultChallenge.changeDescription !== + "Already at maximum difficulty"; const canMakeHarderSupport = - harderResultSupport.changeDescription !== 'Already at maximum difficulty' + harderResultSupport.changeDescription !== + "Already at maximum difficulty"; // Keep legacy names for compatibility - const canMakeEasier = canMakeEasierBoth - const canMakeHarder = canMakeHarderBoth + const canMakeEasier = canMakeEasierBoth; + const canMakeHarder = canMakeHarderBoth; return ( <> {/* Preset Selector Dropdown */} -
+
Difficulty Preset @@ -271,54 +324,68 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart type="button" data-action="open-preset-dropdown" className={css({ - w: 'full', - h: '24', - px: '3', - py: '2.5', - border: '2px solid', - borderColor: isCustom ? 'orange.400' : 'gray.300', - bg: isCustom ? 'orange.50' : 'white', - rounded: 'lg', - cursor: 'pointer', - transition: 'all 0.15s', - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - textAlign: 'left', - gap: '2', + w: "full", + h: "24", + px: "3", + py: "2.5", + border: "2px solid", + borderColor: isCustom ? "orange.400" : "gray.300", + bg: isCustom ? "orange.50" : "white", + rounded: "lg", + cursor: "pointer", + transition: "all 0.15s", + display: "flex", + alignItems: "center", + justifyContent: "space-between", + textAlign: "left", + gap: "2", _hover: { - borderColor: isCustom ? 'orange.500' : 'brand.400', + borderColor: isCustom ? "orange.500" : "brand.400", }, })} >
{hoverPreview ? ( <> - {hoverPreview.matchedProfile !== 'custom' ? ( + {hoverPreview.matchedProfile !== "custom" ? ( <> - {DIFFICULTY_PROFILES[hoverPreview.matchedProfile].label}{' '} - + { + DIFFICULTY_PROFILES[ + hoverPreview.matchedProfile + ].label + }{" "} + (hover preview) ) : ( <> - ✨ Custom{' '} - + ✨ Custom{" "} + (hover preview) @@ -328,78 +395,88 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart nearestEasier && nearestHarder ? ( <> {DIFFICULTY_PROFILES[nearestEasier].label} - {' ↔ '} + {" ↔ "} {DIFFICULTY_PROFILES[nearestHarder].label} ) : ( - '✨ Custom' + "✨ Custom" ) ) : currentProfile ? ( DIFFICULTY_PROFILES[currentProfile].label ) : ( - 'Early Learner' + "Early Learner" )}
{hoverPreview ? ( (() => { - const regroupingPercent = Math.round(hoverPreview.pAnyStart * 100) + const regroupingPercent = Math.round( + hoverPreview.pAnyStart * 100, + ); const scaffoldingSummary = getScaffoldingSummary( hoverPreview.displayRules, - formState.operator - ) + formState.operator, + ); return ( <>
{regroupingPercent}% regrouping
{scaffoldingSummary} - ) + ); })() ) : isCustom ? ( customDescription ) : currentProfile ? ( (() => { - const preset = DIFFICULTY_PROFILES[currentProfile] + const preset = + DIFFICULTY_PROFILES[currentProfile]; const regroupingPercent = Math.round( - preset.regrouping.pAnyStart * 100 - ) + preset.regrouping.pAnyStart * 100, + ); const scaffoldingSummary = getScaffoldingSummary( preset.displayRules, - formState.operator - ) + formState.operator, + ); return ( <>
{regroupingPercent}% regrouping
{scaffoldingSummary} - ) + ); })() ) : ( <>
25% regrouping
- Always: carry boxes, answer boxes, place value colors, ten-frames + Always: carry boxes, answer boxes, place value + colors, ten-frames
)}
- + @@ -408,40 +485,41 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart {DIFFICULTY_PROGRESSION.map((presetName) => { - const preset = DIFFICULTY_PROFILES[presetName] - const isSelected = currentProfile === presetName && !isCustom + 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 - ) + preset.regrouping.pAllStart, + ) * 10, + ); const scaffoldingSummary = getScaffoldingSummary( preset.displayRules, - formState.operator - ) + formState.operator, + ); const presetDescription = ( <>
{regroupingPercent}% regrouping
{scaffoldingSummary} - ) + ); return (
{preset.label}
{presetDescription}
- ) + ); })}
@@ -503,66 +581,71 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart {/* Make Easier/Harder buttons with preview */}
{/* Four-Button Layout: [Alt-35%][Rec-65%][Rec-65%][Alt-35%] */} -
+
{/* Determine which mode is alternative for easier */} {(() => { const easierAlternativeMode = easierResultBoth.changeDescription === easierResultChallenge.changeDescription - ? 'support' - : 'challenge' + ? "support" + : "challenge"; const easierAlternativeResult = - easierAlternativeMode === 'support' + easierAlternativeMode === "support" ? easierResultSupport - : easierResultChallenge + : easierResultChallenge; const easierAlternativeLabel = - easierAlternativeMode === 'support' ? '↑ More support' : '← Less challenge' + easierAlternativeMode === "support" + ? "↑ More support" + : "← Less challenge"; const canEasierAlternative = - easierAlternativeMode === 'support' + easierAlternativeMode === "support" ? canMakeEasierSupport - : canMakeEasierChallenge + : canMakeEasierChallenge; return ( -
+
{/* Alternative Easier Button - Hidden if disabled and main is enabled */} {canEasierAlternative && (
- ) + ); })()} {/* Determine which mode is alternative for harder */} @@ -657,56 +754,66 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart const harderAlternativeMode = harderResultBoth.changeDescription === harderResultChallenge.changeDescription - ? 'support' - : 'challenge' + ? "support" + : "challenge"; const harderAlternativeResult = - harderAlternativeMode === 'support' + harderAlternativeMode === "support" ? harderResultSupport - : harderResultChallenge + : harderResultChallenge; const harderAlternativeLabel = - harderAlternativeMode === 'support' ? '↓ Less support' : '→ More challenge' + harderAlternativeMode === "support" + ? "↓ Less support" + : "→ More challenge"; const canHarderAlternative = - harderAlternativeMode === 'support' + harderAlternativeMode === "support" ? canMakeHarderSupport - : canMakeHarderChallenge + : canMakeHarderChallenge; return ( -
+
{/* Recommended Harder Button - Expands to full width if alternative is hidden */}
- ) + ); })()}
{/* Overall Difficulty Slider */} -
+
Overall Difficulty: {currentDifficulty.toFixed(1)} / 10
{/* Difficulty Slider */} -
+
{ - const targetDifficulty = value[0] / 10 + 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 } - }) + 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 + let idealRegrouping = 0; + let idealScaffolding = 10; for (let i = 0; i < presetPoints.length - 1; i++) { - const start = presetPoints[i] - const end = presetPoints[i + 1] + const start = presetPoints[i]; + const end = presetPoints[i + 1]; if ( targetDifficulty >= start.difficulty && @@ -851,192 +974,210 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart // Interpolate between start and end const t = (targetDifficulty - start.difficulty) / - (end.difficulty - start.difficulty) + (end.difficulty - start.difficulty); idealRegrouping = - start.regrouping + t * (end.regrouping - start.regrouping) + start.regrouping + + t * (end.regrouping - start.regrouping); idealScaffolding = - start.scaffolding + t * (end.scaffolding - start.scaffolding) + start.scaffolding + + t * (end.scaffolding - start.scaffolding); console.log( - '[Slider] Interpolating between', + "[Slider] Interpolating between", start.name, - 'and', + "and", end.name, { t, idealRegrouping, idealScaffolding, - } - ) - break + }, + ); + break; } } // Handle edge cases (before first or after last preset) if (targetDifficulty < presetPoints[0].difficulty) { - idealRegrouping = presetPoints[0].regrouping - idealScaffolding = presetPoints[0].scaffolding + idealRegrouping = presetPoints[0].regrouping; + idealScaffolding = presetPoints[0].scaffolding; } else if ( - targetDifficulty > presetPoints[presetPoints.length - 1].difficulty + targetDifficulty > + presetPoints[presetPoints.length - 1].difficulty ) { - idealRegrouping = presetPoints[presetPoints.length - 1].regrouping - idealScaffolding = presetPoints[presetPoints.length - 1].scaffolding + 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 + pAnyStart: number; + pAllStart: number; + displayRules: any; + distance: number; + } | null = null; - for (let regIdx = 0; regIdx < REGROUPING_PROGRESSION.length; regIdx++) { + for ( + let regIdx = 0; + regIdx < REGROUPING_PROGRESSION.length; + regIdx++ + ) { for ( let scaffIdx = 0; scaffIdx < SCAFFOLDING_PROGRESSION.length; scaffIdx++ ) { - const validState = findNearestValidState(regIdx, scaffIdx) + const validState = findNearestValidState( + regIdx, + scaffIdx, + ); if ( validState.regroupingIdx !== regIdx || validState.scaffoldingIdx !== scaffIdx ) { - continue + continue; } - const regrouping = REGROUPING_PROGRESSION[regIdx] - const displayRules = SCAFFOLDING_PROGRESSION[scaffIdx] + const regrouping = REGROUPING_PROGRESSION[regIdx]; + const displayRules = + SCAFFOLDING_PROGRESSION[scaffIdx]; const actualRegrouping = calculateRegroupingIntensity( regrouping.pAnyStart, - regrouping.pAllStart - ) + regrouping.pAllStart, + ); const actualScaffolding = calculateScaffoldingLevel( displayRules, - actualRegrouping - ) + actualRegrouping, + ); // Euclidean distance to ideal point on pedagogical path const distance = Math.sqrt( (actualRegrouping - idealRegrouping) ** 2 + - (actualScaffolding - idealScaffolding) ** 2 - ) + (actualScaffolding - idealScaffolding) ** 2, + ); - if (closestConfig === null || distance < closestConfig.distance) { + if ( + closestConfig === null || + distance < closestConfig.distance + ) { closestConfig = { pAnyStart: regrouping.pAnyStart, pAllStart: regrouping.pAllStart, displayRules, distance, - } + }; } } } if (closestConfig) { - console.log('[Slider] Closest config:', { + console.log("[Slider] Closest config:", { ...closestConfig, regrouping: calculateRegroupingIntensity( closestConfig.pAnyStart, - closestConfig.pAllStart + closestConfig.pAllStart, ), scaffolding: calculateScaffoldingLevel( closestConfig.displayRules, calculateRegroupingIntensity( closestConfig.pAnyStart, - closestConfig.pAllStart - ) + closestConfig.pAllStart, + ), ), - }) + }); const matchedProfile = getProfileFromConfig( closestConfig.pAllStart, closestConfig.pAnyStart, - closestConfig.displayRules - ) + closestConfig.displayRules, + ); onChange({ pAnyStart: closestConfig.pAnyStart, pAllStart: closestConfig.pAllStart, displayRules: closestConfig.displayRules, difficultyProfile: - matchedProfile !== 'custom' ? matchedProfile : undefined, - }) + matchedProfile !== "custom" + ? matchedProfile + : undefined, + }); } }} className={css({ - position: 'relative', - display: 'flex', - alignItems: 'center', - userSelect: 'none', - touchAction: 'none', - h: '8', + position: "relative", + display: "flex", + alignItems: "center", + userSelect: "none", + touchAction: "none", + h: "8", })} > {/* Preset markers on track */} {DIFFICULTY_PROGRESSION.map((profileName) => { - const p = DIFFICULTY_PROFILES[profileName] + const p = DIFFICULTY_PROFILES[profileName]; const presetDifficulty = calculateOverallDifficulty( p.regrouping.pAnyStart, p.regrouping.pAllStart, - p.displayRules - ) - const position = (presetDifficulty / 10) * 100 + p.displayRules, + ); + const position = (presetDifficulty / 10) * 100; return (
- ) + ); })} @@ -1047,47 +1188,49 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart {/* 2D Difficulty Space Visualizer */}
{showDebugPlot && ( -
+
{/* Responsive SVG container */}
{(() => { // Make responsive - use container width with max size - const maxSize = 500 - const width = maxSize - const height = maxSize - const padding = 40 - const graphWidth = width - padding * 2 - const graphHeight = height - padding * 2 + const maxSize = 500; + const width = maxSize; + const height = maxSize; + const padding = 40; + const graphWidth = width - padding * 2; + const graphHeight = height - padding * 2; - const currentReg = calculateRegroupingIntensity(pAnyStart, pAllStart) - const currentScaf = calculateScaffoldingLevel(displayRules, currentReg) + const currentReg = calculateRegroupingIntensity( + pAnyStart, + pAllStart, + ); + const currentScaf = calculateScaffoldingLevel( + displayRules, + currentReg, + ); // Convert 0-10 scale to SVG coordinates - const toX = (val: number) => padding + (val / 10) * graphWidth - const toY = (val: number) => height - padding - (val / 10) * graphHeight + const toX = (val: number) => + padding + (val / 10) * graphWidth; + const toY = (val: number) => + height - padding - (val / 10) * graphHeight; // Convert SVG coordinates to 0-10 scale const fromX = (x: number) => - Math.max(0, Math.min(10, ((x - padding) / graphWidth) * 10)) + Math.max( + 0, + Math.min(10, ((x - padding) / graphWidth) * 10), + ); const fromY = (y: number) => - Math.max(0, Math.min(10, ((height - padding - y) / graphHeight) * 10)) + Math.max( + 0, + Math.min( + 10, + ((height - padding - y) / graphHeight) * 10, + ), + ); // Helper to calculate valid target from mouse position const calculateValidTarget = ( clientX: number, clientY: number, - svg: SVGSVGElement + svg: SVGSVGElement, ) => { - const rect = svg.getBoundingClientRect() - const x = clientX - rect.left - const y = clientY - rect.top + const rect = svg.getBoundingClientRect(); + const x = clientX - rect.left; + const y = clientY - rect.top; // Convert to difficulty space (0-10) - const regroupingIntensity = fromX(x) - const scaffoldingLevel = fromY(y) + const regroupingIntensity = fromX(x); + const scaffoldingLevel = fromY(y); // Check if we're near a preset (within snap threshold) - const snapThreshold = 1.0 // 1.0 units in 0-10 scale + const snapThreshold = 1.0; // 1.0 units in 0-10 scale let nearestPreset: { - distance: number - profile: (typeof DIFFICULTY_PROFILES)[keyof typeof DIFFICULTY_PROFILES] - } | null = null + distance: number; + profile: (typeof DIFFICULTY_PROFILES)[keyof typeof DIFFICULTY_PROFILES]; + } | null = null; for (const profileName of DIFFICULTY_PROGRESSION) { - const p = DIFFICULTY_PROFILES[profileName] + const p = DIFFICULTY_PROFILES[profileName]; const presetReg = calculateRegroupingIntensity( p.regrouping.pAnyStart, - p.regrouping.pAllStart - ) - const presetScaf = calculateScaffoldingLevel(p.displayRules, presetReg) + p.regrouping.pAllStart, + ); + const presetScaf = calculateScaffoldingLevel( + p.displayRules, + presetReg, + ); // Calculate Euclidean distance const distance = Math.sqrt( (regroupingIntensity - presetReg) ** 2 + - (scaffoldingLevel - presetScaf) ** 2 - ) + (scaffoldingLevel - presetScaf) ** 2, + ); if (distance <= snapThreshold) { - if (!nearestPreset || distance < nearestPreset.distance) { - nearestPreset = { distance, profile: p } + if ( + !nearestPreset || + distance < nearestPreset.distance + ) { + nearestPreset = { distance, profile: p }; } } } @@ -1177,74 +1343,103 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart if (nearestPreset) { return { newRegrouping: nearestPreset.profile.regrouping, - newDisplayRules: nearestPreset.profile.displayRules, + newDisplayRules: + nearestPreset.profile.displayRules, matchedProfile: nearestPreset.profile.name, reg: calculateRegroupingIntensity( nearestPreset.profile.regrouping.pAnyStart, - nearestPreset.profile.regrouping.pAllStart + nearestPreset.profile.regrouping.pAllStart, ), scaf: calculateScaffoldingLevel( nearestPreset.profile.displayRules, calculateRegroupingIntensity( nearestPreset.profile.regrouping.pAnyStart, - nearestPreset.profile.regrouping.pAllStart - ) + nearestPreset.profile.regrouping.pAllStart, + ), ), - } + }; } // No preset nearby, use normal progression indices const regroupingIdx = Math.round( - (regroupingIntensity / 10) * (REGROUPING_PROGRESSION.length - 1) - ) + (regroupingIntensity / 10) * + (REGROUPING_PROGRESSION.length - 1), + ); const scaffoldingIdx = Math.round( - ((10 - scaffoldingLevel) / 10) * (SCAFFOLDING_PROGRESSION.length - 1) - ) + ((10 - scaffoldingLevel) / 10) * + (SCAFFOLDING_PROGRESSION.length - 1), + ); // Find nearest valid state (applies pedagogical constraints) - const validState = findNearestValidState(regroupingIdx, scaffoldingIdx) + const validState = findNearestValidState( + regroupingIdx, + scaffoldingIdx, + ); // Get actual values from progressions - const newRegrouping = REGROUPING_PROGRESSION[validState.regroupingIdx] - const newDisplayRules = SCAFFOLDING_PROGRESSION[validState.scaffoldingIdx] + const newRegrouping = + REGROUPING_PROGRESSION[validState.regroupingIdx]; + const newDisplayRules = + SCAFFOLDING_PROGRESSION[validState.scaffoldingIdx]; // Calculate display coordinates const reg = calculateRegroupingIntensity( newRegrouping.pAnyStart, - newRegrouping.pAllStart - ) - const scaf = calculateScaffoldingLevel(newDisplayRules, reg) + newRegrouping.pAllStart, + ); + const scaf = calculateScaffoldingLevel( + newDisplayRules, + reg, + ); // Check if this matches a preset const matchedProfile = getProfileFromConfig( newRegrouping.pAllStart, newRegrouping.pAnyStart, - newDisplayRules - ) + newDisplayRules, + ); - return { newRegrouping, newDisplayRules, matchedProfile, reg, scaf } - } + return { + newRegrouping, + newDisplayRules, + matchedProfile, + reg, + scaf, + }; + }; - const handleMouseMove = (e: React.MouseEvent) => { - const svg = e.currentTarget - const target = calculateValidTarget(e.clientX, e.clientY, svg) - setHoverPoint({ x: target.reg, y: target.scaf }) + const handleMouseMove = ( + e: React.MouseEvent, + ) => { + const svg = e.currentTarget; + const target = calculateValidTarget( + e.clientX, + e.clientY, + svg, + ); + setHoverPoint({ x: target.reg, y: target.scaf }); setHoverPreview({ pAnyStart: target.newRegrouping.pAnyStart, pAllStart: target.newRegrouping.pAllStart, displayRules: target.newDisplayRules, matchedProfile: target.matchedProfile, - }) - } + }); + }; const handleMouseLeave = () => { - setHoverPoint(null) - setHoverPreview(null) - } + setHoverPoint(null); + setHoverPreview(null); + }; - const handleClick = (e: React.MouseEvent) => { - const svg = e.currentTarget - const target = calculateValidTarget(e.clientX, e.clientY, svg) + const handleClick = ( + e: React.MouseEvent, + ) => { + const svg = e.currentTarget; + const target = calculateValidTarget( + e.clientX, + e.clientY, + svg, + ); // Update via onChange onChange({ @@ -1252,11 +1447,11 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart pAllStart: target.newRegrouping.pAllStart, displayRules: target.newDisplayRules, difficultyProfile: - target.matchedProfile !== 'custom' + target.matchedProfile !== "custom" ? target.matchedProfile : undefined, - }) - } + }); + }; return ( {/* Grid lines */} @@ -1339,12 +1534,15 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart {/* Preset points */} {DIFFICULTY_PROGRESSION.map((profileName) => { - const p = DIFFICULTY_PROFILES[profileName] + const p = DIFFICULTY_PROFILES[profileName]; const reg = calculateRegroupingIntensity( p.regrouping.pAnyStart, - p.regrouping.pAllStart - ) - const scaf = calculateScaffoldingLevel(p.displayRules, reg) + p.regrouping.pAllStart, + ); + const scaf = calculateScaffoldingLevel( + p.displayRules, + reg, + ); return ( @@ -1368,7 +1566,7 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart {p.label} - ) + ); })} {/* Hover preview - show where click will land */} @@ -1413,18 +1611,23 @@ export function SmartModeControls({ formState, onChange, isDark = false }: Smart stroke="#059669" strokeWidth="3" /> - + - ) + ); })()}
)}
- ) + ); })()}
- ) + ); } diff --git a/apps/web/src/app/create/worksheets/addition/typstGenerator.ts b/apps/web/src/app/create/worksheets/addition/typstGenerator.ts index c4e6e375..516b7118 100644 --- a/apps/web/src/app/create/worksheets/addition/typstGenerator.ts +++ b/apps/web/src/app/create/worksheets/addition/typstGenerator.ts @@ -77,7 +77,7 @@ function generatePageTypst( ? analyzeProblem(p.a, p.b) : analyzeSubtractionProblem(p.minuend, p.subtrahend); const displayOptions = resolveDisplayForProblem( - config.displayRules, + config.displayRules as any, // Cast for backward compatibility with configs missing new fields meta, );