Skip to content

Commit c665f12

Browse files
David WagerDavid Wager
authored andcommitted
feat: add 'Drill from Position' functionality to the analysis screen
1 parent 876ceb9 commit c665f12

File tree

6 files changed

+472
-8
lines changed

6 files changed

+472
-8
lines changed

src/components/Analysis/ConfigurableScreens.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface Props {
2020
onDeleteCustomGame?: () => void
2121
onAnalyzeEntireGame?: () => void
2222
onLearnFromMistakes?: () => void
23+
onDrillFromPosition?: () => void
2324
isAnalysisInProgress?: boolean
2425
isLearnFromMistakesActive?: boolean
2526
autoSave?: {
@@ -51,6 +52,7 @@ export const ConfigurableScreens: React.FC<Props> = ({
5152
onDeleteCustomGame,
5253
onAnalyzeEntireGame,
5354
onLearnFromMistakes,
55+
onDrillFromPosition,
5456
isAnalysisInProgress,
5557
isLearnFromMistakesActive,
5658
autoSave,
@@ -161,6 +163,7 @@ export const ConfigurableScreens: React.FC<Props> = ({
161163
onDeleteCustomGame={onDeleteCustomGame}
162164
onAnalyzeEntireGame={onAnalyzeEntireGame}
163165
onLearnFromMistakes={onLearnFromMistakes}
166+
onDrillFromPosition={onDrillFromPosition}
164167
isAnalysisInProgress={isAnalysisInProgress}
165168
isLearnFromMistakesActive={isLearnFromMistakesActive}
166169
autoSave={autoSave}

src/components/Analysis/ConfigureAnalysis.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface Props {
1212
onDeleteCustomGame?: () => void
1313
onAnalyzeEntireGame?: () => void
1414
onLearnFromMistakes?: () => void
15+
onDrillFromPosition?: () => void
1516
isAnalysisInProgress?: boolean
1617
isLearnFromMistakesActive?: boolean
1718
autoSave?: {
@@ -30,6 +31,7 @@ export const ConfigureAnalysis: React.FC<Props> = ({
3031
onDeleteCustomGame,
3132
onAnalyzeEntireGame,
3233
onLearnFromMistakes,
34+
onDrillFromPosition,
3335
isAnalysisInProgress = false,
3436
isLearnFromMistakesActive = false,
3537
autoSave,
@@ -90,6 +92,17 @@ export const ConfigureAnalysis: React.FC<Props> = ({
9092
</div>
9193
</button>
9294
)}
95+
{onDrillFromPosition && (
96+
<button
97+
onClick={onDrillFromPosition}
98+
className="flex w-full items-center gap-1.5 rounded-sm bg-human-4/60 !px-2 !py-1 !text-sm text-primary/70 transition duration-200 hover:bg-human-4/80 hover:text-primary"
99+
>
100+
<div className="flex items-center justify-center gap-1.5">
101+
<span className="material-symbols-outlined !text-sm">explore</span>
102+
<span className="text-xs">Drill from this position</span>
103+
</div>
104+
</button>
105+
)}
93106
{autoSave &&
94107
game.type !== 'custom-pgn' &&
95108
game.type !== 'custom-fen' &&
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import React, { useState, useMemo } from 'react'
2+
import { MAIA_MODELS_WITH_NAMES } from 'src/constants/common'
3+
import { GameBoard } from 'src/components/Board'
4+
import { GameNode } from 'src/types'
5+
6+
interface DrillFromPositionConfig {
7+
maiaVersion: string
8+
targetMoveNumber: number
9+
drillCount: number
10+
playerColor: 'white' | 'black'
11+
position: {
12+
fen: string
13+
turn: string
14+
pgn: string
15+
}
16+
}
17+
18+
interface Props {
19+
isOpen: boolean
20+
onClose: () => void
21+
onConfirm: (config: DrillFromPositionConfig) => void
22+
currentNode: GameNode
23+
initialPgn: string
24+
}
25+
26+
export const DrillFromPositionModal: React.FC<Props> = ({
27+
isOpen,
28+
onClose,
29+
onConfirm,
30+
currentNode,
31+
initialPgn,
32+
}) => {
33+
// Initialize with detected player color from current position
34+
const playerColor = useMemo(() => {
35+
return currentNode.turn === 'w' ? 'white' : 'black'
36+
}, [currentNode.turn])
37+
38+
const [selectedMaiaVersion, setSelectedMaiaVersion] = useState(
39+
MAIA_MODELS_WITH_NAMES[4], // Default to Maia 1500
40+
)
41+
const [targetMoveNumber, setTargetMoveNumber] = useState(10)
42+
const [drillCount, setDrillCount] = useState(3)
43+
44+
const handleConfirm = () => {
45+
const config: DrillFromPositionConfig = {
46+
maiaVersion: selectedMaiaVersion.id,
47+
targetMoveNumber,
48+
drillCount,
49+
playerColor,
50+
position: {
51+
fen: currentNode.fen,
52+
turn: currentNode.turn || 'w',
53+
pgn: initialPgn,
54+
},
55+
}
56+
onConfirm(config)
57+
}
58+
59+
if (!isOpen) return null
60+
61+
return (
62+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
63+
<div className="relative flex h-[90vh] max-h-[700px] w-[95vw] max-w-[900px] flex-col overflow-hidden rounded-lg bg-background-1 shadow-2xl">
64+
{/* Header */}
65+
<div className="flex items-center justify-between border-b border-white/10 p-4">
66+
<div>
67+
<h2 className="text-xl font-bold text-primary">
68+
Configure Drill from Position
69+
</h2>
70+
<p className="mt-1 text-sm text-secondary">
71+
Set up your practice session from the current analysis position
72+
</p>
73+
</div>
74+
<button
75+
onClick={onClose}
76+
className="rounded p-2 text-secondary transition-colors hover:bg-white/10 hover:text-primary"
77+
>
78+
<span className="material-symbols-outlined">close</span>
79+
</button>
80+
</div>
81+
82+
{/* Content */}
83+
<div className="flex flex-1 overflow-hidden">
84+
{/* Left Panel - Configuration Options */}
85+
<div className="flex w-1/2 flex-col border-r border-white/10 p-4">
86+
<div className="space-y-6">
87+
{/* Maia Engine Strength */}
88+
<div>
89+
<label
90+
htmlFor="maia-strength"
91+
className="mb-2 block text-sm font-medium text-primary"
92+
>
93+
Maia Engine Strength
94+
</label>
95+
<select
96+
id="maia-strength"
97+
value={selectedMaiaVersion.id}
98+
onChange={(e) => {
99+
const version = MAIA_MODELS_WITH_NAMES.find(
100+
(v) => v.id === e.target.value,
101+
)
102+
if (version) {
103+
setSelectedMaiaVersion(version)
104+
}
105+
}}
106+
className="w-full rounded bg-background-2 p-2 text-sm focus:outline-none focus:ring-2 focus:ring-human-4"
107+
>
108+
{MAIA_MODELS_WITH_NAMES.map((version) => (
109+
<option key={version.id} value={version.id}>
110+
{version.name}
111+
</option>
112+
))}
113+
</select>
114+
<p className="mt-1 text-xs text-secondary">
115+
Choose the AI opponent strength (1100-1900 rating)
116+
</p>
117+
</div>
118+
119+
{/* Target Move Count */}
120+
<div>
121+
<label
122+
htmlFor="target-moves"
123+
className="mb-2 block text-sm font-medium text-primary"
124+
>
125+
Moves per Drill: {targetMoveNumber}
126+
</label>
127+
<input
128+
id="target-moves"
129+
type="range"
130+
min="5"
131+
max="20"
132+
value={targetMoveNumber}
133+
onChange={(e) =>
134+
setTargetMoveNumber(parseInt(e.target.value))
135+
}
136+
className="w-full accent-human-4"
137+
/>
138+
<div className="mt-1 flex justify-between text-xs text-secondary">
139+
<span>5 moves</span>
140+
<span>20 moves</span>
141+
</div>
142+
<p className="mt-1 text-xs text-secondary">
143+
How many moves to play in each drill session
144+
</p>
145+
</div>
146+
147+
{/* Number of Drills */}
148+
<div>
149+
<label
150+
htmlFor="drill-count"
151+
className="mb-2 block text-sm font-medium text-primary"
152+
>
153+
Number of Drills: {drillCount}
154+
</label>
155+
<input
156+
id="drill-count"
157+
type="range"
158+
min="1"
159+
max="10"
160+
value={drillCount}
161+
onChange={(e) => setDrillCount(parseInt(e.target.value))}
162+
className="w-full accent-human-4"
163+
/>
164+
<div className="mt-1 flex justify-between text-xs text-secondary">
165+
<span>1 drill</span>
166+
<span>10 drills</span>
167+
</div>
168+
<p className="mt-1 text-xs text-secondary">
169+
Total number of practice sessions from this position
170+
</p>
171+
</div>
172+
173+
{/* Player Color Info */}
174+
<div className="rounded bg-background-2/50 p-3">
175+
<h4 className="mb-2 text-xs font-medium text-primary">
176+
Player Color
177+
</h4>
178+
<div className="flex items-center gap-2 text-sm">
179+
<div
180+
className={`h-4 w-4 rounded border ${
181+
playerColor === 'white'
182+
? 'border-gray-400 bg-white'
183+
: 'border-gray-400 bg-gray-800'
184+
}`}
185+
></div>
186+
<span className="text-primary">
187+
Playing as {playerColor} (to move in this position)
188+
</span>
189+
</div>
190+
<p className="mt-1 text-xs text-secondary">
191+
You&apos;ll practice from this position as the player to move
192+
</p>
193+
</div>
194+
</div>
195+
196+
{/* Action Buttons */}
197+
<div className="mt-auto flex gap-3 pt-6">
198+
<button
199+
onClick={onClose}
200+
className="flex-1 rounded bg-background-2 py-2 text-sm font-medium transition-colors hover:bg-background-3"
201+
>
202+
Cancel
203+
</button>
204+
<button
205+
onClick={handleConfirm}
206+
className="flex-1 rounded bg-human-4 py-2 text-sm font-medium text-background-1 transition-colors hover:bg-human-4/80"
207+
>
208+
Start Drilling
209+
</button>
210+
</div>
211+
</div>
212+
213+
{/* Right Panel - Position Preview */}
214+
<div className="flex w-1/2 flex-col p-4">
215+
<h3 className="mb-3 text-sm font-medium text-primary">
216+
Position Preview
217+
</h3>
218+
{/* Board Container */}
219+
<div className="flex flex-1 items-center justify-center">
220+
<div className="aspect-square w-full max-w-[320px]">
221+
<GameBoard
222+
currentNode={currentNode}
223+
orientation={playerColor}
224+
availableMoves={new Map()} // No moves in preview mode
225+
shapes={[]} // No shapes in preview mode
226+
/>
227+
</div>
228+
</div>
229+
230+
{/* Position Info */}
231+
<div className="mt-4 space-y-2 text-xs text-secondary">
232+
<div className="flex justify-between">
233+
<span>Position:</span>
234+
<span className="font-mono text-xs">
235+
{currentNode.fen.split(' ').slice(0, 2).join(' ')}
236+
</span>
237+
</div>
238+
<div className="flex justify-between">
239+
<span>To move:</span>
240+
<span className="capitalize">
241+
{currentNode.turn === 'w' ? 'White' : 'Black'}
242+
</span>
243+
</div>
244+
{currentNode.san && (
245+
<div className="flex justify-between">
246+
<span>Last move:</span>
247+
<span className="font-mono">{currentNode.san}</span>
248+
</div>
249+
)}
250+
</div>
251+
252+
{/* Drill Summary */}
253+
<div className="mt-4 rounded bg-background-2/50 p-3">
254+
<h4 className="mb-2 text-xs font-medium text-primary">
255+
Drill Summary
256+
</h4>
257+
<div className="space-y-1 text-xs text-secondary">
258+
<div>
259+
• Play as {playerColor} against {selectedMaiaVersion.name}
260+
</div>
261+
<div>{targetMoveNumber} moves per drill session</div>
262+
<div>
263+
{drillCount} total drill{drillCount !== 1 ? 's' : ''}
264+
</div>
265+
<div>• Practice from current analysis position</div>
266+
</div>
267+
</div>
268+
</div>
269+
</div>
270+
</div>
271+
</div>
272+
)
273+
}

src/hooks/useOpeningDrillController/useOpeningDrillController.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ export const useOpeningDrillController = (
169169

170170
setAnalysisProgress({ total: 0, completed: 0, currentMove: null })
171171

172+
// Use custom FEN if available, otherwise default starting position
172173
const startingFen =
174+
currentDrill.opening.fen ||
173175
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
174176
const gameTree = new GameTree(startingFen)
175177

0 commit comments

Comments
 (0)