Skip to content

Commit 99b0a90

Browse files
feat: add initial iteration of analysis summary modal
1 parent c062cf6 commit 99b0a90

File tree

3 files changed

+268
-0
lines changed

3 files changed

+268
-0
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import React, { useMemo, useEffect } from 'react'
2+
import { motion } from 'framer-motion'
3+
import { AnalyzedGame } from 'src/types'
4+
import { extractPlayerMistakes } from 'src/lib/analysis'
5+
6+
interface Props {
7+
isOpen: boolean
8+
onClose: () => void
9+
game: AnalyzedGame
10+
}
11+
12+
interface GameSummary {
13+
totalMoves: number
14+
whiteMistakes: {
15+
total: number
16+
blunders: number
17+
inaccuracies: number
18+
}
19+
blackMistakes: {
20+
total: number
21+
blunders: number
22+
inaccuracies: number
23+
}
24+
averageDepth: number
25+
positionsAnalyzed: number
26+
}
27+
28+
export const AnalysisSummaryModal: React.FC<Props> = ({
29+
isOpen,
30+
onClose,
31+
game,
32+
}) => {
33+
useEffect(() => {
34+
if (isOpen) {
35+
document.body.style.overflow = 'hidden'
36+
} else {
37+
document.body.style.overflow = 'unset'
38+
}
39+
40+
return () => {
41+
document.body.style.overflow = 'unset'
42+
}
43+
}, [isOpen])
44+
45+
const summary = useMemo((): GameSummary => {
46+
const mainLine = game.tree.getMainLine()
47+
const whiteMistakes = extractPlayerMistakes(game.tree, 'white')
48+
const blackMistakes = extractPlayerMistakes(game.tree, 'black')
49+
50+
// Calculate analysis depth statistics
51+
let totalDepth = 0
52+
let positionsAnalyzed = 0
53+
54+
mainLine.forEach((node) => {
55+
if (node.analysis.stockfish && node.analysis.stockfish.depth > 0) {
56+
totalDepth += node.analysis.stockfish.depth
57+
positionsAnalyzed++
58+
}
59+
})
60+
61+
const averageDepth =
62+
positionsAnalyzed > 0 ? Math.round(totalDepth / positionsAnalyzed) : 0
63+
64+
return {
65+
totalMoves: Math.ceil((mainLine.length - 1) / 2), // Convert plies to moves
66+
whiteMistakes: {
67+
total: whiteMistakes.length,
68+
blunders: whiteMistakes.filter((m) => m.type === 'blunder').length,
69+
inaccuracies: whiteMistakes.filter((m) => m.type === 'inaccuracy')
70+
.length,
71+
},
72+
blackMistakes: {
73+
total: blackMistakes.length,
74+
blunders: blackMistakes.filter((m) => m.type === 'blunder').length,
75+
inaccuracies: blackMistakes.filter((m) => m.type === 'inaccuracy')
76+
.length,
77+
},
78+
averageDepth,
79+
positionsAnalyzed,
80+
}
81+
}, [game.tree])
82+
83+
if (!isOpen) return null
84+
85+
const MistakeSection = ({
86+
title,
87+
color,
88+
mistakes,
89+
playerName,
90+
}: {
91+
title: string
92+
color: string
93+
mistakes: { total: number; blunders: number; inaccuracies: number }
94+
playerName: string
95+
}) => (
96+
<div className="flex flex-col gap-2 rounded-lg border border-white/10 bg-background-2/40 p-4">
97+
<div className="flex items-center gap-2">
98+
<div
99+
className={`h-3 w-3 rounded-full ${color === 'white' ? 'bg-white' : 'border border-white bg-black'}`}
100+
/>
101+
<h3 className="font-semibold">{title}</h3>
102+
<span className="text-sm text-secondary">({playerName})</span>
103+
</div>
104+
105+
{mistakes.total === 0 ? (
106+
<div className="flex items-center gap-2 text-sm text-green-400">
107+
<span className="material-symbols-outlined !text-sm">
108+
check_circle
109+
</span>
110+
<span>No significant mistakes detected</span>
111+
</div>
112+
) : (
113+
<div className="grid grid-cols-3 gap-3 text-sm">
114+
<div className="flex flex-col items-center gap-1 rounded bg-background-3/50 p-2">
115+
<span className="text-lg font-bold text-red-400">
116+
{mistakes.blunders}
117+
</span>
118+
<span className="text-xs text-secondary">Blunders</span>
119+
</div>
120+
<div className="flex flex-col items-center gap-1 rounded bg-background-3/50 p-2">
121+
<span className="text-lg font-bold text-orange-400">
122+
{mistakes.inaccuracies}
123+
</span>
124+
<span className="text-xs text-secondary">Inaccuracies</span>
125+
</div>
126+
<div className="flex flex-col items-center gap-1 rounded bg-background-3/50 p-2">
127+
<span className="text-lg font-bold text-primary">
128+
{mistakes.total}
129+
</span>
130+
<span className="text-xs text-secondary">Total</span>
131+
</div>
132+
</div>
133+
)}
134+
</div>
135+
)
136+
137+
return (
138+
<motion.div
139+
className="absolute left-0 top-0 z-20 flex h-screen w-screen flex-col items-center justify-center bg-black/70 px-4 backdrop-blur-sm md:px-0"
140+
initial={{ opacity: 0 }}
141+
animate={{ opacity: 1 }}
142+
exit={{ opacity: 0 }}
143+
transition={{ duration: 0.2 }}
144+
onClick={(e) => {
145+
if (e.target === e.currentTarget) onClose()
146+
}}
147+
>
148+
<motion.div
149+
className="flex w-full flex-col gap-5 rounded-md border border-white/10 bg-background-1 p-5 md:w-[min(600px,50vw)] md:p-6"
150+
initial={{ y: 20, opacity: 0 }}
151+
animate={{ y: 0, opacity: 1 }}
152+
exit={{ y: 20, opacity: 0 }}
153+
transition={{ duration: 0.3 }}
154+
onClick={(e) => e.stopPropagation()}
155+
>
156+
<div className="flex items-center gap-2">
157+
<span className="material-symbols-outlined text-2xl text-human-3">
158+
analytics
159+
</span>
160+
<h2 className="text-xl font-semibold">Analysis Summary</h2>
161+
</div>
162+
163+
<div className="flex flex-col gap-4">
164+
{/* Game Overview */}
165+
<div className="flex flex-col gap-2">
166+
<h3 className="font-semibold text-primary/90">Game Overview</h3>
167+
<div className="grid grid-cols-2 gap-3 text-sm md:grid-cols-4">
168+
<div className="flex flex-col items-center gap-1 rounded bg-background-2/60 p-3">
169+
<span className="text-lg font-bold text-human-3">
170+
{summary.totalMoves}
171+
</span>
172+
<span className="text-xs text-secondary">Total Moves</span>
173+
</div>
174+
<div className="flex flex-col items-center gap-1 rounded bg-background-2/60 p-3">
175+
<span className="text-lg font-bold text-human-3">
176+
{summary.positionsAnalyzed}
177+
</span>
178+
<span className="text-xs text-secondary">Positions</span>
179+
</div>
180+
<div className="flex flex-col items-center gap-1 rounded bg-background-2/60 p-3">
181+
<span className="text-lg font-bold text-human-3">
182+
d{summary.averageDepth}
183+
</span>
184+
<span className="text-xs text-secondary">Avg Depth</span>
185+
</div>
186+
<div className="flex flex-col items-center gap-1 rounded bg-background-2/60 p-3">
187+
<span className="text-lg font-bold text-green-400">100%</span>
188+
<span className="text-xs text-secondary">Complete</span>
189+
</div>
190+
</div>
191+
</div>
192+
193+
{/* Player Performance */}
194+
<div className="flex flex-col gap-3">
195+
<h3 className="font-semibold text-primary/90">
196+
Player Performance
197+
</h3>
198+
199+
<MistakeSection
200+
title="White"
201+
color="white"
202+
mistakes={summary.whiteMistakes}
203+
playerName={game.whitePlayer.name}
204+
/>
205+
206+
<MistakeSection
207+
title="Black"
208+
color="black"
209+
mistakes={summary.blackMistakes}
210+
playerName={game.blackPlayer.name}
211+
/>
212+
</div>
213+
214+
{/* Analysis Tips */}
215+
<div className="flex items-start gap-2 rounded bg-human-4/10 p-3">
216+
<span className="material-symbols-outlined !text-base text-human-3">
217+
lightbulb
218+
</span>
219+
<div className="flex flex-col gap-1">
220+
<p className="text-sm font-medium text-human-3">Next Steps</p>
221+
<p className="text-xs text-primary/80">
222+
Navigate through the game to review specific positions. Use the
223+
&quot;Learn from Mistakes&quot; feature to practice improving
224+
the identified errors.
225+
</p>
226+
</div>
227+
</div>
228+
</div>
229+
230+
<div className="flex justify-end gap-2 pt-2">
231+
<button
232+
onClick={onClose}
233+
className="flex h-9 items-center gap-1 rounded bg-human-4 px-4 text-sm font-medium text-white transition duration-200 hover:bg-human-4/90"
234+
>
235+
<span className="material-symbols-outlined text-sm">check</span>
236+
Got it
237+
</button>
238+
</div>
239+
</motion.div>
240+
</motion.div>
241+
)
242+
}

src/components/Analysis/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export * from './AnalysisOverlay'
1313
export * from './InteractiveDescription'
1414
export * from './AnalysisSidebar'
1515
export * from './LearnFromMistakes'
16+
export * from './AnalysisSummaryModal'

src/pages/analysis/[...id].tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { ConfigurableScreens } from 'src/components/Analysis/ConfigurableScreens
3939
import { AnalysisConfigModal } from 'src/components/Analysis/AnalysisConfigModal'
4040
import { AnalysisNotification } from 'src/components/Analysis/AnalysisNotification'
4141
import { AnalysisOverlay } from 'src/components/Analysis/AnalysisOverlay'
42+
import { AnalysisSummaryModal } from 'src/components/Analysis/AnalysisSummaryModal'
4243
import { LearnFromMistakes } from 'src/components/Analysis/LearnFromMistakes'
4344
import { GameBoard } from 'src/components/Board/GameBoard'
4445
import { MovesContainer } from 'src/components/Board/MovesContainer'
@@ -378,6 +379,8 @@ const Analysis: React.FC<Props> = ({
378379
>(null)
379380
const [showCustomModal, setShowCustomModal] = useState(false)
380381
const [showAnalysisConfigModal, setShowAnalysisConfigModal] = useState(false)
382+
const [showAnalysisSummaryModal, setShowAnalysisSummaryModal] =
383+
useState(false)
381384
const [refreshTrigger, setRefreshTrigger] = useState(0)
382385
const [analysisEnabled, setAnalysisEnabled] = useState(true) // Analysis enabled by default
383386
const [lastMoveResult, setLastMoveResult] = useState<
@@ -403,6 +406,19 @@ const Analysis: React.FC<Props> = ({
403406
setHoverArrow(null)
404407
}, [controller.currentNode])
405408

409+
// Show analysis summary modal when analysis completes
410+
useEffect(() => {
411+
if (
412+
controller.gameAnalysis.progress.isComplete &&
413+
!controller.gameAnalysis.progress.isCancelled
414+
) {
415+
setShowAnalysisSummaryModal(true)
416+
}
417+
}, [
418+
controller.gameAnalysis.progress.isComplete,
419+
controller.gameAnalysis.progress.isCancelled,
420+
])
421+
406422
const launchContinue = useCallback(() => {
407423
const fen = controller.currentNode?.fen as string
408424
const url = '/play' + '?fen=' + encodeURIComponent(fen)
@@ -1434,6 +1450,15 @@ const Analysis: React.FC<Props> = ({
14341450
/>
14351451
)}
14361452
</AnimatePresence>
1453+
<AnimatePresence>
1454+
{showAnalysisSummaryModal && (
1455+
<AnalysisSummaryModal
1456+
isOpen={showAnalysisSummaryModal}
1457+
onClose={() => setShowAnalysisSummaryModal(false)}
1458+
game={analyzedGame}
1459+
/>
1460+
)}
1461+
</AnimatePresence>
14371462
<AnimatePresence>
14381463
{controller.gameAnalysis.progress.isAnalyzing && (
14391464
<>

0 commit comments

Comments
 (0)