fix(flowchart): sequence example generation to avoid web worker conflicts

When both WorksheetDebugPanel and FlowchartExampleGrid try to generate
examples simultaneously using the shared web worker pool, the workers'
resolve/reject callbacks get overwritten, causing one request to never
complete.

This fix sequences the generation:
- WorksheetDebugPanel generates first (when worksheet tab is active)
- FlowchartExampleGrid waits until WorksheetDebugPanel signals completion
- Added onGenerationStart/onGenerationComplete callbacks to WorksheetDebugPanel
- Added waitForReady prop to FlowchartExampleGrid to defer generation
- Workshop page coordinates the sequence using isDebugPanelGenerating state

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2026-01-20 20:14:07 -06:00
parent 83c2f261c0
commit de720ab39b
4 changed files with 90 additions and 14 deletions

View File

@@ -86,6 +86,19 @@ export default function WorkshopPage() {
// Examples for worksheet generation
const [worksheetExamples, setWorksheetExamples] = useState<GeneratedExample[]>([])
// Track if WorksheetDebugPanel is generating (to sequence with FlowchartExampleGrid)
// Initialize to true since we start on worksheet tab - WorksheetDebugPanel will set to false when done
const [isDebugPanelGenerating, setIsDebugPanelGenerating] = useState(true)
const handleDebugPanelGenerationStart = useCallback(() => setIsDebugPanelGenerating(true), [])
const handleDebugPanelGenerationComplete = useCallback(() => setIsDebugPanelGenerating(false), [])
// Reset generation state when flowchart changes (to re-sequence generation)
useEffect(() => {
if (executableFlowchart && activeTab === 'worksheet') {
setIsDebugPanelGenerating(true)
}
}, [executableFlowchart?.definition.id, activeTab])
// Generate examples when flowchart changes
useEffect(() => {
if (!executableFlowchart) {
@@ -1203,7 +1216,12 @@ export default function WorkshopPage() {
Create PDF Worksheet
</button>
{/* Debug Panel - shows generated examples with answers */}
<WorksheetDebugPanel flowchart={executableFlowchart} problemCount={10} />
<WorksheetDebugPanel
flowchart={executableFlowchart}
problemCount={10}
onGenerationStart={handleDebugPanelGenerationStart}
onGenerationComplete={handleDebugPanelGenerationComplete}
/>
</div>
)}
{activeTab === 'worksheet' && !executableFlowchart && (
@@ -1233,6 +1251,7 @@ export default function WorkshopPage() {
// Navigate to test page - it will use the passed values
router.push(`/flowchart/workshop/${sessionId}/test`)
}}
waitForReady={activeTab === 'worksheet' && isDebugPanelGenerating}
/>
</div>
</div>
@@ -1659,10 +1678,13 @@ function ExamplesTab({
definition,
flowchart,
onTestExample,
waitForReady = false,
}: {
definition: FlowchartDefinition | null
flowchart: ExecutableFlowchart | null
onTestExample: (values: Record<string, ProblemValue>) => void
/** When true, wait before generating examples (to sequence with other generators) */
waitForReady?: boolean
}) {
if (!definition) {
return (
@@ -1687,6 +1709,7 @@ function ExamplesTab({
onSelect={onTestExample}
compact={true}
enableCaching={false} // Don't cache in workshop - always show fresh examples
waitForReady={waitForReady}
/>
<p
className={css({

View File

@@ -40,6 +40,12 @@ export interface FlowchartExampleGridProps {
showDifficultyFilter?: boolean
/** Compact mode for smaller displays (default: false) */
compact?: boolean
/**
* When true, wait before generating examples. Use this to sequence generation
* with other components that share the web worker pool.
* When undefined/false, generation starts immediately.
*/
waitForReady?: boolean
}
/**
@@ -62,17 +68,18 @@ export function FlowchartExampleGrid({
showDice = true,
showDifficultyFilter = true,
compact = false,
waitForReady = false,
}: FlowchartExampleGridProps) {
// Displayed examples
const [displayedExamples, setDisplayedExamples] = useState<GeneratedExample[]>([])
// Pending examples promise during drag
const pendingExamplesRef = useRef<Promise<GeneratedExample[]> | null>(null)
// Track if we've loaded from cache
const loadedFromCacheRef = useRef(false)
// Selected difficulty tier
const [selectedTier, setSelectedTier] = useState<DifficultyTier>('all')
// Loading state
const [isLoading, setIsLoading] = useState(true)
// Error state
const [error, setError] = useState<string | null>(null)
// Create a stable storage key for caching examples
const storageKey = useMemo(() => {
@@ -102,10 +109,22 @@ export function FlowchartExampleGrid({
}
}, [flowchart, analysis])
// Load/generate examples on mount
// Load/generate examples on mount or when flowchart changes
// If waitForReady is true, wait until it becomes false before generating
useEffect(() => {
if (loadedFromCacheRef.current) return
loadedFromCacheRef.current = true
// If we need to wait, don't start generation yet
if (waitForReady) {
setIsLoading(true)
setError(null)
return
}
// Create a flag to track if this effect is still active (for cleanup)
let isActive = true
// Reset loading state when flowchart changes
setIsLoading(true)
setError(null)
// Try to load from cache first
if (storageKey) {
@@ -127,7 +146,10 @@ export function FlowchartExampleGrid({
// Generate new examples
generateExamplesAsync(flowchart, exampleCount, constraints)
.then((examples) => {
setDisplayedExamples(examples)
if (isActive) {
setDisplayedExamples(examples)
setError(null)
}
if (storageKey) {
try {
sessionStorage.setItem(storageKey, JSON.stringify(examples))
@@ -138,11 +160,21 @@ export function FlowchartExampleGrid({
})
.catch((e) => {
console.error('Error generating examples:', e)
if (isActive) {
setError(e instanceof Error ? e.message : 'Failed to generate examples')
}
})
.finally(() => {
setIsLoading(false)
if (isActive) {
setIsLoading(false)
}
})
}, [flowchart, exampleCount, constraints, storageKey])
// Cleanup: mark this effect as inactive if component unmounts or deps change
return () => {
isActive = false
}
}, [flowchart, exampleCount, constraints, storageKey, waitForReady])
// Calculate difficulty range for visual indicators
const difficultyRange = useMemo(() => {
@@ -362,7 +394,7 @@ export function FlowchartExampleGrid({
color: { base: 'gray.500', _dark: 'gray.400' },
})}
>
Generating examples...
{waitForReady ? 'Waiting...' : 'Generating examples...'}
</div>
)
}
@@ -762,7 +794,15 @@ export function FlowchartExampleGrid({
fontSize: 'sm',
})}
>
No examples could be generated.
{error ? (
<>
<span className={css({ color: { base: 'red.600', _dark: 'red.400' } })}>
Error: {error}
</span>
</>
) : (
'No examples could be generated.'
)}
</div>
)}
</div>

View File

@@ -13,6 +13,10 @@ interface WorksheetDebugPanelProps {
flowchart: ExecutableFlowchart
/** Number of problems to generate (default: 10) */
problemCount?: number
/** Called when example generation starts */
onGenerationStart?: () => void
/** Called when example generation completes (success or error) */
onGenerationComplete?: () => void
}
/** Difficulty tier type */
@@ -22,7 +26,12 @@ type DifficultyTier = 'easy' | 'medium' | 'hard'
* Debug panel for testing worksheet generation.
* Shows generated problems with their computed answers, raw values, and difficulty tiers.
*/
export function WorksheetDebugPanel({ flowchart, problemCount = 10 }: WorksheetDebugPanelProps) {
export function WorksheetDebugPanel({
flowchart,
problemCount = 10,
onGenerationStart,
onGenerationComplete,
}: WorksheetDebugPanelProps) {
const [examples, setExamples] = useState<GeneratedExample[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@@ -37,6 +46,7 @@ export function WorksheetDebugPanel({ flowchart, problemCount = 10 }: WorksheetD
const generateExamples = useCallback(async () => {
setIsLoading(true)
setError(null)
onGenerationStart?.()
try {
const newExamples = await generateExamplesAsync(flowchart, problemCount, {
positiveAnswersOnly: false,
@@ -47,8 +57,9 @@ export function WorksheetDebugPanel({ flowchart, problemCount = 10 }: WorksheetD
setError(err instanceof Error ? err.message : 'Failed to generate examples')
} finally {
setIsLoading(false)
onGenerationComplete?.()
}
}, [flowchart, problemCount])
}, [flowchart, problemCount, onGenerationStart, onGenerationComplete])
// Calculate difficulty range
const difficultyRange = useMemo(() => {

View File

@@ -52,7 +52,9 @@ function getWorkerPool(): WorkerState[] {
}
worker.onerror = (error) => {
state.reject?.(new Error(error.message))
console.error('Web worker error:', error)
const message = error.message || 'Web worker failed to load or execute'
state.reject?.(new Error(message))
state.busy = false
state.resolve = null
state.reject = null