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:
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user