Skip to content

Commit 798c407

Browse files
fix: opening drill sidebar
1 parent 25ad9de commit 798c407

4 files changed

Lines changed: 157 additions & 3 deletions

File tree

src/components/Openings/OpeningDrillSidebar.tsx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface Props {
1010
totalDrills: number
1111
onResetCurrentDrill: () => void
1212
onChangeSelections?: () => void
13+
onLoadCompletedDrill?: (drill: CompletedDrill) => void
1314
}
1415

1516
export const OpeningDrillSidebar: React.FC<Props> = ({
@@ -20,6 +21,7 @@ export const OpeningDrillSidebar: React.FC<Props> = ({
2021
totalDrills,
2122
onResetCurrentDrill,
2223
onChangeSelections,
24+
onLoadCompletedDrill,
2325
}) => {
2426
return (
2527
<div className="flex h-full w-full flex-col border-r border-white/10 bg-background-1 2xl:min-w-72">
@@ -120,6 +122,11 @@ export const OpeningDrillSidebar: React.FC<Props> = ({
120122
</div>
121123
) : (
122124
<div className="flex flex-col">
125+
{onLoadCompletedDrill && (
126+
<div className="px-3 py-2 text-xs text-secondary">
127+
Click on any drill below to analyze it
128+
</div>
129+
)}
123130
{completedDrills.map((completedDrill, index) => {
124131
const accuracy =
125132
completedDrill.totalMoves > 0
@@ -130,10 +137,32 @@ export const OpeningDrillSidebar: React.FC<Props> = ({
130137
)
131138
: 0
132139

140+
// Check if this drill is currently loaded
141+
const isCurrentlyLoaded =
142+
currentDrill &&
143+
currentDrill.opening.id ===
144+
completedDrill.selection.opening.id &&
145+
currentDrill.variation?.id ===
146+
completedDrill.selection.variation?.id &&
147+
currentDrill.playerColor ===
148+
completedDrill.selection.playerColor &&
149+
currentDrill.maiaVersion ===
150+
completedDrill.selection.maiaVersion
151+
133152
return (
134-
<div
153+
<button
135154
key={completedDrill.selection.id}
136-
className="border-b border-white/5 bg-background-1 px-3 py-2 transition-colors"
155+
className={`w-full border-b border-white/5 px-3 py-2 text-left transition-colors ${
156+
isCurrentlyLoaded
157+
? 'border-human-4/30 bg-human-4/20'
158+
: 'bg-background-1'
159+
} ${
160+
onLoadCompletedDrill
161+
? 'cursor-pointer hover:bg-background-2'
162+
: ''
163+
}`}
164+
onClick={() => onLoadCompletedDrill?.(completedDrill)}
165+
disabled={!onLoadCompletedDrill}
137166
>
138167
<div className="flex items-start justify-between">
139168
<div className="min-w-0 flex-1">
@@ -191,7 +220,7 @@ export const OpeningDrillSidebar: React.FC<Props> = ({
191220
{completedDrill.blunders.length}
192221
</span>
193222
</div>
194-
</div>
223+
</button>
195224
)
196225
})}
197226
</div>

src/hooks/useOpeningDrillController/useOpeningDrillController.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ export const useOpeningDrillController = (
251251
: index % 2 === 1
252252
return isPlayerMove
253253
}),
254+
allMoves: drillGame.moves, // Store the full move sequence
254255
totalMoves: playerMoveCount,
255256
blunders: Array(blunders).fill('placeholder'),
256257
goodMoves: Array(goodMoves).fill('placeholder'),
@@ -356,6 +357,125 @@ export const useOpeningDrillController = (
356357
setWaitingForMaiaResponse(false)
357358
}, [])
358359

360+
// Load a specific completed drill for analysis
361+
const loadCompletedDrill = useCallback(
362+
(completedDrill: CompletedDrill) => {
363+
// Set the drill as current
364+
setCurrentDrill(completedDrill.selection)
365+
366+
// Check if this drill is already the current drill and we can reuse the game tree
367+
if (
368+
currentDrillGame &&
369+
currentDrillGame.selection.id === completedDrill.selection.id &&
370+
currentDrillGame.playerMoveCount === completedDrill.totalMoves
371+
) {
372+
// Reuse the existing game tree and just enable analysis mode
373+
setAnalysisEnabled(true)
374+
setContinueAnalyzingMode(true)
375+
setWaitingForMaiaResponse(false)
376+
return
377+
}
378+
379+
// Try to reconstruct the game tree from the finalNode path
380+
const startingFen =
381+
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
382+
const gameTree = new GameTree(startingFen)
383+
384+
// Parse the PGN to populate the tree with opening moves
385+
const pgn = completedDrill.selection.variation
386+
? completedDrill.selection.variation.pgn
387+
: completedDrill.selection.opening.pgn
388+
const endNode = parsePgnToTree(pgn, gameTree)
389+
390+
let finalNode = endNode
391+
392+
// Reconstruct the full game using the stored allMoves sequence
393+
if (
394+
endNode &&
395+
completedDrill.allMoves &&
396+
completedDrill.allMoves.length > 0
397+
) {
398+
let currentNode = endNode
399+
const chess = new Chess(endNode.fen)
400+
401+
// Replay all moves from the drill (both player and Maia moves)
402+
for (const moveUci of completedDrill.allMoves) {
403+
try {
404+
const moveObj = chess.move(moveUci, { sloppy: true })
405+
if (moveObj) {
406+
const newNode = gameTree.addMainMove(
407+
currentNode,
408+
chess.fen(),
409+
moveUci,
410+
moveObj.san,
411+
)
412+
if (newNode) {
413+
currentNode = newNode
414+
finalNode = newNode
415+
}
416+
}
417+
} catch (error) {
418+
console.error('Error replaying move:', moveUci, error)
419+
break
420+
}
421+
}
422+
} else if (endNode && completedDrill.playerMoves.length > 0) {
423+
// Fallback: use only player moves if allMoves is not available
424+
let currentNode = endNode
425+
const chess = new Chess(endNode.fen)
426+
427+
for (const moveUci of completedDrill.playerMoves) {
428+
try {
429+
const moveObj = chess.move(moveUci, { sloppy: true })
430+
if (moveObj) {
431+
const newNode = gameTree.addMainMove(
432+
currentNode,
433+
chess.fen(),
434+
moveUci,
435+
moveObj.san,
436+
)
437+
if (newNode) {
438+
currentNode = newNode
439+
finalNode = newNode
440+
}
441+
}
442+
} catch (error) {
443+
console.error('Error replaying player move:', moveUci, error)
444+
break
445+
}
446+
}
447+
}
448+
449+
const loadedGame: OpeningDrillGame = {
450+
id: completedDrill.selection.id + '-replay',
451+
selection: completedDrill.selection,
452+
moves: completedDrill.allMoves || completedDrill.playerMoves,
453+
tree: gameTree,
454+
currentFen: finalNode?.fen || endNode?.fen || startingFen,
455+
toPlay: finalNode
456+
? new Chess(finalNode.fen).turn() === 'w'
457+
? 'white'
458+
: 'black'
459+
: 'white',
460+
openingEndNode: endNode,
461+
playerMoveCount: completedDrill.totalMoves,
462+
}
463+
464+
setCurrentDrillGame(loadedGame)
465+
setAnalysisEnabled(true) // Auto-enable analysis when loading a completed drill
466+
setContinueAnalyzingMode(true) // Allow moves beyond target count
467+
setWaitingForMaiaResponse(false)
468+
469+
// Set the controller to the final position after a brief delay
470+
setTimeout(() => {
471+
if (finalNode) {
472+
controller.setCurrentNode(finalNode)
473+
}
474+
}, 100)
475+
},
476+
[controller, currentDrillGame],
477+
)
478+
359479
// Calculate overall performance data
360480
const overallPerformanceData = useMemo((): OverallPerformanceData => {
361481
if (completedDrills.length === 0) {
@@ -775,5 +895,8 @@ export const useOpeningDrillController = (
775895

776896
// Check if all drills are completed
777897
areAllDrillsCompleted,
898+
899+
// Load a specific completed drill for analysis
900+
loadCompletedDrill,
778901
}
779902
}

src/pages/openings/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ const OpeningsPage: NextPage = () => {
398398
totalDrills={controller.totalDrills}
399399
onResetCurrentDrill={controller.resetCurrentDrill}
400400
onChangeSelections={handleChangeSelections}
401+
onLoadCompletedDrill={controller.loadCompletedDrill}
401402
/>
402403
</div>
403404

src/types/openings/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface CompletedDrill {
5656
selection: OpeningSelection
5757
finalNode: GameNode
5858
playerMoves: string[]
59+
allMoves: string[]
5960
totalMoves: number
6061
blunders: string[]
6162
goodMoves: string[]

0 commit comments

Comments
 (0)