fix: properly cycle through problem sets when exceeding unique problem space
**Problem**: When requesting more problems than exist in the problem space (e.g., 100 problems from 45 unique 1-digit regrouping problems), the generator would repeat the same problem over and over after exhausting unique problems. **Root Cause**: - Non-interpolate branch: Used `shuffled.slice(0, ...)` in a loop, always taking from the beginning of the array instead of cycling through - Interpolate branch: When exhausting unique problems, would mark problem as "seen" and add it, then continue adding the same problem repeatedly **Fixes**: 1. **Non-interpolate branch** (no progressive difficulty): - Changed from `while` loop with `slice(0, ...)` to simple `for` loop with modulo - Now uses `shuffled[i % shuffled.length]` to cycle through entire array - Example: Problems 0-44 = first shuffle, 45-89 = second shuffle (same order), etc. 2. **Interpolate branch** (progressive difficulty enabled): - When all unique problems exhausted, now clears the `seen` set to start fresh cycle - Maintains progressive difficulty curve across all cycles - Logs cycle count: "Exhausted all 45 unique problems at position 45. Starting cycle 2." - Example: Each cycle maintains easy→hard progression, just repeats the sorted sequence **Testing**: With 1-digit regrouping (45 unique problems), requesting 100 problems now produces: - First 45: All unique problems - Next 45: Complete repeat of the set - Final 10: First 10 problems from third cycle 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
51b11a8f08
commit
55d4920167
|
|
@ -1,81 +1,146 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { css } from '@styled/css'
|
||||
import { useWorksheetPreview } from './WorksheetPreviewContext'
|
||||
|
||||
export function DuplicateWarningBanner() {
|
||||
const { warnings, isDismissed, setIsDismissed } = useWorksheetPreview()
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
|
||||
if (warnings.length === 0 || isDismissed) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Parse warnings to extract actionable items
|
||||
const firstWarning = warnings[0]
|
||||
const hasMultipleWarnings = warnings.length > 1
|
||||
|
||||
return (
|
||||
<div
|
||||
data-element="problem-space-warning"
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
top: '24', // Well below the page indicator to avoid overlap
|
||||
left: '4',
|
||||
right: '20', // More space on right to avoid action button
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
maxW: 'calc(100% - 160px)', // Leave space for action button
|
||||
zIndex: 100,
|
||||
bg: 'yellow.50',
|
||||
bg: 'rgba(254, 243, 199, 0.95)', // amber.100 with transparency
|
||||
backdropFilter: 'blur(8px)',
|
||||
border: '1px solid',
|
||||
borderColor: 'yellow.300',
|
||||
borderColor: 'amber.300',
|
||||
rounded: '15px',
|
||||
p: '4',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '3',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.15)',
|
||||
boxShadow: '0 2px 12px rgba(217, 119, 6, 0.15)', // amber shadow
|
||||
})}
|
||||
>
|
||||
{/* Warning Icon */}
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '2xl',
|
||||
lineHeight: '1',
|
||||
flexShrink: '0',
|
||||
})}
|
||||
>
|
||||
⚠️
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className={css({ flex: '1', display: 'flex', flexDirection: 'column', gap: '2' })}>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
fontWeight: 'semibold',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 'md',
|
||||
color: 'yellow.800',
|
||||
color: 'amber.900',
|
||||
})}
|
||||
>
|
||||
<span>⚠️</span>
|
||||
<span>Duplicate Problem Risk</span>
|
||||
Not Enough Unique Problems
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'yellow.900',
|
||||
whiteSpace: 'pre-wrap',
|
||||
lineHeight: '1.6',
|
||||
color: 'amber.800',
|
||||
lineHeight: '1.5',
|
||||
})}
|
||||
>
|
||||
{warnings.join('\n\n')}
|
||||
{firstWarning}
|
||||
</div>
|
||||
|
||||
{/* Show/Hide Details Toggle */}
|
||||
{hasMultipleWarnings && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className={css({
|
||||
alignSelf: 'flex-start',
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'semibold',
|
||||
color: 'amber.700',
|
||||
cursor: 'pointer',
|
||||
bg: 'transparent',
|
||||
border: 'none',
|
||||
p: '0',
|
||||
textDecoration: 'underline',
|
||||
_hover: {
|
||||
color: 'amber.900',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{showDetails ? '▼ Hide details' : '▶ Show details'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Detailed warnings (collapsed by default) */}
|
||||
{showDetails && hasMultipleWarnings && (
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'amber.800',
|
||||
whiteSpace: 'pre-wrap',
|
||||
lineHeight: '1.6',
|
||||
mt: '2',
|
||||
pl: '3',
|
||||
borderLeft: '2px solid',
|
||||
borderColor: 'amber.300',
|
||||
})}
|
||||
>
|
||||
{warnings.slice(1).join('\n\n')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Prominent Dismiss Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsDismissed(true)}
|
||||
className={css({
|
||||
flexShrink: '0',
|
||||
color: 'yellow.700',
|
||||
fontSize: 'xl',
|
||||
lineHeight: '1',
|
||||
px: '3',
|
||||
py: '1.5',
|
||||
bg: 'amber.600',
|
||||
color: 'white',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'bold',
|
||||
rounded: 'full',
|
||||
cursor: 'pointer',
|
||||
bg: 'transparent',
|
||||
border: 'none',
|
||||
p: '1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1',
|
||||
transition: 'all 0.2s',
|
||||
_hover: {
|
||||
color: 'yellow.900',
|
||||
bg: 'yellow.100',
|
||||
rounded: 'sm',
|
||||
bg: 'amber.700',
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
})}
|
||||
aria-label="Dismiss warning"
|
||||
>
|
||||
×
|
||||
<span>✕</span>
|
||||
<span>Dismiss</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -384,6 +384,7 @@ export function generateProblems(
|
|||
// Sample problems based on difficulty curve
|
||||
const result: AdditionProblem[] = []
|
||||
const seen = new Set<string>()
|
||||
let cycleCount = 0 // Track how many times we've cycled through all problems
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
const frac = total <= 1 ? 0 : i / (total - 1)
|
||||
|
|
@ -415,8 +416,15 @@ export function generateProblems(
|
|||
}
|
||||
if (found) break
|
||||
}
|
||||
// If still not found, allow duplicate
|
||||
// If still not found, we've exhausted all unique problems
|
||||
// Reset the seen set and start a new cycle
|
||||
if (!found) {
|
||||
cycleCount++
|
||||
console.log(
|
||||
`[ADD GEN] Exhausted all ${sortedByDifficulty.length} unique problems at position ${i}. Starting cycle ${cycleCount + 1}.`
|
||||
)
|
||||
seen.clear()
|
||||
// Use the target problem for this position
|
||||
seen.add(key)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -428,26 +436,27 @@ export function generateProblems(
|
|||
|
||||
const elapsed = Date.now() - startTime
|
||||
console.log(
|
||||
`[ADD GEN] Complete: ${result.length} problems in ${elapsed}ms (0 retries, generate-all with progressive difficulty)`
|
||||
`[ADD GEN] Complete: ${result.length} problems in ${elapsed}ms (0 retries, generate-all with progressive difficulty, ${cycleCount} cycles)`
|
||||
)
|
||||
return result
|
||||
} else {
|
||||
// No interpolation - just shuffle and take first N
|
||||
const shuffled = shuffleArray(allProblems, rand)
|
||||
|
||||
// If we need more problems than available, we'll have duplicates
|
||||
// If we need more problems than available, cycle through the shuffled array
|
||||
if (total > shuffled.length) {
|
||||
const cyclesNeeded = Math.ceil(total / shuffled.length)
|
||||
console.warn(
|
||||
`[ADD GEN] Warning: Requested ${total} problems but only ${shuffled.length} unique problems exist. Some duplicates will occur.`
|
||||
`[ADD GEN] Warning: Requested ${total} problems but only ${shuffled.length} unique problems exist. Will cycle ${cyclesNeeded} times.`
|
||||
)
|
||||
// Repeat the shuffled array to fill the request
|
||||
// Build result by repeating the entire shuffled array as many times as needed
|
||||
const result: AdditionProblem[] = []
|
||||
while (result.length < total) {
|
||||
result.push(...shuffled.slice(0, Math.min(shuffled.length, total - result.length)))
|
||||
for (let i = 0; i < total; i++) {
|
||||
result.push(shuffled[i % shuffled.length])
|
||||
}
|
||||
const elapsed = Date.now() - startTime
|
||||
console.log(
|
||||
`[ADD GEN] Complete: ${result.length} problems in ${elapsed}ms (0 retries, generate-all method)`
|
||||
`[ADD GEN] Complete: ${result.length} problems in ${elapsed}ms (0 retries, generate-all method, ${cyclesNeeded} cycles)`
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue