diff --git a/apps/web/src/components/TypstSoroban.tsx b/apps/web/src/components/TypstSoroban.tsx index a0e89016..8c299771 100644 --- a/apps/web/src/components/TypstSoroban.tsx +++ b/apps/web/src/components/TypstSoroban.tsx @@ -1,7 +1,7 @@ 'use client' -import { useState, useEffect, useCallback } from 'react' -import { generateSorobanSVG, type SorobanConfig } from '@/lib/typst-soroban' +import { useState, useEffect, useCallback, useRef } from 'react' +import { generateSorobanSVG, getWasmStatus, triggerWasmPreload, type SorobanConfig } from '@/lib/typst-soroban' import { css } from '../../styled-system/css' import { useAbacusConfig } from '@/contexts/AbacusDisplayContext' @@ -13,6 +13,7 @@ interface TypstSorobanProps { onError?: (error: string) => void onSuccess?: () => void enableServerFallback?: boolean + lazy?: boolean // New prop for lazy loading } export function TypstSoroban({ @@ -22,18 +23,36 @@ export function TypstSoroban({ className, onError, onSuccess, - enableServerFallback = false + enableServerFallback = false, + lazy = false }: TypstSorobanProps) { const [svg, setSvg] = useState(null) - const [isLoading, setIsLoading] = useState(true) + const [isLoading, setIsLoading] = useState(!lazy) // Don't start loading if lazy const [error, setError] = useState(null) + const [shouldLoad, setShouldLoad] = useState(!lazy) // Control when loading starts const globalConfig = useAbacusConfig() + const abortControllerRef = useRef(null) + useEffect(() => { + if (!shouldLoad) return + async function generateSVG() { + // Cancel any previous generation + if (abortControllerRef.current) { + abortControllerRef.current.abort() + } + + abortControllerRef.current = new AbortController() + const signal = abortControllerRef.current.signal + setIsLoading(true) setError(null) - setSvg(null) + + // Longer delay to prevent flashing for fast renders + await new Promise(resolve => setTimeout(resolve, 300)) + + if (signal.aborted) return try { const config: SorobanConfig = { @@ -50,22 +69,83 @@ export function TypstSoroban({ } const generatedSvg = await generateSorobanSVG(config) + + if (signal.aborted) return + setSvg(generatedSvg) // Call success callback after state is set setTimeout(() => onSuccess?.(), 0) } catch (err) { + if (signal.aborted) return + const errorMessage = err instanceof Error ? err.message : 'Unknown error' setError(errorMessage) console.error('TypstSoroban generation error:', err) // Call error callback after state is set setTimeout(() => onError?.(errorMessage), 0) } finally { - setIsLoading(false) + if (!signal.aborted) { + setIsLoading(false) + } } } generateSVG() - }, [number, width, height, globalConfig.beadShape, globalConfig.colorScheme, globalConfig.hideInactiveBeads, globalConfig.coloredNumerals, globalConfig.scaleFactor, enableServerFallback]) + + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort() + } + } + }, [shouldLoad, number, width, height, globalConfig.beadShape, globalConfig.colorScheme, globalConfig.hideInactiveBeads, globalConfig.coloredNumerals, globalConfig.scaleFactor, enableServerFallback]) + + // Handler to trigger loading on user interaction + const handleLoadTrigger = useCallback(() => { + if (!shouldLoad && !isLoading && !svg) { + setShouldLoad(true) + // Also trigger WASM preload if not already started + triggerWasmPreload() + } + }, [shouldLoad, isLoading, svg]) + + // Show lazy loading placeholder + if (lazy && !shouldLoad && !svg) { + const wasmStatus = getWasmStatus() + + return ( +
+
+
+ {wasmStatus.isLoaded ? '🚀' : '🧮'} +
+
+
+ ) + } if (isLoading) { return ( @@ -76,16 +156,16 @@ export function TypstSoroban({ display: 'flex', alignItems: 'center', justifyContent: 'center', - bg: 'gray.50', + bg: 'blue.25', rounded: 'md', minH: '200px' })}>
@@ -101,23 +181,13 @@ export function TypstSoroban({ w: 'full', h: 'full', display: 'flex', - flexDirection: 'column', alignItems: 'center', justifyContent: 'center', - bg: 'red.50', - border: '1px solid', - borderColor: 'red.200', + bg: 'red.25', rounded: 'md', - p: '4', minH: '200px' })}> -
⚠️
-

- Failed to generate soroban -

-

- {error} -

+
⚠️
)