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'
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
import { css } from '@styled/css'
|
import { css } from '@styled/css'
|
||||||
import { useWorksheetPreview } from './WorksheetPreviewContext'
|
import { useWorksheetPreview } from './WorksheetPreviewContext'
|
||||||
|
|
||||||
export function DuplicateWarningBanner() {
|
export function DuplicateWarningBanner() {
|
||||||
const { warnings, isDismissed, setIsDismissed } = useWorksheetPreview()
|
const { warnings, isDismissed, setIsDismissed } = useWorksheetPreview()
|
||||||
|
const [showDetails, setShowDetails] = useState(false)
|
||||||
|
|
||||||
if (warnings.length === 0 || isDismissed) {
|
if (warnings.length === 0 || isDismissed) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse warnings to extract actionable items
|
||||||
|
const firstWarning = warnings[0]
|
||||||
|
const hasMultipleWarnings = warnings.length > 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-element="problem-space-warning"
|
data-element="problem-space-warning"
|
||||||
className={css({
|
className={css({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '24', // Well below the page indicator to avoid overlap
|
top: '50%',
|
||||||
left: '4',
|
left: '50%',
|
||||||
right: '20', // More space on right to avoid action button
|
transform: 'translate(-50%, -50%)',
|
||||||
|
maxW: 'calc(100% - 160px)', // Leave space for action button
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
bg: 'yellow.50',
|
bg: 'rgba(254, 243, 199, 0.95)', // amber.100 with transparency
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
border: '1px solid',
|
border: '1px solid',
|
||||||
borderColor: 'yellow.300',
|
borderColor: 'amber.300',
|
||||||
rounded: '15px',
|
rounded: '15px',
|
||||||
p: '4',
|
p: '4',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
gap: '3',
|
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({ flex: '1', display: 'flex', flexDirection: 'column', gap: '2' })}>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
fontWeight: 'bold',
|
||||||
alignItems: 'center',
|
|
||||||
gap: '2',
|
|
||||||
fontWeight: 'semibold',
|
|
||||||
fontSize: 'md',
|
fontSize: 'md',
|
||||||
color: 'yellow.800',
|
color: 'amber.900',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span>⚠️</span>
|
Not Enough Unique Problems
|
||||||
<span>Duplicate Problem Risk</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
fontSize: 'sm',
|
fontSize: 'sm',
|
||||||
color: 'yellow.900',
|
color: 'amber.800',
|
||||||
whiteSpace: 'pre-wrap',
|
lineHeight: '1.5',
|
||||||
lineHeight: '1.6',
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{warnings.join('\n\n')}
|
{firstWarning}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
{/* Prominent Dismiss Button */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsDismissed(true)}
|
onClick={() => setIsDismissed(true)}
|
||||||
className={css({
|
className={css({
|
||||||
flexShrink: '0',
|
flexShrink: '0',
|
||||||
color: 'yellow.700',
|
px: '3',
|
||||||
fontSize: 'xl',
|
py: '1.5',
|
||||||
lineHeight: '1',
|
bg: 'amber.600',
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 'sm',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
rounded: 'full',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
bg: 'transparent',
|
|
||||||
border: 'none',
|
border: 'none',
|
||||||
p: '1',
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '1',
|
||||||
|
transition: 'all 0.2s',
|
||||||
_hover: {
|
_hover: {
|
||||||
color: 'yellow.900',
|
bg: 'amber.700',
|
||||||
bg: 'yellow.100',
|
transform: 'scale(1.05)',
|
||||||
rounded: 'sm',
|
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
aria-label="Dismiss warning"
|
aria-label="Dismiss warning"
|
||||||
>
|
>
|
||||||
×
|
<span>✕</span>
|
||||||
|
<span>Dismiss</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -384,6 +384,7 @@ export function generateProblems(
|
||||||
// Sample problems based on difficulty curve
|
// Sample problems based on difficulty curve
|
||||||
const result: AdditionProblem[] = []
|
const result: AdditionProblem[] = []
|
||||||
const seen = new Set<string>()
|
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++) {
|
for (let i = 0; i < total; i++) {
|
||||||
const frac = total <= 1 ? 0 : i / (total - 1)
|
const frac = total <= 1 ? 0 : i / (total - 1)
|
||||||
|
|
@ -415,8 +416,15 @@ export function generateProblems(
|
||||||
}
|
}
|
||||||
if (found) break
|
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) {
|
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)
|
seen.add(key)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -428,26 +436,27 @@ export function generateProblems(
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime
|
const elapsed = Date.now() - startTime
|
||||||
console.log(
|
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
|
return result
|
||||||
} else {
|
} else {
|
||||||
// No interpolation - just shuffle and take first N
|
// No interpolation - just shuffle and take first N
|
||||||
const shuffled = shuffleArray(allProblems, rand)
|
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) {
|
if (total > shuffled.length) {
|
||||||
|
const cyclesNeeded = Math.ceil(total / shuffled.length)
|
||||||
console.warn(
|
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[] = []
|
const result: AdditionProblem[] = []
|
||||||
while (result.length < total) {
|
for (let i = 0; i < total; i++) {
|
||||||
result.push(...shuffled.slice(0, Math.min(shuffled.length, total - result.length)))
|
result.push(shuffled[i % shuffled.length])
|
||||||
}
|
}
|
||||||
const elapsed = Date.now() - startTime
|
const elapsed = Date.now() - startTime
|
||||||
console.log(
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue