From 56742c511dff81b597eab84f28b64bd0d4ff6499 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Thu, 18 Dec 2025 10:49:49 -0600 Subject: [PATCH] fix(StartPracticeModal): responsive improvements + integrated tutorial CTA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Full-screen mode at ≤700px height for iPhone SE support - Two-column grid layout for settings in landscape mode - Integrated tutorial CTA: combines unlock banner + start button - Fixed collapsed mode clipping of target skills section - Made "focusing on weak skills" visible on all screen sizes - Fixed duplicate CSS media query breakpoints - BKT: changed computeBktFromHistory to accept Partial 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../practice/StartPracticeModal.tsx | 914 +++++++++++------- .../web/src/lib/curriculum/bkt/compute-bkt.ts | 12 +- 2 files changed, 577 insertions(+), 349 deletions(-) diff --git a/apps/web/src/components/practice/StartPracticeModal.tsx b/apps/web/src/components/practice/StartPracticeModal.tsx index 9ea9d250..3bd3b81e 100644 --- a/apps/web/src/components/practice/StartPracticeModal.tsx +++ b/apps/web/src/components/practice/StartPracticeModal.tsx @@ -7,7 +7,6 @@ import { useCallback, useMemo, useState } from 'react' import { useTheme } from '@/contexts/ThemeContext' import type { SessionPlan } from '@/db/schema/session-plans' import { DEFAULT_PLAN_CONFIG } from '@/db/schema/session-plans' -import type { ProblemGenerationMode } from '@/lib/curriculum/config' import { ActiveSessionExistsClientError, NoSkillsEnabledClientError, @@ -103,8 +102,6 @@ export function StartPracticeModal({ const [abacusMaxTerms, setAbacusMaxTerms] = useState( DEFAULT_PLAN_CONFIG.abacusTermCount?.max ?? 5 ) - const [problemGenerationMode, setProblemGenerationMode] = - useState('adaptive-bkt') const togglePart = useCallback((partType: keyof EnabledParts) => { setEnabledParts((prev) => { @@ -234,7 +231,7 @@ export function StartPracticeModal({ durationMinutes, abacusTermCount: { min: 3, max: abacusMaxTerms }, enabledParts, - problemGenerationMode, + problemGenerationMode: 'adaptive-bkt', }) } catch (err) { if (err instanceof ActiveSessionExistsClientError) { @@ -258,7 +255,6 @@ export function StartPracticeModal({ durationMinutes, abacusMaxTerms, enabledParts, - problemGenerationMode, existingPlan, generatePlan, approvePlan, @@ -361,8 +357,6 @@ export function StartPracticeModal({ transform: 'translate(-50%, -50%)', width: 'calc(100% - 2rem)', maxWidth: '360px', - maxHeight: 'calc(100vh - 2rem)', - overflowY: 'auto', borderRadius: '20px', boxShadow: '0 20px 50px -12px rgba(0, 0, 0, 0.4)', zIndex: 1001, @@ -371,6 +365,19 @@ export function StartPracticeModal({ width: 'auto', minWidth: '360px', }, + // Full-screen on short viewports + '@media (max-height: 700px)': { + top: 0, + left: 0, + transform: 'none', + width: '100%', + maxWidth: 'none', + height: '100%', + borderRadius: 0, + boxShadow: 'none', + display: 'flex', + flexDirection: 'column', + }, })} style={{ background: isDark @@ -381,6 +388,7 @@ export function StartPracticeModal({ -
+
{/* Header */} -
-
🎯
+
+
+ 🎯 +
- {/* Session config card */} + {/* Config and action wrapper */}
+ {/* Session config card */} +
{/* Summary view (collapses when expanded) */}
- {/* Target skills summary - shown when adaptive-bkt mode is selected */} - {problemGenerationMode === 'adaptive-bkt' && targetSkillsInfo.hasData && ( + {/* Target skills summary */} + {targetSkillsInfo.hasData && (
0 ? ( <> ( 3 && ( ) : ( + {/* Expanded header with collapse button */} +
+ + Session Settings + + +
+ + {/* Settings grid - 2 columns in landscape */} +
+ {/* Duration options */} -
+
Duration
-
+
{[5, 10, 15, 20].map((min) => { // Estimate problems for this duration using current settings const enabledPartTypes = PART_TYPES.filter((p) => enabledParts[p.type]).map( @@ -709,6 +949,8 @@ export function StartPracticeModal({
{/* Modes */} -
+
Practice Modes
-
+
{PART_TYPES.map(({ type, emoji, label }) => { const isEnabled = enabledParts[type] const problemCount = problemsPerType[type] @@ -785,6 +1048,8 @@ export function StartPracticeModal({
{/* Numbers per problem */} -
+
Numbers per problem
-
+
{[3, 4, 5, 6, 7, 8].map((terms) => { const isSelected = abacusMaxTerms === terms return (
- {/* Problem Selection Algorithm */} -
+ {/* Target skills info - in the grid for landscape layout */} + {targetSkillsInfo.hasData && (
- Problem Selection -
-
- {[ - { - mode: 'adaptive-bkt' as const, - label: 'Focus on weak spots', - desc: 'Practices what you need most (recommended)', + padding: '0.625rem', + borderRadius: '6px', + '@media (max-height: 700px)': { + padding: '0.375rem 0.5rem', }, - { - mode: 'classic' as const, - label: 'Practice everything', - desc: 'Equal time on all skills', - }, - ].map(({ mode, label, desc }) => { - const isSelected = problemGenerationMode === mode - return ( - - ) - })} -
- - {/* Target skills display - shown when adaptive-bkt mode is selected */} - {problemGenerationMode === 'adaptive-bkt' && targetSkillsInfo.hasData && ( -
0 + ? 'rgba(245, 158, 11, 0.08)' + : 'rgba(100, 116, 139, 0.08)' + : targetSkillsInfo.targetedSkills.length > 0 + ? 'rgba(245, 158, 11, 0.06)' + : 'rgba(100, 116, 139, 0.06)', + border: `1px solid ${ + isDark ? targetSkillsInfo.targetedSkills.length > 0 - ? 'rgba(245, 158, 11, 0.08)' - : 'rgba(100, 116, 139, 0.08)' + ? 'rgba(245, 158, 11, 0.2)' + : 'rgba(100, 116, 139, 0.2)' : targetSkillsInfo.targetedSkills.length > 0 - ? 'rgba(245, 158, 11, 0.06)' - : 'rgba(100, 116, 139, 0.06)', - border: `1px solid ${ - isDark - ? targetSkillsInfo.targetedSkills.length > 0 - ? 'rgba(245, 158, 11, 0.2)' - : 'rgba(100, 116, 139, 0.2)' - : targetSkillsInfo.targetedSkills.length > 0 - ? 'rgba(245, 158, 11, 0.15)' - : 'rgba(100, 116, 139, 0.15)' - }`, - })} - > - {targetSkillsInfo.targetedSkills.length > 0 ? ( - <> -
- Will target these weak skills (pKnown < 50%): -
-
- {targetSkillsInfo.targetedSkills.map((skill) => ( - - {skill.displayName}{' '} - - ({Math.round(skill.pKnown * 100)}%) - - - ))} -
- - ) : ( + ? 'rgba(245, 158, 11, 0.15)' + : 'rgba(100, 116, 139, 0.15)' + }`, + })} + > + {targetSkillsInfo.targetedSkills.length > 0 ? ( + <>
- No weak skills detected. Problems will be evenly distributed across all - practicing skills. + Focusing on weak skills:
- )} -
- )} -
+
+ {targetSkillsInfo.targetedSkills.map((skill) => ( + + {skill.displayName}{' '} + + ({Math.round(skill.pKnown * 100)}%) + + + ))} +
+ + ) : ( +
+ ✓ On track! Problems will be evenly distributed across all skills. +
+ )} +
+ )} + +
{/* End settings-grid */} - {/* Collapse button */} -
- {/* Tutorial gate - New skill available */} + {/* Tutorial CTA - New skill unlocked with integrated start button */} {showTutorialGate && tutorialConfig && nextSkill && (
-
- 🌟 + {/* Info section */} +
+ + 🌟 +

- New skill available! + You've unlocked: {tutorialConfig.title}

- Ready to learn {tutorialConfig.title}? - {nextSkill.skipCount > 0 && ( - - {' '} - (skipped {nextSkill.skipCount} time{nextSkill.skipCount > 1 ? 's' : ''}) - - )} + Start with a quick tutorial

-
- - -
+ {/* Integrated start button */} +
)} {/* Error display */} {displayError && (
)} - {/* Start button */} - + {/* Start button - only shown when no tutorial is pending */} + {!showTutorialGate && ( + + )} +
{/* End config-and-action wrapper */}
diff --git a/apps/web/src/lib/curriculum/bkt/compute-bkt.ts b/apps/web/src/lib/curriculum/bkt/compute-bkt.ts index ed354353..7d833cf4 100644 --- a/apps/web/src/lib/curriculum/bkt/compute-bkt.ts +++ b/apps/web/src/lib/curriculum/bkt/compute-bkt.ts @@ -75,8 +75,10 @@ function applyTimeDecay( */ export function computeBktFromHistory( results: ProblemResultWithContext[], - options: BktComputeExtendedOptions = DEFAULT_BKT_OPTIONS + options: Partial = {} ): BktComputeResult { + // Merge with defaults so callers can override just what they need + const opts: BktComputeExtendedOptions = { ...DEFAULT_BKT_OPTIONS, ...options } // Sort by timestamp to replay in chronological order // Note: timestamp may be a Date or a string (from JSON serialization) const sorted = [...results].sort((a, b) => { @@ -126,7 +128,7 @@ export function computeBktFromHistory( const evidenceWeight = helpWeight * rtWeight // Compute BKT updates (conjunctive model) - const blameMethod = options.blameMethod ?? 'heuristic' + const blameMethod = opts.blameMethod ?? 'heuristic' const updates = result.isCorrect ? updateOnCorrect(skillRecords) : updateOnIncorrectWithMethod(skillRecords, blameMethod) @@ -158,13 +160,13 @@ export function computeBktFromHistory( // Apply decay if enabled let finalPKnown = state.pKnown - if (options.applyDecay && state.lastPracticedAt) { + if (opts.applyDecay && state.lastPracticedAt) { const daysSinceLastPractice = (now.getTime() - state.lastPracticedAt.getTime()) / (1000 * 60 * 60 * 24) finalPKnown = applyTimeDecay( state.pKnown, daysSinceLastPractice, - options.decayHalfLifeDays, + opts.decayHalfLifeDays, state.params.pInit ) } @@ -175,7 +177,7 @@ export function computeBktFromHistory( const masteryClassification = classifyMastery( finalPKnown, confidence, - options.confidenceThreshold + opts.confidenceThreshold ) skills.push({