Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 7 additions & 27 deletions src/components/Analysis/AnalysisSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
MoveMap,
Highlight,
BlunderMeter,
MovesByRating,
Expand Down Expand Up @@ -57,7 +56,6 @@ export const AnalysisSidebar: React.FC<Props> = ({
hover,
makeMove,
controller,
setHoverArrow,
analysisEnabled,
handleToggleAnalysis,
hideDetailedBlunderMeter = false,
Expand Down Expand Up @@ -140,14 +138,6 @@ export const AnalysisSidebar: React.FC<Props> = ({
...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 : {},
Expand Down Expand Up @@ -275,31 +265,21 @@ export const AnalysisSidebar: React.FC<Props> = ({
<div className="flex h-full flex-col gap-3 xl:hidden">
<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">
{renderHeader('mobile', 'absolute left-0 top-0 z-10 w-full')}
<div className="flex h-full w-full border-r border-glass-border">
<Highlight {...highlightProps} />
<div className="flex h-full w-full">
<SimplifiedAnalysisOverview
highlightProps={{ ...highlightProps, simplified: true }}
blunderMeterProps={simplifiedBlunderMeterProps}
analysisEnabled={analysisEnabled}
hideBlunderMeter={hideDetailedBlunderMeter}
/>
</div>
{!hideDetailedBlunderMeter && (
<div className="flex h-full w-auto min-w-[40%] max-w-[40%] p-3">
<div className="h-full w-full">
<BlunderMeter {...blunderMeterProps} showContainer={false} />
</div>
</div>
)}
{!analysisEnabled &&
renderDisabledOverlay('Enable analysis to see move evaluations', {
offsetTop: true,
})}
</div>

<div className="desktop-analysis-small-row-2-container relative flex w-full">
<div className="h-full w-full">
<MoveMap {...moveMapProps} />
</div>
{!analysisEnabled &&
renderDisabledOverlay('Enable analysis to see position evaluation')}
</div>

<div className="desktop-analysis-small-row-3-container relative flex w-full">
<div className="relative flex h-full w-full flex-col overflow-hidden rounded-md border border-glass-border bg-glass backdrop-blur-md">
<MovesByRating {...movesByRatingProps} />
{!analysisEnabled &&
Expand Down
45 changes: 11 additions & 34 deletions src/components/Home/Sections/AnalysisSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<DrawShape | null>(null)

useEffect(() => {
const handleResize = () => {
setRenderKey((prev) => prev + 1)
Expand Down Expand Up @@ -274,40 +271,20 @@ export const AnalysisSection = ({ id }: AnalysisSectionProps) => {
</motion.div>
</div>
</div>
<div className="flex flex-col gap-3 md:flex-row">
<motion.div
className="h-64 md:w-1/2"
initial={{ opacity: 0, y: 20 }}
animate={
inView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }
}
transition={{ duration: 0.3, delay: 0.6 }}
>
<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">
<MovesByRating
moves={analysisMockData.movesByRating}
colorSanMapping={analysisMockData.colorSanMapping}
isHomePage={true}
/>
</div>
</motion.div>
<motion.div
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"
initial={{ opacity: 0, y: 20 }}
animate={
inView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }
}
transition={{ duration: 0.3, delay: 0.7 }}
>
<MoveMap
moveMap={analysisMockData.moveMap}
<motion.div
className="h-64"
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
transition={{ duration: 0.3, delay: 0.6 }}
>
<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">
<MovesByRating
moves={analysisMockData.movesByRating}
colorSanMapping={analysisMockData.colorSanMapping}
setHoverArrow={setHoverArrow}
makeMove={handleMakeMove}
isHomePage={true}
/>
</motion.div>
</div>
</div>
</motion.div>
</div>
</div>
</motion.div>
Expand Down
34 changes: 0 additions & 34 deletions src/components/Openings/OpeningDrillAnalysis.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useMemo, useCallback, useContext } from 'react'
import {
Highlight,
MoveMap,
BlunderMeter,
MovesByRating,
AnalysisSidebar,
Expand Down Expand Up @@ -70,10 +69,6 @@ export const OpeningDrillAnalysis: React.FC<Props> = ({
// 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(
() => ({
Expand Down Expand Up @@ -232,35 +227,6 @@ export const OpeningDrillAnalysis: React.FC<Props> = ({
)}
</div>

<div className="relative">
<MoveMap
moveMap={analysisEnabled ? analysisController.moveMap : undefined}
colorSanMapping={
analysisEnabled ? analysisController.colorSanMapping : {}
}
setHoverArrow={
analysisEnabled ? parentSetHoverArrow : mockSetHoverArrow
}
makeMove={analysisEnabled ? makeMove : mockMakeMove}
playerToMove={
analysisEnabled
? (analysisController.currentNode?.turn ?? 'w')
: 'w'
}
/>
{!analysisEnabled && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-backdrop/90 backdrop-blur-sm">
<div className="rounded bg-glass p-4 text-center shadow-lg">
<span className="material-symbols-outlined mb-1 text-xl text-human-3">
lock
</span>
<p className="text-xs font-medium text-primary">
Analysis Disabled
</p>
</div>
</div>
)}
</div>
</div>
</div>
)
Expand Down
117 changes: 92 additions & 25 deletions src/lib/engine/stockfish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1789,10 +1789,13 @@ const loadNnueModel = async (
storage: StockfishModelStorage,
timeoutMs: number,
onNetworkFetchStart?: () => void,
forceRefresh = false,
): Promise<ArrayBuffer> => {
const cachedModel = await storage.getModel(modelUrl)
if (cachedModel) {
return cachedModel
if (!forceRefresh) {
const cachedModel = await storage.getModel(modelUrl)
if (cachedModel) {
return cachedModel
}
}

onNetworkFetchStart?.()
Expand All @@ -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<StockfishWeb> => {
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.
Expand All @@ -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<StockfishWeb> => {
onPhaseChange?.('loading-module')
return makeModule.default({
wasmMemory: sharedWasmMemory(2560),
locateFile: (name: string) => `/stockfish/${name}`,
})
}

const loadWeightsIntoInstance = async (
instance: StockfishWeb,
forceRefresh = false,
): Promise<void> => {
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)
Expand Down
Loading
Loading