feat(practice): add Remediation CTA for weak skill focus sessions

When a student is in remediation mode (has weak skills to strengthen),
the StartPracticeModal now shows a special amber-themed CTA similar to
the tutorial CTA:

- 💪 "Time to build strength!" heading
- Lists weak skills with pKnown percentages
- "Start Focus Practice →" amber button
- Shows up to 4 skills with "+N more" overflow

Includes Storybook stories for:
- Single weak skill
- Multiple weak skills (2)
- Many weak skills (6, with overflow)
- Dark theme variant

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-12-18 14:42:44 -06:00
parent 9851c01026
commit 7d8bb2f525
3 changed files with 402 additions and 3 deletions

View File

@ -0,0 +1,151 @@
# Remediation CTA Plan
## Overview
Add special "fancy" treatment to the StartPracticeModal when the student is in remediation mode (has weak skills that need strengthening). This mirrors the existing tutorial CTA treatment.
## Current Tutorial CTA Treatment (lines 1311-1428)
When `sessionMode.type === 'progression' && tutorialRequired`:
1. **Visual Design:**
- Green gradient background with border
- 🌟 icon
- "You've unlocked: [skill name]" heading
- "Start with a quick tutorial" subtitle
- Green gradient button: "🎓 Begin Tutorial →"
2. **Behavior:**
- Replaces the regular "Let's Go!" button
- Clicking opens the SkillTutorialLauncher
## Proposed Remediation CTA
When `sessionMode.type === 'remediation'`:
1. **Visual Design:**
- Amber/orange gradient background with border (warm "focus" colors)
- 💪 icon (strength/building)
- "Time to build strength!" heading
- "Focusing on [N] skills that need practice" subtitle
- Show weak skill badges with pKnown percentages
- Amber gradient button: "💪 Start Focus Practice →"
2. **Behavior:**
- Replaces the regular "Let's Go!" button
- Clicking goes straight to practice (no separate launcher needed)
- The session will automatically target weak skills via sessionMode
## Implementation Steps
### Step 1: Add remediation detection
```typescript
// Derive whether to show remediation CTA
const showRemediationCta = sessionMode.type === 'remediation' && sessionMode.weakSkills.length > 0
```
### Step 2: Create RemediationCta component section
Add after the Tutorial CTA section (line ~1428), or restructure to have a single "special CTA" section that handles both cases.
```tsx
{/* Remediation CTA - Weak skills need strengthening */}
{showRemediationCta && !showTutorialGate && (
<div
data-element="remediation-cta"
className={css({...})}
style={{
background: isDark
? 'linear-gradient(135deg, rgba(245, 158, 11, 0.12) 0%, rgba(234, 88, 12, 0.08) 100%)'
: 'linear-gradient(135deg, rgba(245, 158, 11, 0.08) 0%, rgba(234, 88, 12, 0.05) 100%)',
border: `2px solid ${isDark ? 'rgba(245, 158, 11, 0.25)' : 'rgba(245, 158, 11, 0.2)'}`,
}}
>
{/* Info section */}
<div className={css({...})}>
<span>💪</span>
<div>
<p>Time to build strength!</p>
<p>Focusing on {weakSkills.length} skill{weakSkills.length > 1 ? 's' : ''} that need practice</p>
</div>
</div>
{/* Weak skills badges */}
<div className={css({...})}>
{sessionMode.weakSkills.slice(0, 4).map((skill) => (
<span key={skill.skillId} className={css({...})}>
{skill.displayName} ({Math.round(skill.pKnown * 100)}%)
</span>
))}
{sessionMode.weakSkills.length > 4 && (
<span>+{sessionMode.weakSkills.length - 4} more</span>
)}
</div>
{/* Integrated start button */}
<button
data-action="start-focus-practice"
onClick={handleStart}
disabled={isStarting}
style={{
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
}}
>
{isStarting ? 'Starting...' : (
<>
<span>💪</span>
<span>Start Focus Practice</span>
<span></span>
</>
)}
</button>
</div>
)}
```
### Step 3: Update start button visibility logic
Change from:
```tsx
{!showTutorialGate && (
<button>Let's Go! →</button>
)}
```
To:
```tsx
{!showTutorialGate && !showRemediationCta && (
<button>Let's Go! →</button>
)}
```
## Visual Comparison
| Mode | Icon | Color Theme | Heading | Button Text |
|------|------|-------------|---------|-------------|
| Tutorial | 🌟 | Green | "You've unlocked: [skill]" | "🎓 Begin Tutorial →" |
| Remediation | 💪 | Amber | "Time to build strength!" | "💪 Start Focus Practice →" |
| Normal | - | Blue | "Ready to practice?" | "Let's Go! →" |
## Files to Modify
1. `apps/web/src/components/practice/StartPracticeModal.tsx`
- Add `showRemediationCta` derived state
- Add Remediation CTA section (similar structure to Tutorial CTA)
- Update regular start button visibility condition
## Testing Considerations
1. Storybook stories should cover:
- Remediation mode with 1 weak skill
- Remediation mode with 3+ weak skills
- Remediation mode with 5+ weak skills (overflow)
2. The existing `StartPracticeModal.stories.tsx` already has sessionMode mocks - add remediation variants.
## Accessibility
- Ensure proper ARIA labels on the remediation CTA
- Color contrast should meet WCAG guidelines (amber text on amber background needs checking)
- Screen reader should announce the focus practice intent

View File

@ -108,6 +108,25 @@ const mockRemediationMode: RemediationMode = {
focusDescription: 'Strengthening: +3 and +4',
}
const mockRemediationModeSingleSkill: RemediationMode = {
type: 'remediation',
weakSkills: [{ skillId: 'add-2', displayName: '+2', pKnown: 0.28 }],
focusDescription: 'Strengthening: +2',
}
const mockRemediationModeManySkills: RemediationMode = {
type: 'remediation',
weakSkills: [
{ skillId: 'add-1', displayName: '+1', pKnown: 0.31 },
{ skillId: 'add-2', displayName: '+2', pKnown: 0.38 },
{ skillId: 'add-3', displayName: '+3', pKnown: 0.25 },
{ skillId: 'add-4', displayName: '+4', pKnown: 0.42 },
{ skillId: 'sub-1', displayName: '-1', pKnown: 0.33 },
{ skillId: 'sub-2', displayName: '-2', pKnown: 0.29 },
],
focusDescription: 'Strengthening: +1, +2, +3, +4, -1, -2',
}
// Default props
const defaultProps = {
studentId: 'test-student-1',
@ -183,7 +202,7 @@ export const DarkTheme: Story = {
}
/**
* Remediation mode - student has weak skills to strengthen
* Remediation mode - student has weak skills to strengthen (2 skills)
*/
export const RemediationMode: Story = {
render: () => (
@ -198,6 +217,56 @@ export const RemediationMode: Story = {
),
}
/**
* Remediation mode with a single weak skill
*/
export const RemediationModeSingleSkill: Story = {
render: () => (
<StoryWrapper>
<StartPracticeModal
{...defaultProps}
studentName="Jordan"
sessionMode={mockRemediationModeSingleSkill}
focusDescription={mockRemediationModeSingleSkill.focusDescription}
/>
</StoryWrapper>
),
}
/**
* Remediation mode with many weak skills (shows overflow)
*/
export const RemediationModeManySkills: Story = {
render: () => (
<StoryWrapper>
<StartPracticeModal
{...defaultProps}
studentName="Riley"
sessionMode={mockRemediationModeManySkills}
focusDescription={mockRemediationModeManySkills.focusDescription}
/>
</StoryWrapper>
),
}
/**
* Remediation mode - dark theme
*/
export const RemediationModeDark: Story = {
render: () => (
<StoryWrapper theme="dark">
<div data-theme="dark">
<StartPracticeModal
{...defaultProps}
studentName="Alex"
sessionMode={mockRemediationMode}
focusDescription={mockRemediationMode.focusDescription}
/>
</div>
</StoryWrapper>
),
}
/**
* Progression mode - student is ready to learn a new skill
*/

View File

@ -83,6 +83,10 @@ export function StartPracticeModal({
// Whether to show the tutorial gate prompt
const showTutorialGate = !!tutorialConfig && !showTutorial
// Whether to show the remediation CTA (weak skills need strengthening)
const showRemediationCta =
sessionMode.type === 'remediation' && sessionMode.weakSkills.length > 0
// Get skill info for tutorial from sessionMode
const nextSkill = sessionMode.type === 'progression' ? sessionMode.nextSkill : null
@ -1427,6 +1431,181 @@ export function StartPracticeModal({
</div>
)}
{/* Remediation CTA - Weak skills need strengthening */}
{showRemediationCta && !showTutorialGate && sessionMode.type === 'remediation' && (
<div
data-element="remediation-cta"
className={css({
borderRadius: '12px',
overflow: 'hidden',
'@media (max-height: 700px)': {
borderRadius: '10px',
marginTop: 'auto',
},
})}
style={{
background: isDark
? 'linear-gradient(135deg, rgba(245, 158, 11, 0.12) 0%, rgba(234, 88, 12, 0.08) 100%)'
: 'linear-gradient(135deg, rgba(245, 158, 11, 0.08) 0%, rgba(234, 88, 12, 0.05) 100%)',
border: `2px solid ${isDark ? 'rgba(245, 158, 11, 0.25)' : 'rgba(245, 158, 11, 0.2)'}`,
}}
>
{/* Info section */}
<div
className={css({
padding: '0.875rem 1rem',
display: 'flex',
gap: '0.625rem',
alignItems: 'flex-start',
'@media (max-height: 700px)': {
padding: '0.5rem 0.75rem',
gap: '0.5rem',
},
})}
>
<span
className={css({
fontSize: '1.5rem',
lineHeight: 1,
'@media (max-height: 700px)': {
fontSize: '1.25rem',
},
})}
>
💪
</span>
<div className={css({ flex: 1 })}>
<p
className={css({
fontSize: '0.875rem',
fontWeight: '600',
'@media (max-height: 700px)': {
fontSize: '0.8125rem',
},
})}
style={{ color: isDark ? '#fcd34d' : '#b45309' }}
>
Time to build strength!
</p>
<p
className={css({
fontSize: '0.75rem',
marginTop: '0.125rem',
'@media (max-height: 700px)': {
fontSize: '0.6875rem',
},
})}
style={{ color: isDark ? '#a1a1aa' : '#6b7280' }}
>
Focusing on {sessionMode.weakSkills.length} skill
{sessionMode.weakSkills.length > 1 ? 's' : ''} that need practice
</p>
{/* Weak skills badges */}
<div
className={css({
display: 'flex',
flexWrap: 'wrap',
gap: '0.25rem',
marginTop: '0.5rem',
'@media (max-height: 700px)': {
marginTop: '0.375rem',
gap: '0.1875rem',
},
})}
>
{sessionMode.weakSkills.slice(0, 4).map((skill) => (
<span
key={skill.skillId}
data-skill={skill.skillId}
className={css({
fontSize: '0.625rem',
padding: '0.125rem 0.375rem',
borderRadius: '4px',
'@media (max-height: 700px)': {
fontSize: '0.5625rem',
padding: '0.0625rem 0.25rem',
},
})}
style={{
backgroundColor: isDark
? 'rgba(245, 158, 11, 0.2)'
: 'rgba(245, 158, 11, 0.15)',
color: isDark ? '#fcd34d' : '#92400e',
}}
>
{skill.displayName}{' '}
<span style={{ opacity: 0.7 }}>
({Math.round(skill.pKnown * 100)}%)
</span>
</span>
))}
{sessionMode.weakSkills.length > 4 && (
<span
className={css({
fontSize: '0.625rem',
padding: '0.125rem 0.375rem',
'@media (max-height: 700px)': {
fontSize: '0.5625rem',
padding: '0.0625rem 0.25rem',
},
})}
style={{ color: isDark ? '#a1a1aa' : '#6b7280' }}
>
+{sessionMode.weakSkills.length - 4} more
</span>
)}
</div>
</div>
</div>
{/* Integrated start button */}
<button
type="button"
data-action="start-focus-practice"
data-status={isStarting ? 'starting' : 'ready'}
onClick={handleStart}
disabled={isStarting}
className={css({
width: '100%',
padding: '0.875rem',
fontSize: '1rem',
fontWeight: 'bold',
color: 'white',
border: 'none',
borderRadius: '0 0 10px 10px',
cursor: isStarting ? 'not-allowed' : 'pointer',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0.5rem',
_hover: {
filter: isStarting ? 'none' : 'brightness(1.05)',
},
'@media (max-height: 700px)': {
padding: '0.75rem',
fontSize: '0.9375rem',
},
})}
style={{
background: isStarting
? '#9ca3af'
: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
boxShadow: isStarting ? 'none' : 'inset 0 1px 0 rgba(255,255,255,0.15)',
}}
>
{isStarting ? (
'Starting...'
) : (
<>
<span>💪</span>
<span>Start Focus Practice</span>
<span></span>
</>
)}
</button>
</div>
)}
{/* Error display */}
{displayError && (
<div
@ -1489,8 +1668,8 @@ export function StartPracticeModal({
</div>
)}
{/* Start button - only shown when no tutorial is pending */}
{!showTutorialGate && (
{/* Start button - only shown when no special CTA is active */}
{!showTutorialGate && !showRemediationCta && (
<button
type="button"
data-action="start-practice"