feat: integrate unified skill configuration interface into practice step editor
Updates PracticeStepEditor to use the new SkillSelector component with unified skill modes (off/allowed/target/forbidden) instead of separate required/target skill sections. This provides a more intuitive and compact interface for configuring practice steps. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { vstack, hstack } from '../../styled-system/patterns'
|
||||
import { PracticeStep, SkillSet, createBasicSkillSet, createEmptySkillSet } from '../../types/tutorial'
|
||||
import { SkillSelector } from './SkillSelector'
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { vstack, hstack } from '../../../styled-system/patterns'
|
||||
import { PracticeStep, createBasicSkillSet } from '../../types/tutorial'
|
||||
import { SkillSelector, SkillConfiguration } from './SkillSelector'
|
||||
import { validatePracticeStepConfiguration, generateSingleProblem } from '../../utils/problemGenerator'
|
||||
import { createBasicAllowedConfiguration, skillConfigurationToSkillSets } from '../../utils/skillConfiguration'
|
||||
import type { GeneratedProblem } from '../../utils/problemGenerator'
|
||||
|
||||
interface PracticeStepEditorProps {
|
||||
step: PracticeStep
|
||||
@@ -20,21 +23,25 @@ export function PracticeStepEditor({
|
||||
className
|
||||
}: PracticeStepEditorProps) {
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
const [sampleProblems, setSampleProblems] = useState<GeneratedProblem[]>([])
|
||||
const [validationResult, setValidationResult] = useState<ReturnType<typeof validatePracticeStepConfiguration> | null>(null)
|
||||
const [skillConfig, setSkillConfig] = useState<SkillConfiguration>(() => {
|
||||
// Initialize with a basic configuration for new steps or convert from existing
|
||||
return createBasicAllowedConfiguration()
|
||||
})
|
||||
|
||||
const updateStep = useCallback((updates: Partial<PracticeStep>) => {
|
||||
onChange({ ...step, ...updates })
|
||||
}, [step, onChange])
|
||||
|
||||
const updateRequiredSkills = useCallback((skills: SkillSet) => {
|
||||
updateStep({ requiredSkills: skills })
|
||||
}, [updateStep])
|
||||
|
||||
const updateTargetSkills = useCallback((skills: Partial<SkillSet>) => {
|
||||
updateStep({ targetSkills: skills })
|
||||
}, [updateStep])
|
||||
|
||||
const updateForbiddenSkills = useCallback((skills: Partial<SkillSet>) => {
|
||||
updateStep({ forbiddenSkills: skills })
|
||||
const updateSkillConfiguration = useCallback((config: SkillConfiguration) => {
|
||||
setSkillConfig(config)
|
||||
const { required, target, forbidden } = skillConfigurationToSkillSets(config)
|
||||
updateStep({
|
||||
requiredSkills: required,
|
||||
targetSkills: target,
|
||||
forbiddenSkills: forbidden
|
||||
})
|
||||
}, [updateStep])
|
||||
|
||||
// Convert partial skill sets to full skill sets for the selector
|
||||
@@ -63,33 +70,71 @@ export function PracticeStepEditor({
|
||||
}
|
||||
}
|
||||
|
||||
// Validate configuration when step changes
|
||||
useEffect(() => {
|
||||
const result = validatePracticeStepConfiguration(step)
|
||||
setValidationResult(result)
|
||||
}, [step])
|
||||
|
||||
// Generate sample problems
|
||||
const generateSampleProblems = useCallback(() => {
|
||||
const samples: GeneratedProblem[] = []
|
||||
const maxSamples = Math.min(3, step.problemCount) // Show up to 3 samples
|
||||
const { required, target, forbidden } = skillConfigurationToSkillSets(skillConfig)
|
||||
|
||||
for (let i = 0; i < maxSamples; i++) {
|
||||
const problem = generateSingleProblem(
|
||||
{
|
||||
numberRange: step.numberRange || { min: 1, max: 9 },
|
||||
maxSum: step.sumConstraints?.maxSum,
|
||||
minSum: step.sumConstraints?.minSum,
|
||||
maxTerms: step.maxTerms,
|
||||
problemCount: step.problemCount
|
||||
},
|
||||
required,
|
||||
target,
|
||||
forbidden,
|
||||
50 // More attempts for samples
|
||||
)
|
||||
|
||||
if (problem) {
|
||||
samples.push(problem)
|
||||
}
|
||||
}
|
||||
|
||||
setSampleProblems(samples)
|
||||
}, [step, skillConfig])
|
||||
|
||||
const presetConfigurations = [
|
||||
{
|
||||
name: 'Basic Addition (1-4)',
|
||||
skills: createBasicSkillSet()
|
||||
name: 'Basic Addition Only',
|
||||
config: {
|
||||
...createBasicAllowedConfiguration(),
|
||||
basic: { directAddition: 'allowed', heavenBead: 'off', simpleCombinations: 'off' }
|
||||
} as SkillConfiguration
|
||||
},
|
||||
{
|
||||
name: 'With Heaven Bead',
|
||||
skills: {
|
||||
...createBasicSkillSet(),
|
||||
basic: { ...createBasicSkillSet().basic, heavenBead: true, simpleCombinations: true }
|
||||
}
|
||||
name: 'Practice Heaven Bead',
|
||||
config: {
|
||||
...createBasicAllowedConfiguration(),
|
||||
basic: { directAddition: 'allowed', heavenBead: 'target', simpleCombinations: 'allowed' }
|
||||
} as SkillConfiguration
|
||||
},
|
||||
{
|
||||
name: 'First Five Complement (4=5-1)',
|
||||
skills: {
|
||||
...createBasicSkillSet(),
|
||||
basic: { directAddition: true, heavenBead: true, simpleCombinations: true },
|
||||
fiveComplements: { ...createEmptySkillSet().fiveComplements, "4=5-1": true }
|
||||
}
|
||||
name: 'Learn Five Complements',
|
||||
config: {
|
||||
...createBasicAllowedConfiguration(),
|
||||
basic: { directAddition: 'allowed', heavenBead: 'allowed', simpleCombinations: 'allowed' },
|
||||
fiveComplements: { "4=5-1": 'target', "3=5-2": 'target', "2=5-3": 'off', "1=5-4": 'off' }
|
||||
} as SkillConfiguration
|
||||
},
|
||||
{
|
||||
name: 'All Five Complements',
|
||||
skills: {
|
||||
...createBasicSkillSet(),
|
||||
basic: { directAddition: true, heavenBead: true, simpleCombinations: true },
|
||||
fiveComplements: { "4=5-1": true, "3=5-2": true, "2=5-3": true, "1=5-4": true }
|
||||
}
|
||||
name: 'All Basic Skills',
|
||||
config: {
|
||||
...createBasicAllowedConfiguration(),
|
||||
basic: { directAddition: 'allowed', heavenBead: 'allowed', simpleCombinations: 'allowed' },
|
||||
fiveComplements: { "4=5-1": 'allowed', "3=5-2": 'allowed', "2=5-3": 'allowed', "1=5-4": 'allowed' }
|
||||
} as SkillConfiguration
|
||||
}
|
||||
]
|
||||
|
||||
@@ -260,7 +305,7 @@ export function PracticeStepEditor({
|
||||
{presetConfigurations.map((preset) => (
|
||||
<button
|
||||
key={preset.name}
|
||||
onClick={() => updateRequiredSkills(preset.skills)}
|
||||
onClick={() => updateSkillConfiguration(preset.config)}
|
||||
className={css({
|
||||
px: 3,
|
||||
py: 2,
|
||||
@@ -280,12 +325,11 @@ export function PracticeStepEditor({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Required Skills */}
|
||||
{/* Unified Skill Configuration */}
|
||||
<SkillSelector
|
||||
skills={step.requiredSkills}
|
||||
onChange={updateRequiredSkills}
|
||||
mode="required"
|
||||
title="Required Skills (User Must Know)"
|
||||
skills={skillConfig}
|
||||
onChange={updateSkillConfiguration}
|
||||
title="Skill Configuration"
|
||||
/>
|
||||
|
||||
{/* Advanced Options Toggle */}
|
||||
@@ -304,172 +348,203 @@ export function PracticeStepEditor({
|
||||
_hover: { bg: 'gray.200' }
|
||||
})}
|
||||
>
|
||||
{showAdvanced ? '▼' : '▶'} Advanced Options
|
||||
{showAdvanced ? '▼' : '▶'} Advanced Constraints
|
||||
</button>
|
||||
|
||||
{/* Advanced Options */}
|
||||
{showAdvanced && (
|
||||
<div className={vstack({ gap: 4, alignItems: 'stretch' })}>
|
||||
{/* Target Skills */}
|
||||
<SkillSelector
|
||||
skills={targetSkillsForSelector}
|
||||
onChange={(skills) => updateTargetSkills(skills)}
|
||||
mode="target"
|
||||
title="Target Skills (Specific Practice Focus)"
|
||||
/>
|
||||
<div className={vstack({ gap: 3, alignItems: 'stretch' })}>
|
||||
<h5 className={css({
|
||||
fontSize: 'md',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700'
|
||||
})}>
|
||||
Number & Sum Constraints
|
||||
</h5>
|
||||
|
||||
{/* Constraints */}
|
||||
<div className={vstack({ gap: 3, alignItems: 'stretch' })}>
|
||||
<h5 className={css({
|
||||
fontSize: 'md',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700'
|
||||
})}>
|
||||
Problem Constraints
|
||||
</h5>
|
||||
|
||||
<div className={hstack({ gap: 4 })}>
|
||||
<div className={css({ flex: 1 })}>
|
||||
<label className={css({
|
||||
display: 'block',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700',
|
||||
mb: 1
|
||||
})}>
|
||||
Number Range Min
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={99}
|
||||
value={step.numberRange?.min || 1}
|
||||
onChange={(e) => updateStep({
|
||||
numberRange: {
|
||||
...step.numberRange,
|
||||
min: parseInt(e.target.value) || 1,
|
||||
max: step.numberRange?.max || 9
|
||||
}
|
||||
})}
|
||||
className={css({
|
||||
w: 'full',
|
||||
px: 3,
|
||||
py: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
rounded: 'md',
|
||||
fontSize: 'sm'
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={css({ flex: 1 })}>
|
||||
<label className={css({
|
||||
display: 'block',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700',
|
||||
mb: 1
|
||||
})}>
|
||||
Number Range Max
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={99}
|
||||
value={step.numberRange?.max || 9}
|
||||
onChange={(e) => updateStep({
|
||||
numberRange: {
|
||||
min: step.numberRange?.min || 1,
|
||||
max: parseInt(e.target.value) || 9
|
||||
}
|
||||
})}
|
||||
className={css({
|
||||
w: 'full',
|
||||
px: 3,
|
||||
py: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
rounded: 'md',
|
||||
fontSize: 'sm'
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className={hstack({ gap: 4 })}>
|
||||
<div className={css({ flex: 1 })}>
|
||||
<label className={css({
|
||||
display: 'block',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700',
|
||||
mb: 1
|
||||
})}>
|
||||
Number Range Min
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={99}
|
||||
value={step.numberRange?.min || 1}
|
||||
onChange={(e) => updateStep({
|
||||
numberRange: {
|
||||
...step.numberRange,
|
||||
min: parseInt(e.target.value) || 1,
|
||||
max: step.numberRange?.max || 9
|
||||
}
|
||||
})}
|
||||
className={css({
|
||||
w: 'full',
|
||||
px: 3,
|
||||
py: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
rounded: 'md',
|
||||
fontSize: 'sm'
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={hstack({ gap: 4 })}>
|
||||
<div className={css({ flex: 1 })}>
|
||||
<label className={css({
|
||||
display: 'block',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700',
|
||||
mb: 1
|
||||
})}>
|
||||
Maximum Sum
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={999}
|
||||
value={step.sumConstraints?.maxSum || 9}
|
||||
onChange={(e) => updateStep({
|
||||
sumConstraints: {
|
||||
...step.sumConstraints,
|
||||
maxSum: parseInt(e.target.value) || 9
|
||||
}
|
||||
})}
|
||||
className={css({
|
||||
w: 'full',
|
||||
px: 3,
|
||||
py: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
rounded: 'md',
|
||||
fontSize: 'sm'
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className={css({ flex: 1 })}>
|
||||
<label className={css({
|
||||
display: 'block',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700',
|
||||
mb: 1
|
||||
})}>
|
||||
Number Range Max
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={99}
|
||||
value={step.numberRange?.max || 9}
|
||||
onChange={(e) => updateStep({
|
||||
numberRange: {
|
||||
min: step.numberRange?.min || 1,
|
||||
max: parseInt(e.target.value) || 9
|
||||
}
|
||||
})}
|
||||
className={css({
|
||||
w: 'full',
|
||||
px: 3,
|
||||
py: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
rounded: 'md',
|
||||
fontSize: 'sm'
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={css({ flex: 1 })}>
|
||||
<label className={css({
|
||||
display: 'block',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700',
|
||||
mb: 1
|
||||
})}>
|
||||
Minimum Sum (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={999}
|
||||
value={step.sumConstraints?.minSum || ''}
|
||||
onChange={(e) => updateStep({
|
||||
sumConstraints: {
|
||||
...step.sumConstraints,
|
||||
minSum: e.target.value ? parseInt(e.target.value) : undefined
|
||||
}
|
||||
})}
|
||||
className={css({
|
||||
w: 'full',
|
||||
px: 3,
|
||||
py: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
rounded: 'md',
|
||||
fontSize: 'sm'
|
||||
})}
|
||||
placeholder="Leave empty for no minimum"
|
||||
/>
|
||||
</div>
|
||||
<div className={hstack({ gap: 4 })}>
|
||||
<div className={css({ flex: 1 })}>
|
||||
<label className={css({
|
||||
display: 'block',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700',
|
||||
mb: 1
|
||||
})}>
|
||||
Maximum Sum
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={999}
|
||||
value={step.sumConstraints?.maxSum || 9}
|
||||
onChange={(e) => updateStep({
|
||||
sumConstraints: {
|
||||
...step.sumConstraints,
|
||||
maxSum: parseInt(e.target.value) || 9
|
||||
}
|
||||
})}
|
||||
className={css({
|
||||
w: 'full',
|
||||
px: 3,
|
||||
py: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
rounded: 'md',
|
||||
fontSize: 'sm'
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={css({ flex: 1 })}>
|
||||
<label className={css({
|
||||
display: 'block',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700',
|
||||
mb: 1
|
||||
})}>
|
||||
Minimum Sum (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={999}
|
||||
value={step.sumConstraints?.minSum || ''}
|
||||
onChange={(e) => updateStep({
|
||||
sumConstraints: {
|
||||
...step.sumConstraints,
|
||||
minSum: e.target.value ? parseInt(e.target.value) : undefined
|
||||
}
|
||||
})}
|
||||
className={css({
|
||||
w: 'full',
|
||||
px: 3,
|
||||
py: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
rounded: 'md',
|
||||
fontSize: 'sm'
|
||||
})}
|
||||
placeholder="Leave empty for no minimum"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Problem Preview */}
|
||||
{/* Validation Results */}
|
||||
{validationResult && (
|
||||
<div className={css({
|
||||
p: 3,
|
||||
bg: validationResult.isValid ? 'green.50' : 'yellow.50',
|
||||
border: '1px solid',
|
||||
borderColor: validationResult.isValid ? 'green.200' : 'yellow.200',
|
||||
rounded: 'md'
|
||||
})}>
|
||||
<h5 className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: validationResult.isValid ? 'green.800' : 'yellow.800',
|
||||
mb: 2
|
||||
})}>
|
||||
{validationResult.isValid ? '✅ Configuration Valid' : '⚠️ Configuration Warnings'}
|
||||
</h5>
|
||||
|
||||
{validationResult.warnings.length > 0 && (
|
||||
<div className={css({ mb: 2 })}>
|
||||
<strong className={css({ fontSize: 'xs', color: 'yellow.800' })}>Warnings:</strong>
|
||||
<ul className={css({ fontSize: 'xs', color: 'yellow.700', pl: 4, mt: 1 })}>
|
||||
{validationResult.warnings.map((warning, index) => (
|
||||
<li key={index}>• {warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{validationResult.suggestions.length > 0 && (
|
||||
<div>
|
||||
<strong className={css({ fontSize: 'xs', color: 'blue.800' })}>Suggestions:</strong>
|
||||
<ul className={css({ fontSize: 'xs', color: 'blue.700', pl: 4, mt: 1 })}>
|
||||
{validationResult.suggestions.map((suggestion, index) => (
|
||||
<li key={index}>• {suggestion}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sample Problems Preview */}
|
||||
<div className={css({
|
||||
p: 3,
|
||||
bg: 'blue.50',
|
||||
@@ -477,15 +552,117 @@ export function PracticeStepEditor({
|
||||
borderColor: 'blue.200',
|
||||
rounded: 'md'
|
||||
})}>
|
||||
<h5 className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'blue.800',
|
||||
mb: 2
|
||||
})}>
|
||||
Configuration Summary
|
||||
</h5>
|
||||
<div className={hstack({ justifyContent: 'space-between', alignItems: 'center', mb: 2 })}>
|
||||
<h5 className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'blue.800'
|
||||
})}>
|
||||
Sample Problems Preview
|
||||
</h5>
|
||||
<button
|
||||
onClick={generateSampleProblems}
|
||||
className={css({
|
||||
px: 2,
|
||||
py: 1,
|
||||
fontSize: 'xs',
|
||||
bg: 'blue.500',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
rounded: 'sm',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'blue.600' }
|
||||
})}
|
||||
>
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{sampleProblems.length > 0 ? (
|
||||
<div className={hstack({ gap: 3, flexWrap: 'wrap', alignItems: 'flex-start' })}>
|
||||
{sampleProblems.map((problem, index) => (
|
||||
<div key={problem.id} className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 1
|
||||
})}>
|
||||
{/* Compact vertical problem display */}
|
||||
<div className={css({
|
||||
textAlign: 'right',
|
||||
fontFamily: 'mono',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'bold',
|
||||
bg: 'gray.50',
|
||||
px: 2,
|
||||
py: 1,
|
||||
rounded: 'sm',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
minW: '40px'
|
||||
})}>
|
||||
{problem.terms.map((term, termIndex) => (
|
||||
<div key={termIndex} className={css({ lineHeight: 'tight' })}>
|
||||
{term}
|
||||
</div>
|
||||
))}
|
||||
<div className={css({
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'gray.400',
|
||||
mt: 0.5,
|
||||
pt: 0.5
|
||||
})}>
|
||||
{problem.answer}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Difficulty badge below */}
|
||||
<span className={css({
|
||||
px: 1,
|
||||
py: 0.5,
|
||||
rounded: 'xs',
|
||||
fontSize: 'xs',
|
||||
bg: problem.difficulty === 'easy' ? 'green.100' :
|
||||
problem.difficulty === 'medium' ? 'yellow.100' : 'red.100',
|
||||
color: problem.difficulty === 'easy' ? 'green.800' :
|
||||
problem.difficulty === 'medium' ? 'yellow.800' : 'red.800'
|
||||
})}>
|
||||
{problem.difficulty}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'blue.700',
|
||||
textAlign: 'center',
|
||||
py: 2
|
||||
})}>
|
||||
Click "Generate" to see sample problems
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Skills summary below problems */}
|
||||
{sampleProblems.length > 0 && (
|
||||
<div className={css({
|
||||
mt: 2,
|
||||
pt: 2,
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'blue.200',
|
||||
fontSize: 'xs',
|
||||
color: 'blue.600'
|
||||
})}>
|
||||
<strong>Skills used:</strong> {[...new Set(sampleProblems.flatMap(p => p.requiredSkills))].join(', ')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Configuration Summary */}
|
||||
<div className={css({
|
||||
mt: 3,
|
||||
pt: 2,
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'blue.200',
|
||||
fontSize: 'xs',
|
||||
color: 'blue.700',
|
||||
lineHeight: 'relaxed'
|
||||
|
||||
Reference in New Issue
Block a user