feat: create unified skill configuration interface with intuitive modes

- Replace separate required/target/forbidden sections with single interface
- Implement 4 clear skill modes: Off, Allowed, Target, Forbidden
- Add click-to-cycle interaction with visual icons and color coding
- Provide conversion utilities between old and new configuration formats
- Include clear legend explaining each mode's purpose
- Maintain backward compatibility with existing problem generation logic

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-21 08:07:38 -05:00
parent c01f968ff7
commit fc79540f78
2 changed files with 285 additions and 48 deletions

View File

@@ -1,54 +1,51 @@
'use client'
import { useCallback } from 'react'
import { css } from '../../styled-system/css'
import { hstack, vstack } from '../../styled-system/patterns'
import { SkillSet } from '../../types/tutorial'
import { css } from '../../../styled-system/css'
import { hstack, vstack } from '../../../styled-system/patterns'
import { SkillConfiguration, SkillMode } from '../../utils/skillConfiguration'
// Export for convenience
export type { SkillConfiguration, SkillMode } from '../../utils/skillConfiguration'
interface SkillSelectorProps {
skills: SkillSet
onChange: (skills: SkillSet) => void
mode?: 'required' | 'target' | 'forbidden'
skills: SkillConfiguration
onChange: (skills: SkillConfiguration) => void
title?: string
className?: string
}
type SkillMode = 'required' | 'target' | 'forbidden'
export function SkillSelector({
skills,
onChange,
mode = 'required',
title = 'Skills',
title = 'Skill Configuration',
className
}: SkillSelectorProps) {
const updateSkill = useCallback((category: keyof SkillSet, skill: string, enabled: boolean) => {
const updateSkill = useCallback((category: keyof SkillConfiguration, skill: string, mode: SkillMode) => {
const newSkills = { ...skills }
if (category === 'basic') {
newSkills.basic = { ...newSkills.basic, [skill]: enabled }
newSkills.basic = { ...newSkills.basic, [skill]: mode }
} else if (category === 'fiveComplements') {
newSkills.fiveComplements = { ...newSkills.fiveComplements, [skill]: enabled }
newSkills.fiveComplements = { ...newSkills.fiveComplements, [skill]: mode }
} else if (category === 'tenComplements') {
newSkills.tenComplements = { ...newSkills.tenComplements, [skill]: enabled }
newSkills.tenComplements = { ...newSkills.tenComplements, [skill]: mode }
}
onChange(newSkills)
}, [skills, onChange])
const getModeStyles = (skillEnabled: boolean): string => {
if (!skillEnabled) {
return css({
bg: 'gray.100',
color: 'gray.400',
border: '1px solid',
borderColor: 'gray.200'
})
}
switch (mode) {
case 'required':
const getModeStyles = (skillMode: SkillMode): string => {
switch (skillMode) {
case 'off':
return css({
bg: 'gray.100',
color: 'gray.400',
border: '1px solid',
borderColor: 'gray.200'
})
case 'allowed':
return css({
bg: 'green.100',
color: 'green.800',
@@ -79,19 +76,35 @@ export function SkillSelector({
}
}
const getModeIcon = (skillMode: SkillMode): string => {
switch (skillMode) {
case 'off': return '⚫'
case 'allowed': return '✅'
case 'target': return '🎯'
case 'forbidden': return '❌'
default: return '⚫'
}
}
const getNextMode = (currentMode: SkillMode): SkillMode => {
const modes: SkillMode[] = ['off', 'allowed', 'target', 'forbidden']
const currentIndex = modes.indexOf(currentMode)
return modes[(currentIndex + 1) % modes.length]
}
const SkillButton = ({
category,
skill,
label,
enabled
mode
}: {
category: keyof SkillSet
category: keyof SkillConfiguration
skill: string
label: string
enabled: boolean
mode: SkillMode
}) => (
<button
onClick={() => updateSkill(category, skill, !enabled)}
onClick={() => updateSkill(category, skill, getNextMode(mode))}
className={css({
px: 3,
py: 2,
@@ -100,13 +113,15 @@ export function SkillSelector({
fontWeight: 'medium',
cursor: 'pointer',
transition: 'all 0.2s',
_hover: { opacity: 0.8 }
}, getModeStyles(enabled))}
_hover: { opacity: 0.8 },
display: 'flex',
alignItems: 'center',
gap: 2
}, getModeStyles(mode))}
title={`Click to cycle: ${mode}${getNextMode(mode)}`}
>
{enabled && mode === 'required' && '✅ '}
{enabled && mode === 'target' && '🎯 '}
{enabled && mode === 'forbidden' && '❌ '}
{label}
<span>{getModeIcon(mode)}</span>
<span>{label}</span>
</button>
)
@@ -143,19 +158,19 @@ export function SkillSelector({
category="basic"
skill="directAddition"
label="Direct Addition (1-4)"
enabled={skills.basic.directAddition}
mode={skills.basic.directAddition}
/>
<SkillButton
category="basic"
skill="heavenBead"
label="Heaven Bead (5)"
enabled={skills.basic.heavenBead}
mode={skills.basic.heavenBead}
/>
<SkillButton
category="basic"
skill="simpleCombinations"
label="Simple Combinations (6-9)"
enabled={skills.basic.simpleCombinations}
mode={skills.basic.simpleCombinations}
/>
</div>
</div>
@@ -175,25 +190,25 @@ export function SkillSelector({
category="fiveComplements"
skill="4=5-1"
label="4 = 5 - 1"
enabled={skills.fiveComplements["4=5-1"]}
mode={skills.fiveComplements["4=5-1"]}
/>
<SkillButton
category="fiveComplements"
skill="3=5-2"
label="3 = 5 - 2"
enabled={skills.fiveComplements["3=5-2"]}
mode={skills.fiveComplements["3=5-2"]}
/>
<SkillButton
category="fiveComplements"
skill="2=5-3"
label="2 = 5 - 3"
enabled={skills.fiveComplements["2=5-3"]}
mode={skills.fiveComplements["2=5-3"]}
/>
<SkillButton
category="fiveComplements"
skill="1=5-4"
label="1 = 5 - 4"
enabled={skills.fiveComplements["1=5-4"]}
mode={skills.fiveComplements["1=5-4"]}
/>
</div>
</div>
@@ -209,13 +224,13 @@ export function SkillSelector({
Ten Complements
</h5>
<div className={hstack({ gap: 2, flexWrap: 'wrap' })}>
{Object.entries(skills.tenComplements).map(([complement, enabled]) => (
{Object.entries(skills.tenComplements).map(([complement, mode]) => (
<SkillButton
key={complement}
category="tenComplements"
skill={complement}
label={complement}
enabled={enabled}
mode={mode}
/>
))}
</div>
@@ -231,9 +246,21 @@ export function SkillSelector({
fontSize: 'xs',
color: 'gray.600'
})}>
{mode === 'required' && '✅ Skills the user must know to solve problems'}
{mode === 'target' && '🎯 Skills to specifically practice (generates problems requiring these)'}
{mode === 'forbidden' && '❌ Skills the user hasn\'t learned yet (problems won\'t require these)'}
<div className={css({ fontWeight: 'medium', mb: 2 })}>Click skills to cycle through modes:</div>
<div className={hstack({ gap: 4, flexWrap: 'wrap' })}>
<div className={hstack({ gap: 1, alignItems: 'center' })}>
<span></span><span>Off - Not used</span>
</div>
<div className={hstack({ gap: 1, alignItems: 'center' })}>
<span></span><span>Allowed - Can be used</span>
</div>
<div className={hstack({ gap: 1, alignItems: 'center' })}>
<span>🎯</span><span>Target - Focus practice</span>
</div>
<div className={hstack({ gap: 1, alignItems: 'center' })}>
<span></span><span>Forbidden - Not learned yet</span>
</div>
</div>
</div>
</div>
)

View File

@@ -0,0 +1,210 @@
import { SkillSet } from '../types/tutorial'
export type SkillMode = 'off' | 'allowed' | 'target' | 'forbidden'
export interface SkillConfiguration {
basic: {
directAddition: SkillMode
heavenBead: SkillMode
simpleCombinations: SkillMode
}
fiveComplements: {
"4=5-1": SkillMode
"3=5-2": SkillMode
"2=5-3": SkillMode
"1=5-4": SkillMode
}
tenComplements: {
"9=10-1": SkillMode
"8=10-2": SkillMode
"7=10-3": SkillMode
"6=10-4": SkillMode
"5=10-5": SkillMode
"4=10-6": SkillMode
"3=10-7": SkillMode
"2=10-8": SkillMode
"1=10-9": SkillMode
}
}
// Helper functions for new skill configuration
export function createDefaultSkillConfiguration(): SkillConfiguration {
return {
basic: {
directAddition: 'allowed',
heavenBead: 'off',
simpleCombinations: 'off'
},
fiveComplements: {
"4=5-1": 'off',
"3=5-2": 'off',
"2=5-3": 'off',
"1=5-4": 'off'
},
tenComplements: {
"9=10-1": 'off',
"8=10-2": 'off',
"7=10-3": 'off',
"6=10-4": 'off',
"5=10-5": 'off',
"4=10-6": 'off',
"3=10-7": 'off',
"2=10-8": 'off',
"1=10-9": 'off'
}
}
}
export function createBasicAllowedConfiguration(): SkillConfiguration {
return {
basic: {
directAddition: 'allowed',
heavenBead: 'allowed',
simpleCombinations: 'off'
},
fiveComplements: {
"4=5-1": 'off',
"3=5-2": 'off',
"2=5-3": 'off',
"1=5-4": 'off'
},
tenComplements: {
"9=10-1": 'off',
"8=10-2": 'off',
"7=10-3": 'off',
"6=10-4": 'off',
"5=10-5": 'off',
"4=10-6": 'off',
"3=10-7": 'off',
"2=10-8": 'off',
"1=10-9": 'off'
}
}
}
// Convert between old and new formats
export function skillConfigurationToSkillSets(config: SkillConfiguration): {
required: SkillSet
target: Partial<SkillSet>
forbidden: Partial<SkillSet>
} {
const required: SkillSet = {
basic: {
directAddition: false,
heavenBead: false,
simpleCombinations: false
},
fiveComplements: {
"4=5-1": false,
"3=5-2": false,
"2=5-3": false,
"1=5-4": false
},
tenComplements: {
"9=10-1": false,
"8=10-2": false,
"7=10-3": false,
"6=10-4": false,
"5=10-5": false,
"4=10-6": false,
"3=10-7": false,
"2=10-8": false,
"1=10-9": false
}
}
const target: Partial<SkillSet> = { basic: {}, fiveComplements: {}, tenComplements: {} }
const forbidden: Partial<SkillSet> = { basic: {}, fiveComplements: {}, tenComplements: {} }
// Basic skills
Object.entries(config.basic).forEach(([skill, mode]) => {
if (mode === 'allowed' || mode === 'target') {
required.basic[skill as keyof typeof required.basic] = true
}
if (mode === 'target') {
target.basic![skill as keyof typeof target.basic] = true
}
if (mode === 'forbidden') {
forbidden.basic![skill as keyof typeof forbidden.basic] = true
}
})
// Five complements
Object.entries(config.fiveComplements).forEach(([skill, mode]) => {
if (mode === 'allowed' || mode === 'target') {
required.fiveComplements[skill as keyof typeof required.fiveComplements] = true
}
if (mode === 'target') {
target.fiveComplements![skill as keyof typeof target.fiveComplements] = true
}
if (mode === 'forbidden') {
forbidden.fiveComplements![skill as keyof typeof forbidden.fiveComplements] = true
}
})
// Ten complements
Object.entries(config.tenComplements).forEach(([skill, mode]) => {
if (mode === 'allowed' || mode === 'target') {
required.tenComplements[skill as keyof typeof required.tenComplements] = true
}
if (mode === 'target') {
target.tenComplements![skill as keyof typeof target.tenComplements] = true
}
if (mode === 'forbidden') {
forbidden.tenComplements![skill as keyof typeof forbidden.tenComplements] = true
}
})
return { required, target, forbidden }
}
// Convert from old format to new format
export function skillSetsToConfiguration(
required: SkillSet,
target?: Partial<SkillSet>,
forbidden?: Partial<SkillSet>
): SkillConfiguration {
const config = createDefaultSkillConfiguration()
// Process each skill category
Object.entries(required.basic).forEach(([skill, isRequired]) => {
const skillKey = skill as keyof typeof config.basic
if (forbidden?.basic?.[skillKey]) {
config.basic[skillKey] = 'forbidden'
} else if (target?.basic?.[skillKey]) {
config.basic[skillKey] = 'target'
} else if (isRequired) {
config.basic[skillKey] = 'allowed'
} else {
config.basic[skillKey] = 'off'
}
})
Object.entries(required.fiveComplements).forEach(([skill, isRequired]) => {
const skillKey = skill as keyof typeof config.fiveComplements
if (forbidden?.fiveComplements?.[skillKey]) {
config.fiveComplements[skillKey] = 'forbidden'
} else if (target?.fiveComplements?.[skillKey]) {
config.fiveComplements[skillKey] = 'target'
} else if (isRequired) {
config.fiveComplements[skillKey] = 'allowed'
} else {
config.fiveComplements[skillKey] = 'off'
}
})
Object.entries(required.tenComplements).forEach(([skill, isRequired]) => {
const skillKey = skill as keyof typeof config.tenComplements
if (forbidden?.tenComplements?.[skillKey]) {
config.tenComplements[skillKey] = 'forbidden'
} else if (target?.tenComplements?.[skillKey]) {
config.tenComplements[skillKey] = 'target'
} else if (isRequired) {
config.tenComplements[skillKey] = 'allowed'
} else {
config.tenComplements[skillKey] = 'off'
}
})
return config
}