Skip to content

Commit 0159477

Browse files
chore: update with moves container + variations
1 parent b9a9fd8 commit 0159477

5 files changed

Lines changed: 477 additions & 182 deletions

File tree

src/components/Openings/OpeningDrillAnalysis.tsx

Lines changed: 168 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,148 @@
1-
import React from 'react'
1+
import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react'
22
import { Highlight, MoveMap, BlunderMeter } from '../Analysis'
3-
import { useAnalysisController } from 'src/hooks'
4-
import { AnalyzedGame } from 'src/types'
3+
import { GameNode } from 'src/types'
54
import { GameTree } from 'src/types/base/tree'
6-
import { Chess } from 'chess.ts'
7-
8-
// Create a mock analyzed game for when no real game is available
9-
const createMockAnalyzedGame = (): AnalyzedGame => ({
10-
id: 'mock',
11-
blackPlayer: { name: 'Player', rating: undefined },
12-
whitePlayer: { name: 'Player', rating: undefined },
13-
moves: [],
14-
availableMoves: [],
15-
gameType: 'play',
16-
termination: {
17-
result: '*',
18-
winner: 'none',
19-
condition: 'Normal',
20-
},
21-
maiaEvaluations: [],
22-
stockfishEvaluations: [],
23-
tree: new GameTree(new Chess().fen()),
24-
type: 'play',
25-
})
5+
import { Chess, PieceSymbol } from 'chess.ts'
6+
import type { Key } from 'chessground/types'
7+
import type { DrawShape } from 'chessground/draw'
8+
import toast from 'react-hot-toast'
269

2710
interface Props {
28-
analyzedGame: AnalyzedGame | null
11+
currentNode: GameNode | null
12+
gameTree: GameTree | null
2913
analysisEnabled: boolean
3014
onToggleAnalysis: () => void
15+
playerColor: 'white' | 'black'
16+
maiaVersion: string
17+
analysisController: any // Analysis controller passed from parent
3118
}
3219

3320
export const OpeningDrillAnalysis: React.FC<Props> = ({
34-
analyzedGame,
21+
currentNode,
22+
gameTree,
3523
analysisEnabled,
3624
onToggleAnalysis,
25+
playerColor,
26+
maiaVersion,
27+
analysisController,
3728
}) => {
38-
// Always call the hook but use mock data when disabled
39-
const analysisController = useAnalysisController(
40-
analyzedGame || createMockAnalyzedGame(),
41-
'white',
29+
const [hoverArrow, setHoverArrow] = useState<DrawShape | null>(null)
30+
const toastId = useRef<string | null>(null)
31+
32+
// Toast notifications for Maia model status
33+
useEffect(() => {
34+
return () => {
35+
toast.dismiss()
36+
}
37+
}, [])
38+
39+
useEffect(() => {
40+
if (analysisController.maiaStatus === 'loading' && !toastId.current) {
41+
toastId.current = toast.loading('Loading Maia Model...')
42+
} else if (analysisController.maiaStatus === 'ready') {
43+
if (toastId.current) {
44+
toast.success('Loaded Maia! Analysis is ready', {
45+
id: toastId.current,
46+
})
47+
toastId.current = null
48+
} else {
49+
toast.success('Loaded Maia! Analysis is ready')
50+
}
51+
}
52+
}, [analysisController.maiaStatus])
53+
54+
const hover = useCallback(
55+
(move?: string) => {
56+
if (move && analysisEnabled) {
57+
setHoverArrow({
58+
orig: move.slice(0, 2) as Key,
59+
dest: move.slice(2, 4) as Key,
60+
brush: 'green',
61+
modifiers: { lineWidth: 10 },
62+
})
63+
} else {
64+
setHoverArrow(null)
65+
}
66+
},
67+
[analysisEnabled],
68+
)
69+
70+
const makeMove = useCallback(
71+
(move: string) => {
72+
if (!analysisEnabled || !currentNode || !gameTree) return
73+
74+
const chess = new Chess(currentNode.fen)
75+
const moveAttempt = chess.move({
76+
from: move.slice(0, 2),
77+
to: move.slice(2, 4),
78+
promotion: move[4] ? (move[4] as PieceSymbol) : undefined,
79+
})
80+
81+
if (moveAttempt) {
82+
const newFen = chess.fen()
83+
const moveString =
84+
moveAttempt.from +
85+
moveAttempt.to +
86+
(moveAttempt.promotion ? moveAttempt.promotion : '')
87+
const san = moveAttempt.san
88+
89+
// Check if this move already exists as a child
90+
const existingChild = currentNode.children.find(
91+
(child: any) => child.move === moveString,
92+
)
93+
94+
if (existingChild) {
95+
// Move already exists, just navigate to it
96+
analysisController.goToNode(existingChild)
97+
} else if (currentNode.mainChild?.move === moveString) {
98+
// This is the main child move
99+
analysisController.goToNode(currentNode.mainChild)
100+
} else {
101+
// Create new variation
102+
const newVariation = gameTree.addVariation(
103+
currentNode,
104+
newFen,
105+
moveString,
106+
san,
107+
analysisController.currentMaiaModel,
108+
)
109+
analysisController.goToNode(newVariation)
110+
}
111+
}
112+
},
113+
[analysisEnabled, currentNode, gameTree, analysisController],
42114
)
43-
const mockHover = () => {
44-
// No-op for disabled analysis
45-
}
46115

47-
const mockMakeMove = () => {
48-
// No-op for disabled analysis
49-
}
116+
// No-op handlers for blurred analysis components when disabled
117+
const mockHover = useCallback(() => {
118+
// Intentionally empty - no interaction allowed when analysis disabled
119+
}, [])
120+
121+
const mockMakeMove = useCallback(() => {
122+
// Intentionally empty - no moves allowed when analysis disabled
123+
}, [])
124+
125+
const mockSetHoverArrow = useCallback(() => {
126+
// Intentionally empty - no hover arrows when analysis disabled
127+
}, [])
128+
129+
// Create empty data structures that match expected types
130+
const emptyBlunderMeterData = useMemo(
131+
() => ({
132+
goodMoves: { moves: [], probability: 0 },
133+
okMoves: { moves: [], probability: 0 },
134+
blunderMoves: { moves: [], probability: 0 },
135+
}),
136+
[],
137+
)
50138

51-
const mockSetHoverArrow = () => {
52-
// No-op for disabled analysis
53-
}
139+
const emptyRecommendations = useMemo(
140+
() => ({
141+
maia: undefined,
142+
stockfish: undefined,
143+
}),
144+
[],
145+
)
54146

55147
return (
56148
<div className="flex h-[calc(55vh+4.5rem)] w-full flex-col gap-2">
@@ -79,21 +171,31 @@ export const OpeningDrillAnalysis: React.FC<Props> = ({
79171
<div className="relative">
80172
<div className="flex h-[calc((55vh+4.5rem)/2)]">
81173
<Highlight
82-
hover={mockHover}
83-
makeMove={mockMakeMove}
84-
currentMaiaModel="maia_kdd_1500"
85-
recommendations={analysisController.moveRecommendations}
174+
hover={analysisEnabled ? hover : mockHover}
175+
makeMove={analysisEnabled ? makeMove : mockMakeMove}
176+
currentMaiaModel={analysisController.currentMaiaModel}
177+
recommendations={
178+
analysisEnabled
179+
? analysisController.moveRecommendations
180+
: emptyRecommendations
181+
}
86182
moveEvaluation={
87-
analysisController.moveEvaluation || {
88-
maia: undefined,
89-
stockfish: undefined,
90-
}
183+
analysisEnabled && analysisController.moveEvaluation
184+
? analysisController.moveEvaluation
185+
: {
186+
maia: undefined,
187+
stockfish: undefined,
188+
}
189+
}
190+
movesByRating={
191+
analysisEnabled ? analysisController.movesByRating : undefined
192+
}
193+
colorSanMapping={
194+
analysisEnabled ? analysisController.colorSanMapping : {}
91195
}
92-
movesByRating={analysisController.movesByRating}
93-
colorSanMapping={analysisController.colorSanMapping}
94196
boardDescription={
95197
analysisEnabled
96-
? 'This position offers multiple strategic options. Consider central control and piece development.'
198+
? analysisController.boardDescription || 'Analyzing position...'
97199
: 'Analysis is disabled. Enable analysis to see detailed move evaluations and recommendations.'
98200
}
99201
/>
@@ -118,16 +220,26 @@ export const OpeningDrillAnalysis: React.FC<Props> = ({
118220
<div className="flex h-[calc((55vh+4.5rem)/2)] flex-row gap-2">
119221
<div className="flex h-full w-full flex-col">
120222
<MoveMap
121-
moveMap={analysisController.moveMap}
122-
colorSanMapping={analysisController.colorSanMapping}
123-
setHoverArrow={mockSetHoverArrow}
223+
moveMap={analysisEnabled ? analysisController.moveMap : undefined}
224+
colorSanMapping={
225+
analysisEnabled ? analysisController.colorSanMapping : {}
226+
}
227+
setHoverArrow={
228+
analysisEnabled ? setHoverArrow : mockSetHoverArrow
229+
}
124230
/>
125231
</div>
126232
<BlunderMeter
127-
hover={mockHover}
128-
makeMove={mockMakeMove}
129-
data={analysisController.blunderMeter}
130-
colorSanMapping={analysisController.colorSanMapping}
233+
hover={analysisEnabled ? hover : mockHover}
234+
makeMove={analysisEnabled ? makeMove : mockMakeMove}
235+
data={
236+
analysisEnabled
237+
? analysisController.blunderMeter
238+
: emptyBlunderMeterData
239+
}
240+
colorSanMapping={
241+
analysisEnabled ? analysisController.colorSanMapping : {}
242+
}
131243
/>
132244
</div>
133245
{!analysisEnabled && (

src/components/Openings/OpeningDrillSidebar.tsx

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,54 @@
11
import React from 'react'
2-
import { OpeningSelection } from 'src/types'
2+
import { OpeningSelection, GameNode, GameTree } from 'src/types'
33
import { GameInfo } from '../Misc/GameInfo'
4+
import { MovesContainer } from '../Board/MovesContainer'
5+
import { BoardController } from '../Board/BoardController'
46

57
interface Props {
68
selections: OpeningSelection[]
79
currentSelectionIndex: number
810
onSwitchSelection: (index: number) => void
911
onResetCurrent: () => void
12+
gameTree: GameTree
13+
currentNode: GameNode
14+
goToNode: (node: GameNode) => void
15+
goToNextNode: () => void
16+
goToPreviousNode: () => void
17+
goToRootNode: () => void
18+
plyCount: number
19+
orientation: 'white' | 'black'
20+
setOrientation: (orientation: 'white' | 'black') => void
1021
}
1122

1223
export const OpeningDrillSidebar: React.FC<Props> = ({
1324
selections,
1425
currentSelectionIndex,
1526
onSwitchSelection,
1627
onResetCurrent,
28+
gameTree,
29+
currentNode,
30+
goToNode,
31+
goToNextNode,
32+
goToPreviousNode,
33+
goToRootNode,
34+
plyCount,
35+
orientation,
36+
setOrientation,
1737
}) => {
1838
const currentSelection = selections[currentSelectionIndex]
1939

40+
// Create a base game structure for MovesContainer
41+
const baseGame = {
42+
id: currentSelection?.id || 'opening-drill',
43+
tree: gameTree,
44+
moves: [],
45+
termination: {
46+
result: '*',
47+
winner: 'none' as const,
48+
condition: 'Normal',
49+
},
50+
}
51+
2052
return (
2153
<div className="flex h-[85vh] w-72 min-w-60 max-w-72 flex-col gap-2 overflow-hidden 2xl:min-w-72">
2254
<GameInfo title="Opening Drill" icon="school" type="analysis">
@@ -53,7 +85,7 @@ export const OpeningDrillSidebar: React.FC<Props> = ({
5385
</div>
5486
</GameInfo>
5587

56-
<div className="flex flex-col overflow-hidden rounded bg-background-1">
88+
<div className="flex max-h-[25vh] min-h-[25vh] flex-col overflow-hidden rounded bg-background-1">
5789
<div className="flex items-center justify-between border-b border-white/10 p-3">
5890
<h3 className="font-semibold">
5991
Selected Openings ({selections.length})
@@ -123,6 +155,23 @@ export const OpeningDrillSidebar: React.FC<Props> = ({
123155
))}
124156
</div>
125157
</div>
158+
159+
<div className="flex h-1/2 w-full flex-1 flex-col gap-2">
160+
<div className="flex h-full flex-col overflow-y-scroll">
161+
<MovesContainer game={baseGame} type="analysis" />
162+
<BoardController
163+
gameTree={gameTree}
164+
orientation={orientation}
165+
setOrientation={setOrientation}
166+
currentNode={currentNode}
167+
plyCount={plyCount}
168+
goToNode={goToNode}
169+
goToNextNode={goToNextNode}
170+
goToPreviousNode={goToPreviousNode}
171+
goToRootNode={goToRootNode}
172+
/>
173+
</div>
174+
</div>
126175
</div>
127176
)
128177
}

0 commit comments

Comments
 (0)