From 065b82fc3305600b3fb8caaac486710934468419 Mon Sep 17 00:00:00 2001 From: Ashton Anderson Date: Thu, 26 Mar 2026 09:58:16 -0400 Subject: [PATCH] Remove move map panels and harden Stockfish NNUE loading --- src/components/Analysis/AnalysisSidebar.tsx | 34 +-- .../Home/Sections/AnalysisSection.tsx | 45 +--- .../Openings/OpeningDrillAnalysis.tsx | 34 --- src/lib/engine/stockfish.ts | 117 +++++++--- src/pages/analysis/[...id].tsx | 202 ++++++++---------- src/pages/puzzles.tsx | 56 ----- 6 files changed, 203 insertions(+), 285 deletions(-) diff --git a/src/components/Analysis/AnalysisSidebar.tsx b/src/components/Analysis/AnalysisSidebar.tsx index ef54d114..c67a4dc3 100644 --- a/src/components/Analysis/AnalysisSidebar.tsx +++ b/src/components/Analysis/AnalysisSidebar.tsx @@ -1,5 +1,4 @@ import { - MoveMap, Highlight, BlunderMeter, MovesByRating, @@ -57,7 +56,6 @@ export const AnalysisSidebar: React.FC = ({ hover, makeMove, controller, - setHoverArrow, analysisEnabled, handleToggleAnalysis, hideDetailedBlunderMeter = false, @@ -140,14 +138,6 @@ export const AnalysisSidebar: React.FC = ({ ...simplifiedBlunderMeterProps, } - const moveMapProps = { - moveMap: analysisEnabled ? controller.moveMap : undefined, - colorSanMapping: analysisEnabled ? controller.colorSanMapping : {}, - setHoverArrow, - makeMove: analysisEnabled ? makeMove : mockMakeMove, - playerToMove: analysisEnabled ? (controller.currentNode?.turn ?? 'w') : 'w', - } - const movesByRatingProps = { moves: analysisEnabled ? controller.movesByRating : undefined, colorSanMapping: analysisEnabled ? controller.colorSanMapping : {}, @@ -275,16 +265,14 @@ export const AnalysisSidebar: React.FC = ({
{renderHeader('mobile', 'absolute left-0 top-0 z-10 w-full')} -
- +
+
- {!hideDetailedBlunderMeter && ( -
-
- -
-
- )} {!analysisEnabled && renderDisabledOverlay('Enable analysis to see move evaluations', { offsetTop: true, @@ -292,14 +280,6 @@ export const AnalysisSidebar: React.FC = ({
-
- -
- {!analysisEnabled && - renderDisabledOverlay('Enable analysis to see position evaluation')} -
- -
{!analysisEnabled && diff --git a/src/components/Home/Sections/AnalysisSection.tsx b/src/components/Home/Sections/AnalysisSection.tsx index 4023f6da..214881b2 100644 --- a/src/components/Home/Sections/AnalysisSection.tsx +++ b/src/components/Home/Sections/AnalysisSection.tsx @@ -7,7 +7,6 @@ import { useState, useEffect, useRef } from 'react' import { DemoBlunderMeter } from './DemoBlunderMeter' import { useInView } from 'react-intersection-observer' import { analysisMockData } from './analysisMockData.js' -import { MoveMap } from 'src/components/Analysis/MoveMap' import { Highlight } from 'src/components/Analysis/Highlight' import { MovesByRating } from 'src/components/Analysis/MovesByRating' import { MaiaEvaluation, StockfishEvaluation, GameNode } from 'src/types' @@ -120,8 +119,6 @@ export const AnalysisSection = ({ id }: AnalysisSectionProps) => { const [renderKey, setRenderKey] = useState(0) const [currentMaiaModel, setCurrentMaiaModel] = useState('maia_kdd_1500') - const [hoverArrow, setHoverArrow] = useState(null) - useEffect(() => { const handleResize = () => { setRenderKey((prev) => prev + 1) @@ -274,40 +271,20 @@ export const AnalysisSection = ({ id }: AnalysisSectionProps) => {
-
- -
- -
-
- - +
+ - -
+
+
diff --git a/src/components/Openings/OpeningDrillAnalysis.tsx b/src/components/Openings/OpeningDrillAnalysis.tsx index 2ba88e21..7a2f78b6 100644 --- a/src/components/Openings/OpeningDrillAnalysis.tsx +++ b/src/components/Openings/OpeningDrillAnalysis.tsx @@ -1,7 +1,6 @@ import React, { useMemo, useCallback, useContext } from 'react' import { Highlight, - MoveMap, BlunderMeter, MovesByRating, AnalysisSidebar, @@ -70,10 +69,6 @@ export const OpeningDrillAnalysis: React.FC = ({ // Intentionally empty - no moves allowed when analysis disabled }, []) - const mockSetHoverArrow = useCallback(() => { - // Intentionally empty - no hover arrows when analysis disabled - }, []) - // Create empty data structures that match expected types const emptyBlunderMeterData = useMemo( () => ({ @@ -232,35 +227,6 @@ export const OpeningDrillAnalysis: React.FC = ({ )} -
- - {!analysisEnabled && ( -
-
- - lock - -

- Analysis Disabled -

-
-
- )} -
) diff --git a/src/lib/engine/stockfish.ts b/src/lib/engine/stockfish.ts index 536c49de..c0074c74 100644 --- a/src/lib/engine/stockfish.ts +++ b/src/lib/engine/stockfish.ts @@ -1789,10 +1789,13 @@ const loadNnueModel = async ( storage: StockfishModelStorage, timeoutMs: number, onNetworkFetchStart?: () => void, + forceRefresh = false, ): Promise => { - const cachedModel = await storage.getModel(modelUrl) - if (cachedModel) { - return cachedModel + if (!forceRefresh) { + const cachedModel = await storage.getModel(modelUrl) + if (cachedModel) { + return cachedModel + } } onNetworkFetchStart?.() @@ -1808,16 +1811,27 @@ const loadNnueModel = async ( return buffer } +const shouldRetryNnueLoad = (error: unknown): boolean => { + if (error instanceof RangeError) { + return true + } + + if (!(error instanceof Error)) { + return false + } + + return ( + /offset is out of bounds/i.test(error.message) || + /could not allocate/i.test(error.message) || + /bytes\?/i.test(error.message) + ) +} + const setupStockfish = async ( onPhaseChange?: (phase: StockfishInitPhase) => void, ): Promise => { - onPhaseChange?.('loading-module') // eslint-disable-next-line @typescript-eslint/no-explicit-any const makeModule: any = await import('lila-stockfish-web/sf17-79.js') - const instance: StockfishWeb = await makeModule.default({ - wasmMemory: sharedWasmMemory(2560), - locateFile: (name: string) => `/stockfish/${name}`, - }) // NNUE weights served via raw.githubusercontent.com permalink (CORS + COEP compatible). // Override with NEXT_PUBLIC_STOCKFISH_NNUE_BASE_URL for self-hosted deployments. @@ -1826,31 +1840,84 @@ const setupStockfish = async ( 'https://raw.githubusercontent.com/CSSLab/maia-platform-frontend/e23a50e/public/stockfish' const storage = new StockfishModelStorage() await storage.requestPersistentStorage() - - const nnue0Url = `${nnueBaseUrl}/${instance.getRecommendedNnue(0)}` - const nnue1Url = `${nnueBaseUrl}/${instance.getRecommendedNnue(1)}` const timeoutMs = getNnueFetchTimeoutMs() - let downloadStarted = false + let nnueUrls: [string, string] | null = null + + const createInstance = async (): Promise => { + onPhaseChange?.('loading-module') + return makeModule.default({ + wasmMemory: sharedWasmMemory(2560), + locateFile: (name: string) => `/stockfish/${name}`, + }) + } + + const loadWeightsIntoInstance = async ( + instance: StockfishWeb, + forceRefresh = false, + ): Promise => { + if (!nnueUrls) { + throw new Error('Stockfish recommended NNUE URLs were not initialized') + } + + let downloadStarted = false - try { onPhaseChange?.('checking-cache') const buffers = await Promise.all([ - loadNnueModel(nnue0Url, storage, timeoutMs, () => { - if (!downloadStarted) { - downloadStarted = true - onPhaseChange?.('downloading-nnue') - } - }), - loadNnueModel(nnue1Url, storage, timeoutMs, () => { - if (!downloadStarted) { - downloadStarted = true - onPhaseChange?.('downloading-nnue') - } - }), + loadNnueModel( + nnueUrls[0], + storage, + timeoutMs, + () => { + if (!downloadStarted) { + downloadStarted = true + onPhaseChange?.('downloading-nnue') + } + }, + forceRefresh, + ), + loadNnueModel( + nnueUrls[1], + storage, + timeoutMs, + () => { + if (!downloadStarted) { + downloadStarted = true + onPhaseChange?.('downloading-nnue') + } + }, + forceRefresh, + ), ]) + onPhaseChange?.('loading-nnue') instance.setNnueBuffer(new Uint8Array(buffers[0]), 0) instance.setNnueBuffer(new Uint8Array(buffers[1]), 1) + } + + try { + let instance = await createInstance() + nnueUrls = [ + `${nnueBaseUrl}/${instance.getRecommendedNnue(0)}`, + `${nnueBaseUrl}/${instance.getRecommendedNnue(1)}`, + ] + + try { + await loadWeightsIntoInstance(instance) + } catch (error) { + if (!shouldRetryNnueLoad(error)) { + throw error + } + + console.warn( + 'Stockfish NNUE load failed; clearing cached weights and retrying once.', + error, + ) + await Promise.all(nnueUrls.map((url) => storage.deleteModel(url))) + + instance = await createInstance() + await loadWeightsIntoInstance(instance, true) + } + return instance } catch (error) { console.error('Failed to load NNUE models:', error) diff --git a/src/pages/analysis/[...id].tsx b/src/pages/analysis/[...id].tsx index cf73f332..d195854a 100644 --- a/src/pages/analysis/[...id].tsx +++ b/src/pages/analysis/[...id].tsx @@ -26,9 +26,10 @@ import { import { WindowSizeContext, TreeControllerContext, useTour } from 'src/contexts' import { Loading } from 'src/components' import { AuthenticatedWrapper } from 'src/components/Common/AuthenticatedWrapper' -import { MoveMap } from 'src/components/Analysis/MoveMap' -import { Highlight } from 'src/components/Analysis/Highlight' -import { AnalysisSidebar } from 'src/components/Analysis' +import { + AnalysisSidebar, + SimplifiedAnalysisOverview, +} from 'src/components/Analysis' import { AnalysisArrowLegend, AnalysisCompactBlunderMeter, @@ -726,7 +727,6 @@ const Analysis: React.FC = ({ } const mockHover = useCallback(() => void 0, []) - const mockSetHoverArrow = useCallback(() => void 0, []) const makeMove = (move: string) => { if (!controller.currentNode || !analyzedGame.tree) return @@ -2057,64 +2057,98 @@ const Analysis: React.FC = ({
-
)}
- -
- - {(!analysisEnabled || - controller.learnFromMistakes.state.isActive) && ( -
-
- - lock - -

- {controller.learnFromMistakes.state.isActive - ? 'Learning Mode Active' - : 'Analysis Disabled'} -

-
-
- )} -
= ({ const mockMakeMove = useCallback(() => { // Intentionally empty - no moves allowed in puzzle mode }, []) - const mockSetHoverArrow = useCallback(() => { - // Intentionally empty - no hover arrows in puzzle mode - }, []) - const containerVariants = { hidden: { opacity: 0 }, visible: { @@ -1186,57 +1181,6 @@ const Train: React.FC = ({ )} -
- - {!analysisEnabled && showAnalysis && ( -
-
- - lock - -

- Analysis Disabled -

-
-
- )} - {!showAnalysis && ( -
-
- - lock - -

- Analysis Locked -

-
-
- )} -
{gamesController}