1
1
import React , { useMemo , useEffect } from 'react'
2
2
import { motion } from 'framer-motion'
3
+ import { LineChart , Line , XAxis , YAxis , ResponsiveContainer , ReferenceLine } from 'recharts'
3
4
import { AnalyzedGame } from 'src/types'
4
5
import { extractPlayerMistakes } from 'src/lib/analysis'
5
6
@@ -10,7 +11,6 @@ interface Props {
10
11
}
11
12
12
13
interface GameSummary {
13
- totalMoves : number
14
14
whiteMistakes : {
15
15
total : number
16
16
blunders : number
@@ -21,8 +21,11 @@ interface GameSummary {
21
21
blunders : number
22
22
inaccuracies : number
23
23
}
24
- averageDepth : number
25
- positionsAnalyzed : number
24
+ evaluationData : Array < {
25
+ ply : number
26
+ evaluation : number
27
+ moveNumber : number
28
+ } >
26
29
}
27
30
28
31
export const AnalysisSummaryModal : React . FC < Props > = ( {
@@ -47,87 +50,79 @@ export const AnalysisSummaryModal: React.FC<Props> = ({
47
50
const whiteMistakes = extractPlayerMistakes ( game . tree , 'white' )
48
51
const blackMistakes = extractPlayerMistakes ( game . tree , 'black' )
49
52
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 ++
53
+ // Generate evaluation data for the chart
54
+ const evaluationData = mainLine . slice ( 1 ) . map ( ( node , index ) => {
55
+ const evaluation = node . analysis . stockfish ?. model_optimal_cp || 0
56
+ // Convert centipawns to evaluation (cap at +/- 5 for chart readability)
57
+ const normalizedEval = Math . max ( - 5 , Math . min ( 5 , evaluation / 100 ) )
58
+
59
+ return {
60
+ ply : index + 1 ,
61
+ evaluation : normalizedEval ,
62
+ moveNumber : Math . ceil ( ( index + 1 ) / 2 ) ,
58
63
}
59
64
} )
60
65
61
- const averageDepth =
62
- positionsAnalyzed > 0 ? Math . round ( totalDepth / positionsAnalyzed ) : 0
63
-
64
66
return {
65
- totalMoves : Math . ceil ( ( mainLine . length - 1 ) / 2 ) , // Convert plies to moves
66
67
whiteMistakes : {
67
68
total : whiteMistakes . length ,
68
69
blunders : whiteMistakes . filter ( ( m ) => m . type === 'blunder' ) . length ,
69
- inaccuracies : whiteMistakes . filter ( ( m ) => m . type === 'inaccuracy' )
70
- . length ,
70
+ inaccuracies : whiteMistakes . filter ( ( m ) => m . type === 'inaccuracy' ) . length ,
71
71
} ,
72
72
blackMistakes : {
73
73
total : blackMistakes . length ,
74
74
blunders : blackMistakes . filter ( ( m ) => m . type === 'blunder' ) . length ,
75
- inaccuracies : blackMistakes . filter ( ( m ) => m . type === 'inaccuracy' )
76
- . length ,
75
+ inaccuracies : blackMistakes . filter ( ( m ) => m . type === 'inaccuracy' ) . length ,
77
76
} ,
78
- averageDepth,
79
- positionsAnalyzed,
77
+ evaluationData,
80
78
}
81
79
} , [ game . tree ] )
82
80
83
81
if ( ! isOpen ) return null
84
82
85
- const MistakeSection = ( {
86
- title,
87
- color,
88
- mistakes,
89
- playerName,
90
- } : {
83
+ const formatYAxisLabel = ( value : number ) => {
84
+ if ( value === 0 ) return '0.00'
85
+ return value > 0 ? `+${ value . toFixed ( 1 ) } ` : `${ value . toFixed ( 1 ) } `
86
+ }
87
+
88
+ const PlayerPerformanceRow = ( {
89
+ title,
90
+ color,
91
+ mistakes,
92
+ playerName
93
+ } : {
91
94
title : string
92
95
color : string
93
96
mistakes : { total : number ; blunders : number ; inaccuracies : number }
94
- playerName : string
97
+ playerName : string
95
98
} ) => (
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 >
99
+ < div className = "flex items-center justify-between rounded border border-white/5 bg-background-2/30 p-3 " >
100
+ < div className = "flex items-center gap-3 " >
101
+ < div className = { `h-4 w-4 rounded-full ${ color === 'white' ? 'bg-white' : 'border border-white bg-black' } ` } />
102
+ < div >
103
+ < p className = "text-sm font-medium text-primary" > { playerName } </ p >
104
+ < p className = "text-xs text-secondary " > { title } </ p >
105
+ </ div >
103
106
</ div >
104
-
107
+
105
108
{ 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 >
109
+ < div className = "flex items-center gap-2" >
110
+ < span className = "material-symbols-outlined !text-sm text-green-400" > check_circle</ span >
111
+ < span className = "text-xs text-green-400" > Clean game</ span >
111
112
</ div >
112
113
) : (
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 >
114
+ < div className = "flex items-center gap-3 text-xs" >
115
+ < div className = "flex items-center gap-1" >
116
+ < span className = "font-semibold text-red-400" > { mistakes . blunders } </ span >
117
+ < span className = "text-secondary" > blunders</ span >
119
118
</ 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 >
119
+ < div className = "flex items-center gap-1" >
120
+ < span className = "font-semibold text-orange-400" > { mistakes . inaccuracies } </ span >
121
+ < span className = "text-secondary" > inaccuracies</ span >
125
122
</ 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 >
123
+ < div className = "flex items-center gap-1" >
124
+ < span className = "font-semibold text-primary" > { mistakes . total } </ span >
125
+ < span className = "text-secondary" > total</ span >
131
126
</ div >
132
127
</ div >
133
128
) }
@@ -136,7 +131,7 @@ export const AnalysisSummaryModal: React.FC<Props> = ({
136
131
137
132
return (
138
133
< 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"
134
+ className = "fixed inset -0 z-20 flex items-center justify-center bg-black/70 px-4 backdrop-blur-sm md:px-0"
140
135
initial = { { opacity : 0 } }
141
136
animate = { { opacity : 1 } }
142
137
exit = { { opacity : 0 } }
@@ -146,97 +141,104 @@ export const AnalysisSummaryModal: React.FC<Props> = ({
146
141
} }
147
142
>
148
143
< 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 "
144
+ className = "flex w-full max-w-4xl flex-col gap-5 rounded-lg border border-white/10 bg-background-1 p-6 shadow-2xl "
150
145
initial = { { y : 20 , opacity : 0 } }
151
146
animate = { { y : 0 , opacity : 1 } }
152
147
exit = { { y : 20 , opacity : 0 } }
153
148
transition = { { duration : 0.3 } }
154
149
onClick = { ( e ) => e . stopPropagation ( ) }
155
150
>
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 >
151
+ { /* Header */ }
152
+ < div className = "flex items-center justify-between border-b border-white/10 pb-4" >
153
+ < div className = "flex items-center gap-3" >
154
+ < span className = "material-symbols-outlined text-2xl text-human-4" >
155
+ analytics
156
+ </ span >
157
+ < h2 className = "text-xl font-bold text-primary" > Analysis Complete</ h2 >
191
158
</ div >
159
+ < button
160
+ onClick = { onClose }
161
+ className = "text-secondary transition-colors hover:text-primary"
162
+ >
163
+ < span className = "material-symbols-outlined" > close</ span >
164
+ </ button >
165
+ </ div >
192
166
167
+ < div className = "flex flex-col gap-5" >
193
168
{ /* Player Performance */ }
194
169
< 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
- />
170
+ < h3 className = "text-sm font-semibold text-primary/90" > Player Performance</ h3 >
171
+ < div className = "flex flex-col gap-2" >
172
+ < PlayerPerformanceRow
173
+ title = "White"
174
+ color = "white"
175
+ mistakes = { summary . whiteMistakes }
176
+ playerName = { game . whitePlayer . name }
177
+ />
178
+ < PlayerPerformanceRow
179
+ title = "Black"
180
+ color = "black"
181
+ mistakes = { summary . blackMistakes }
182
+ playerName = { game . blackPlayer . name }
183
+ />
184
+ </ div >
212
185
</ div >
213
186
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
- "Learn from Mistakes" feature to practice improving
224
- the identified errors.
225
- </ p >
187
+ { /* Evaluation Chart */ }
188
+ < div className = "flex flex-col gap-3" >
189
+ < h3 className = "text-sm font-semibold text-primary/90" > Game Evaluation</ h3 >
190
+ < div className = "rounded border border-white/10 bg-background-2/20 p-4" >
191
+ < div className = "h-48" >
192
+ < ResponsiveContainer width = "100%" height = "100%" >
193
+ < LineChart data = { summary . evaluationData } margin = { { top : 5 , right : 5 , left : 5 , bottom : 5 } } >
194
+ < XAxis
195
+ dataKey = "moveNumber"
196
+ tick = { { fontSize : 12 , fill : '#94a3b8' } }
197
+ tickLine = { { stroke : '#334155' } }
198
+ axisLine = { { stroke : '#334155' } }
199
+ />
200
+ < YAxis
201
+ domain = { [ - 5 , 5 ] }
202
+ tickFormatter = { formatYAxisLabel }
203
+ tick = { { fontSize : 12 , fill : '#94a3b8' } }
204
+ tickLine = { { stroke : '#334155' } }
205
+ axisLine = { { stroke : '#334155' } }
206
+ />
207
+ < ReferenceLine y = { 0 } stroke = "#475569" strokeDasharray = "2 2" />
208
+ < Line
209
+ type = "monotone"
210
+ dataKey = "evaluation"
211
+ stroke = "#3b82f6"
212
+ strokeWidth = { 2 }
213
+ dot = { false }
214
+ activeDot = { { r : 4 , fill : '#3b82f6' } }
215
+ />
216
+ </ LineChart >
217
+ </ ResponsiveContainer >
218
+ </ div >
219
+ < div className = "mt-2 flex items-center justify-center gap-4 text-xs text-secondary" >
220
+ < div className = "flex items-center gap-1" >
221
+ < div className = "h-2 w-2 rounded-full bg-blue-500" > </ div >
222
+ < span > Stockfish Evaluation</ span >
223
+ </ div >
224
+ < span > •</ span >
225
+ < span > Positive values favor White</ span >
226
+ </ div >
226
227
</ div >
227
228
</ div >
228
229
</ div >
229
230
230
- < div className = "flex justify-end gap-2 pt-2" >
231
+ { /* Footer */ }
232
+ < div className = "flex justify-end border-t border-white/10 pt-4" >
231
233
< button
232
234
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"
235
+ className = "flex items-center gap-2 rounded bg-human-4 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-human-4/90"
234
236
>
235
- < span className = "material-symbols-outlined text-sm" > check</ span >
236
- Got it
237
+ < span className = "material-symbols-outlined ! text-sm" > check</ span >
238
+ Continue
237
239
</ button >
238
240
</ div >
239
241
</ motion . div >
240
242
</ motion . div >
241
243
)
242
- }
244
+ }
0 commit comments