diff --git a/src/components/Analysis/AnalysisSummaryModal.tsx b/src/components/Analysis/AnalysisSummaryModal.tsx
new file mode 100644
index 00000000..72fb2b5c
--- /dev/null
+++ b/src/components/Analysis/AnalysisSummaryModal.tsx
@@ -0,0 +1,504 @@
+import React, { useMemo, useEffect } from 'react'
+import { motion } from 'framer-motion'
+import {
+ ComposedChart,
+ Line,
+ Area,
+ XAxis,
+ YAxis,
+ ResponsiveContainer,
+ ReferenceLine,
+ CartesianGrid,
+ Tooltip,
+ Dot
+} from 'recharts'
+import { AnalyzedGame } from 'src/types'
+import { extractPlayerMistakes } from 'src/lib/analysis'
+
+interface Props {
+ isOpen: boolean
+ onClose: () => void
+ game: AnalyzedGame
+}
+
+interface GameSummary {
+ whiteMistakes: {
+ total: number
+ blunders: number
+ inaccuracies: number
+ }
+ blackMistakes: {
+ total: number
+ blunders: number
+ inaccuracies: number
+ }
+ evaluationData: Array<{
+ moveNumber: number | string
+ evaluation: number
+ whiteAdvantage: number
+ blackAdvantage: number
+ san?: string
+ isBlunder: boolean
+ isInaccuracy: boolean
+ isWhiteMove: boolean
+ }>
+ criticalMoments: Array<{
+ moveNumber: number
+ san: string
+ playerColor: 'white' | 'black'
+ type: 'blunder' | 'inaccuracy' | 'excellent'
+ evaluation: number
+ }>
+ gameInsights: {
+ totalMoves: number
+ turningPoints: number
+ maxAdvantage: { player: 'white' | 'black'; value: number }
+ gamePhase: 'opening' | 'middlegame' | 'endgame'
+ }
+}
+
+// Custom dot component for move quality indicators
+const CustomDot: React.FC<{
+ cx?: number
+ cy?: number
+ payload?: {
+ isBlunder: boolean
+ isInaccuracy: boolean
+ isWhiteMove: boolean
+ }
+}> = ({ cx, cy, payload }) => {
+ if (!payload || (!payload.isBlunder && !payload.isInaccuracy)) return null
+
+ const color = payload.isBlunder ? '#ef4444' : '#eab308' // Red for blunders, yellow for inaccuracies
+ const radius = payload.isBlunder ? 5 : 4
+
+ return (
+
+ )
+}
+
+// Custom tooltip component
+const CustomTooltip: React.FC<{
+ active?: boolean
+ payload?: Array<{
+ payload: {
+ san?: string
+ evaluation: number
+ moveNumber: number | string
+ isBlunder: boolean
+ isInaccuracy: boolean
+ isWhiteMove: boolean
+ }
+ }>
+}> = ({ active, payload }) => {
+ if (!active || !payload || !payload[0]) return null
+
+ const data = payload[0].payload
+ const formatEvaluation = (evaluation: number) => {
+ if (Math.abs(evaluation) >= 10) {
+ return evaluation > 0 ? '+M' : '-M'
+ }
+ return evaluation > 0 ? `+${evaluation.toFixed(1)}` : `${evaluation.toFixed(1)}`
+ }
+
+ const moveType = data.isBlunder ? 'Blunder' : data.isInaccuracy ? 'Inaccuracy' : null
+
+ return (
+
+
+ {typeof data.moveNumber === 'string' ? data.moveNumber : `${data.moveNumber}.`} {data.san}
+
+
+ Evaluation: {formatEvaluation(data.evaluation)}
+
+ {moveType && (
+
+ {moveType}
+
+ )}
+
+ )
+}
+
+export const AnalysisSummaryModal: React.FC = ({
+ isOpen,
+ onClose,
+ game,
+}) => {
+ useEffect(() => {
+ if (isOpen) {
+ document.body.style.overflow = 'hidden'
+ } else {
+ document.body.style.overflow = 'unset'
+ }
+
+ return () => {
+ document.body.style.overflow = 'unset'
+ }
+ }, [isOpen])
+
+ const summary = useMemo((): GameSummary => {
+ const mainLine = game.tree.getMainLine()
+ const whiteMistakes = extractPlayerMistakes(game.tree, 'white')
+ const blackMistakes = extractPlayerMistakes(game.tree, 'black')
+
+ // Generate evaluation data for the chart
+ const evaluationData = mainLine.slice(1).map((node, index) => {
+ const evaluation = (node.analysis.stockfish?.model_optimal_cp || 0) / 100
+ // Cap evaluation for chart readability but store original for tooltips
+ const clampedEval = Math.max(-10, Math.min(10, evaluation))
+ const isWhiteMove = index % 2 === 0
+
+ return {
+ moveNumber: isWhiteMove ? Math.ceil((index + 1) / 2) : `${Math.ceil((index + 1) / 2)}...`,
+ evaluation: clampedEval,
+ whiteAdvantage: clampedEval > 0 ? clampedEval : 0,
+ blackAdvantage: clampedEval < 0 ? clampedEval : 0,
+ san: node.san || '',
+ isBlunder: node.blunder || false,
+ isInaccuracy: node.inaccuracy || false,
+ isWhiteMove,
+ }
+ })
+
+ // Extract critical moments
+ const criticalMoments = mainLine
+ .slice(1)
+ .filter(node => node.blunder || node.inaccuracy || node.excellentMove)
+ .map((node, index) => ({
+ moveNumber: node.moveNumber || Math.ceil((index + 1) / 2),
+ san: node.san || '',
+ playerColor: (node.moveNumber || 1) % 2 === 1 ? 'white' : 'black' as 'white' | 'black',
+ type: node.blunder ? 'blunder' : node.inaccuracy ? 'inaccuracy' : 'excellent' as 'blunder' | 'inaccuracy' | 'excellent',
+ evaluation: (node.analysis.stockfish?.model_optimal_cp || 0) / 100,
+ }))
+ .slice(0, 5)
+
+ // Calculate game insights
+ const evaluations = evaluationData.map(d => d.evaluation)
+ const maxEval = Math.max(...evaluations)
+ const minEval = Math.min(...evaluations)
+ const maxAdvantageValue = Math.max(Math.abs(maxEval), Math.abs(minEval))
+ const maxAdvantagePlayer = Math.abs(maxEval) > Math.abs(minEval) ? 'white' : 'black'
+
+ // Count significant evaluation swings (turning points)
+ let turningPoints = 0
+ for (let i = 1; i < evaluations.length - 1; i++) {
+ const prev = evaluations[i - 1]
+ const curr = evaluations[i]
+ const next = evaluations[i + 1]
+ if ((prev > 1 && curr < -1) || (prev < -1 && curr > 1) ||
+ (Math.abs(curr - prev) > 2 && Math.abs(next - curr) > 2)) {
+ turningPoints++
+ }
+ }
+
+ const gameInsights = {
+ totalMoves: Math.ceil((mainLine.length - 1) / 2),
+ turningPoints,
+ maxAdvantage: { player: maxAdvantagePlayer, value: maxAdvantageValue },
+ gamePhase: mainLine.length > 40 ? 'endgame' : mainLine.length > 20 ? 'middlegame' : 'opening' as 'opening' | 'middlegame' | 'endgame',
+ }
+
+ return {
+ whiteMistakes: {
+ total: whiteMistakes.length,
+ blunders: whiteMistakes.filter((m) => m.type === 'blunder').length,
+ inaccuracies: whiteMistakes.filter((m) => m.type === 'inaccuracy').length,
+ },
+ blackMistakes: {
+ total: blackMistakes.length,
+ blunders: blackMistakes.filter((m) => m.type === 'blunder').length,
+ inaccuracies: blackMistakes.filter((m) => m.type === 'inaccuracy').length,
+ },
+ evaluationData,
+ criticalMoments,
+ gameInsights,
+ }
+ }, [game.tree])
+
+ if (!isOpen) return null
+
+ const formatEvaluation = (evaluation: number) => {
+ if (Math.abs(evaluation) >= 10) {
+ return evaluation > 0 ? '+M' : '-M'
+ }
+ return evaluation > 0 ? `+${evaluation.toFixed(1)}` : `${evaluation.toFixed(1)}`
+ }
+
+ const PlayerPerformanceRow = ({
+ title,
+ color,
+ mistakes,
+ playerName
+ }: {
+ title: string
+ color: string
+ mistakes: { total: number; blunders: number; inaccuracies: number }
+ playerName: string
+ }) => (
+
+
+
+
+
{playerName}
+
{title}
+
+
+
+ {mistakes.total === 0 ? (
+
+ check_circle
+ Clean game
+
+ ) : (
+
+
+ {mistakes.blunders}
+ blunders
+
+
+ {mistakes.inaccuracies}
+ inaccuracies
+
+
+ {mistakes.total}
+ total
+
+
+ )}
+
+ )
+
+ return (
+ {
+ if (e.target === e.currentTarget) onClose()
+ }}
+ >
+ e.stopPropagation()}
+ >
+ {/* Header */}
+
+
+
+ analytics
+
+
+
Analysis Complete
+
+ {summary.gameInsights.totalMoves} moves • {summary.gameInsights.gamePhase} phase
+
+
+
+
+
+
+ {/* Content Grid */}
+
+ {/* Left Column - Player Performance & Insights */}
+
+ {/* Player Performance */}
+
+
+ {/* Game Insights */}
+
+
Game Insights
+
+
+
Turning Points
+
{summary.gameInsights.turningPoints}
+
+
+
Max Advantage
+
+ {formatEvaluation(summary.gameInsights.maxAdvantage.value)}
+
+
+ {summary.gameInsights.maxAdvantage.player}
+
+
+
+
Game Length
+
{summary.gameInsights.totalMoves}
+
moves
+
+
+
Phase
+
+ {summary.gameInsights.gamePhase}
+
+
+
+
+
+ {/* Critical Moments */}
+ {summary.criticalMoments.length > 0 && (
+
+
Critical Moments
+
+ {summary.criticalMoments.slice(0, 3).map((moment, index) => (
+
+
+
+
+ {moment.moveNumber}. {moment.san}
+
+
+ {moment.type} • {formatEvaluation(moment.evaluation)}
+
+
+
+ ))}
+
+
+ )}
+
+
+ {/* Right Columns - Evaluation Chart */}
+
+
+
Position Evaluation
+
+
+
+
+
+
+
+
+
+
+ } />
+
+ {/* White advantage area */}
+
+
+ {/* Black advantage area */}
+
+
+
+
+ }
+ activeDot={false}
+ />
+
+
+
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/Analysis/MoveMap.tsx b/src/components/Analysis/MoveMap.tsx
index 27b23b83..70ed1ab9 100644
--- a/src/components/Analysis/MoveMap.tsx
+++ b/src/components/Analysis/MoveMap.tsx
@@ -330,7 +330,7 @@ export const MoveMap: React.FC = ({
tickMargin={0}
tickLine={false}
tickFormatter={(value) => `${value}%`}
- domain={([dataMin, dataMax]) => [0, dataMax > 60 ? 100 : 60]}
+ domain={([dataMin, dataMax]) => [0, dataMax > 40 ? 100 : 40]}
>