Skip to content

Commit 5f5e34c

Browse files
Merge pull request #254 from CSSLab/codex/analysis-right-column-desktop
Remove move map panels and harden Stockfish NNUE loading
2 parents 48eae3b + 065b82f commit 5f5e34c

6 files changed

Lines changed: 203 additions & 285 deletions

File tree

src/components/Analysis/AnalysisSidebar.tsx

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
MoveMap,
32
Highlight,
43
BlunderMeter,
54
MovesByRating,
@@ -57,7 +56,6 @@ export const AnalysisSidebar: React.FC<Props> = ({
5756
hover,
5857
makeMove,
5958
controller,
60-
setHoverArrow,
6159
analysisEnabled,
6260
handleToggleAnalysis,
6361
hideDetailedBlunderMeter = false,
@@ -140,14 +138,6 @@ export const AnalysisSidebar: React.FC<Props> = ({
140138
...simplifiedBlunderMeterProps,
141139
}
142140

143-
const moveMapProps = {
144-
moveMap: analysisEnabled ? controller.moveMap : undefined,
145-
colorSanMapping: analysisEnabled ? controller.colorSanMapping : {},
146-
setHoverArrow,
147-
makeMove: analysisEnabled ? makeMove : mockMakeMove,
148-
playerToMove: analysisEnabled ? (controller.currentNode?.turn ?? 'w') : 'w',
149-
}
150-
151141
const movesByRatingProps = {
152142
moves: analysisEnabled ? controller.movesByRating : undefined,
153143
colorSanMapping: analysisEnabled ? controller.colorSanMapping : {},
@@ -275,31 +265,21 @@ export const AnalysisSidebar: React.FC<Props> = ({
275265
<div className="flex h-full flex-col gap-3 xl:hidden">
276266
<div className="desktop-analysis-small-row-1-container relative flex overflow-hidden rounded-md border border-glass-border bg-glass-strong pt-10 backdrop-blur-md">
277267
{renderHeader('mobile', 'absolute left-0 top-0 z-10 w-full')}
278-
<div className="flex h-full w-full border-r border-glass-border">
279-
<Highlight {...highlightProps} />
268+
<div className="flex h-full w-full">
269+
<SimplifiedAnalysisOverview
270+
highlightProps={{ ...highlightProps, simplified: true }}
271+
blunderMeterProps={simplifiedBlunderMeterProps}
272+
analysisEnabled={analysisEnabled}
273+
hideBlunderMeter={hideDetailedBlunderMeter}
274+
/>
280275
</div>
281-
{!hideDetailedBlunderMeter && (
282-
<div className="flex h-full w-auto min-w-[40%] max-w-[40%] p-3">
283-
<div className="h-full w-full">
284-
<BlunderMeter {...blunderMeterProps} showContainer={false} />
285-
</div>
286-
</div>
287-
)}
288276
{!analysisEnabled &&
289277
renderDisabledOverlay('Enable analysis to see move evaluations', {
290278
offsetTop: true,
291279
})}
292280
</div>
293281

294282
<div className="desktop-analysis-small-row-2-container relative flex w-full">
295-
<div className="h-full w-full">
296-
<MoveMap {...moveMapProps} />
297-
</div>
298-
{!analysisEnabled &&
299-
renderDisabledOverlay('Enable analysis to see position evaluation')}
300-
</div>
301-
302-
<div className="desktop-analysis-small-row-3-container relative flex w-full">
303283
<div className="relative flex h-full w-full flex-col overflow-hidden rounded-md border border-glass-border bg-glass backdrop-blur-md">
304284
<MovesByRating {...movesByRatingProps} />
305285
{!analysisEnabled &&

src/components/Home/Sections/AnalysisSection.tsx

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { useState, useEffect, useRef } from 'react'
77
import { DemoBlunderMeter } from './DemoBlunderMeter'
88
import { useInView } from 'react-intersection-observer'
99
import { analysisMockData } from './analysisMockData.js'
10-
import { MoveMap } from 'src/components/Analysis/MoveMap'
1110
import { Highlight } from 'src/components/Analysis/Highlight'
1211
import { MovesByRating } from 'src/components/Analysis/MovesByRating'
1312
import { MaiaEvaluation, StockfishEvaluation, GameNode } from 'src/types'
@@ -120,8 +119,6 @@ export const AnalysisSection = ({ id }: AnalysisSectionProps) => {
120119
const [renderKey, setRenderKey] = useState(0)
121120

122121
const [currentMaiaModel, setCurrentMaiaModel] = useState('maia_kdd_1500')
123-
const [hoverArrow, setHoverArrow] = useState<DrawShape | null>(null)
124-
125122
useEffect(() => {
126123
const handleResize = () => {
127124
setRenderKey((prev) => prev + 1)
@@ -274,40 +271,20 @@ export const AnalysisSection = ({ id }: AnalysisSectionProps) => {
274271
</motion.div>
275272
</div>
276273
</div>
277-
<div className="flex flex-col gap-3 md:flex-row">
278-
<motion.div
279-
className="h-64 md:w-1/2"
280-
initial={{ opacity: 0, y: 20 }}
281-
animate={
282-
inView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }
283-
}
284-
transition={{ duration: 0.3, delay: 0.6 }}
285-
>
286-
<div className="from-white/8 to-white/4 h-full w-full overflow-hidden rounded border border-glass-border bg-gradient-to-br backdrop-blur-md">
287-
<MovesByRating
288-
moves={analysisMockData.movesByRating}
289-
colorSanMapping={analysisMockData.colorSanMapping}
290-
isHomePage={true}
291-
/>
292-
</div>
293-
</motion.div>
294-
<motion.div
295-
className="from-white/8 to-white/4 h-64 overflow-hidden rounded border border-glass-border bg-gradient-to-br backdrop-blur-md md:w-1/2"
296-
initial={{ opacity: 0, y: 20 }}
297-
animate={
298-
inView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }
299-
}
300-
transition={{ duration: 0.3, delay: 0.7 }}
301-
>
302-
<MoveMap
303-
moveMap={analysisMockData.moveMap}
274+
<motion.div
275+
className="h-64"
276+
initial={{ opacity: 0, y: 20 }}
277+
animate={inView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
278+
transition={{ duration: 0.3, delay: 0.6 }}
279+
>
280+
<div className="from-white/8 to-white/4 h-full w-full overflow-hidden rounded border border-glass-border bg-gradient-to-br backdrop-blur-md">
281+
<MovesByRating
282+
moves={analysisMockData.movesByRating}
304283
colorSanMapping={analysisMockData.colorSanMapping}
305-
setHoverArrow={setHoverArrow}
306-
makeMove={handleMakeMove}
307284
isHomePage={true}
308285
/>
309-
</motion.div>
310-
</div>
286+
</div>
287+
</motion.div>
311288
</div>
312289
</div>
313290
</motion.div>

src/components/Openings/OpeningDrillAnalysis.tsx

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, { useMemo, useCallback, useContext } from 'react'
22
import {
33
Highlight,
4-
MoveMap,
54
BlunderMeter,
65
MovesByRating,
76
AnalysisSidebar,
@@ -70,10 +69,6 @@ export const OpeningDrillAnalysis: React.FC<Props> = ({
7069
// Intentionally empty - no moves allowed when analysis disabled
7170
}, [])
7271

73-
const mockSetHoverArrow = useCallback(() => {
74-
// Intentionally empty - no hover arrows when analysis disabled
75-
}, [])
76-
7772
// Create empty data structures that match expected types
7873
const emptyBlunderMeterData = useMemo(
7974
() => ({
@@ -232,35 +227,6 @@ export const OpeningDrillAnalysis: React.FC<Props> = ({
232227
)}
233228
</div>
234229

235-
<div className="relative">
236-
<MoveMap
237-
moveMap={analysisEnabled ? analysisController.moveMap : undefined}
238-
colorSanMapping={
239-
analysisEnabled ? analysisController.colorSanMapping : {}
240-
}
241-
setHoverArrow={
242-
analysisEnabled ? parentSetHoverArrow : mockSetHoverArrow
243-
}
244-
makeMove={analysisEnabled ? makeMove : mockMakeMove}
245-
playerToMove={
246-
analysisEnabled
247-
? (analysisController.currentNode?.turn ?? 'w')
248-
: 'w'
249-
}
250-
/>
251-
{!analysisEnabled && (
252-
<div className="absolute inset-0 z-10 flex items-center justify-center bg-backdrop/90 backdrop-blur-sm">
253-
<div className="rounded bg-glass p-4 text-center shadow-lg">
254-
<span className="material-symbols-outlined mb-1 text-xl text-human-3">
255-
lock
256-
</span>
257-
<p className="text-xs font-medium text-primary">
258-
Analysis Disabled
259-
</p>
260-
</div>
261-
</div>
262-
)}
263-
</div>
264230
</div>
265231
</div>
266232
)

src/lib/engine/stockfish.ts

Lines changed: 92 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1789,10 +1789,13 @@ const loadNnueModel = async (
17891789
storage: StockfishModelStorage,
17901790
timeoutMs: number,
17911791
onNetworkFetchStart?: () => void,
1792+
forceRefresh = false,
17921793
): Promise<ArrayBuffer> => {
1793-
const cachedModel = await storage.getModel(modelUrl)
1794-
if (cachedModel) {
1795-
return cachedModel
1794+
if (!forceRefresh) {
1795+
const cachedModel = await storage.getModel(modelUrl)
1796+
if (cachedModel) {
1797+
return cachedModel
1798+
}
17961799
}
17971800

17981801
onNetworkFetchStart?.()
@@ -1808,16 +1811,27 @@ const loadNnueModel = async (
18081811
return buffer
18091812
}
18101813

1814+
const shouldRetryNnueLoad = (error: unknown): boolean => {
1815+
if (error instanceof RangeError) {
1816+
return true
1817+
}
1818+
1819+
if (!(error instanceof Error)) {
1820+
return false
1821+
}
1822+
1823+
return (
1824+
/offset is out of bounds/i.test(error.message) ||
1825+
/could not allocate/i.test(error.message) ||
1826+
/bytes\?/i.test(error.message)
1827+
)
1828+
}
1829+
18111830
const setupStockfish = async (
18121831
onPhaseChange?: (phase: StockfishInitPhase) => void,
18131832
): Promise<StockfishWeb> => {
1814-
onPhaseChange?.('loading-module')
18151833
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18161834
const makeModule: any = await import('lila-stockfish-web/sf17-79.js')
1817-
const instance: StockfishWeb = await makeModule.default({
1818-
wasmMemory: sharedWasmMemory(2560),
1819-
locateFile: (name: string) => `/stockfish/${name}`,
1820-
})
18211835

18221836
// NNUE weights served via raw.githubusercontent.com permalink (CORS + COEP compatible).
18231837
// Override with NEXT_PUBLIC_STOCKFISH_NNUE_BASE_URL for self-hosted deployments.
@@ -1826,31 +1840,84 @@ const setupStockfish = async (
18261840
'https://raw.githubusercontent.com/CSSLab/maia-platform-frontend/e23a50e/public/stockfish'
18271841
const storage = new StockfishModelStorage()
18281842
await storage.requestPersistentStorage()
1829-
1830-
const nnue0Url = `${nnueBaseUrl}/${instance.getRecommendedNnue(0)}`
1831-
const nnue1Url = `${nnueBaseUrl}/${instance.getRecommendedNnue(1)}`
18321843
const timeoutMs = getNnueFetchTimeoutMs()
1833-
let downloadStarted = false
1844+
let nnueUrls: [string, string] | null = null
1845+
1846+
const createInstance = async (): Promise<StockfishWeb> => {
1847+
onPhaseChange?.('loading-module')
1848+
return makeModule.default({
1849+
wasmMemory: sharedWasmMemory(2560),
1850+
locateFile: (name: string) => `/stockfish/${name}`,
1851+
})
1852+
}
1853+
1854+
const loadWeightsIntoInstance = async (
1855+
instance: StockfishWeb,
1856+
forceRefresh = false,
1857+
): Promise<void> => {
1858+
if (!nnueUrls) {
1859+
throw new Error('Stockfish recommended NNUE URLs were not initialized')
1860+
}
1861+
1862+
let downloadStarted = false
18341863

1835-
try {
18361864
onPhaseChange?.('checking-cache')
18371865
const buffers = await Promise.all([
1838-
loadNnueModel(nnue0Url, storage, timeoutMs, () => {
1839-
if (!downloadStarted) {
1840-
downloadStarted = true
1841-
onPhaseChange?.('downloading-nnue')
1842-
}
1843-
}),
1844-
loadNnueModel(nnue1Url, storage, timeoutMs, () => {
1845-
if (!downloadStarted) {
1846-
downloadStarted = true
1847-
onPhaseChange?.('downloading-nnue')
1848-
}
1849-
}),
1866+
loadNnueModel(
1867+
nnueUrls[0],
1868+
storage,
1869+
timeoutMs,
1870+
() => {
1871+
if (!downloadStarted) {
1872+
downloadStarted = true
1873+
onPhaseChange?.('downloading-nnue')
1874+
}
1875+
},
1876+
forceRefresh,
1877+
),
1878+
loadNnueModel(
1879+
nnueUrls[1],
1880+
storage,
1881+
timeoutMs,
1882+
() => {
1883+
if (!downloadStarted) {
1884+
downloadStarted = true
1885+
onPhaseChange?.('downloading-nnue')
1886+
}
1887+
},
1888+
forceRefresh,
1889+
),
18501890
])
1891+
18511892
onPhaseChange?.('loading-nnue')
18521893
instance.setNnueBuffer(new Uint8Array(buffers[0]), 0)
18531894
instance.setNnueBuffer(new Uint8Array(buffers[1]), 1)
1895+
}
1896+
1897+
try {
1898+
let instance = await createInstance()
1899+
nnueUrls = [
1900+
`${nnueBaseUrl}/${instance.getRecommendedNnue(0)}`,
1901+
`${nnueBaseUrl}/${instance.getRecommendedNnue(1)}`,
1902+
]
1903+
1904+
try {
1905+
await loadWeightsIntoInstance(instance)
1906+
} catch (error) {
1907+
if (!shouldRetryNnueLoad(error)) {
1908+
throw error
1909+
}
1910+
1911+
console.warn(
1912+
'Stockfish NNUE load failed; clearing cached weights and retrying once.',
1913+
error,
1914+
)
1915+
await Promise.all(nnueUrls.map((url) => storage.deleteModel(url)))
1916+
1917+
instance = await createInstance()
1918+
await loadWeightsIntoInstance(instance, true)
1919+
}
1920+
18541921
return instance
18551922
} catch (error) {
18561923
console.error('Failed to load NNUE models:', error)

0 commit comments

Comments
 (0)