"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Chess } from "chess.js"; import { Chessboard } from "react-chessboard"; import { Brain, ChevronLeft, ChevronRight, Loader2, ArrowLeft, Download } from "lucide-react"; import { useRouter } from "next/navigation"; import Header from "@/components/Header"; import { SupportedLanguage } from "@/lib/i18n/translations"; import { useTranslation } from "@/lib/i18n/useTranslation"; import { Personality, PERSONALITIES } from "@/lib/personalities"; import { Stockfish, StockfishEvaluation } from "@/lib/stockfish"; import { detectChessFormat, ChessFormat } from "@/lib/chessFormatDetector"; import { detectMissedTactics, DetectedTactic, uciToSan } from "@/lib/tacticDetection"; import { lookupPossibleOpenings, buildMoveSequenceFromSteps, OpeningMetadata } from "@/lib/openings"; import { getGenAIModel } from "@/lib/gemini"; import { ChatSession } from "@google/generative-ai"; import ReactMarkdown from "react-markdown"; import { useDebug } from "@/contexts/DebugContext"; import { GameImportModal } from "@/components/GameImportModal"; import { EvaluationBar } from "@/components/EvaluationBar"; interface MoveStep { san: string; color: "white" | "black"; moveNumber: number; fenBefore: string; fenAfter: string; } interface StepDetails { evalBefore?: StockfishEvaluation; evalAfter?: StockfishEvaluation; cpLoss?: number; missedTactics?: DetectedTactic[]; bestMoveSan?: string | null; comment?: string; } const DEFAULT_START = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; export default function AnalysisPage() { const router = useRouter(); const [language, setLanguage] = useState("en"); const [apiKey, setApiKey] = useState(null); const t = useTranslation(language); const { addEntry } = useDebug(); const [input, setInput] = useState(""); const [detectedFormat, setDetectedFormat] = useState(null); const [selectedPersonality, setSelectedPersonality] = useState(PERSONALITIES[0]); const [orientation, setOrientation] = useState<"white" | "black">("white"); const [initialFen, setInitialFen] = useState(DEFAULT_START); const [steps, setSteps] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); // 0 = starting position const [error, setError] = useState(null); const [stockfish, setStockfish] = useState(null); const evaluationCache = useRef>({}); const [evaluationVersion, setEvaluationVersion] = useState(0); const [stepDetails, setStepDetails] = useState>({}); const [isCommenting, setIsCommenting] = useState(false); const [comments, setComments] = useState>({}); const [chatSession, setChatSession] = useState(null); const [showImportModal, setShowImportModal] = useState(false); useEffect(() => { const storedKey = localStorage.getItem("gemini_api_key"); const storedLang = localStorage.getItem("chess_tutor_language"); if (storedKey) setApiKey(storedKey); if (storedLang) setLanguage(storedLang as SupportedLanguage); }, []); useEffect(() => { const sf = new Stockfish(); setStockfish(sf); return () => sf.terminate(); }, []); // Initialize chat session for conversational analysis useEffect(() => { if (apiKey) { const model = getGenAIModel(apiKey, "gemini-2.5-flash"); const session = model.startChat({ history: [ { role: "user", parts: [{ text: `You are ${selectedPersonality.name}. You will analyze a chess game move by move. Stay in character and maintain your personality throughout the analysis. Language: ${language.toUpperCase()}. IMPORTANT: - You are analyzing moves sequentially - Each move you analyze builds on the previous context - If the user navigates backwards or forwards, you will see the move number - Provide educational commentary in your characteristic style - Be concise (3-4 sentences per move) - Focus on what the move accomplishes and what was missed` }] }, { role: "model", parts: [{ text: `Understood. I am ${selectedPersonality.name}, and I will analyze this game move by move in ${language}, maintaining my personality while providing educational insights. I'll keep track of the game's progression and provide context-aware commentary.` }] } ] }); setChatSession(session); } }, [apiKey, selectedPersonality, language]); const currentFen = useMemo(() => { if (currentIndex === 0) return initialFen; return steps[currentIndex - 1]?.fenAfter || initialFen; }, [currentIndex, steps, initialFen]); const possibleOpenings = useMemo(() => { if (currentIndex === 0) return []; const moveSequence = buildMoveSequenceFromSteps(steps, currentIndex); return lookupPossibleOpenings(moveSequence, 5); }, [currentIndex, steps]); const handleInputChange = (value: string) => { setInput(value); setDetectedFormat(value.trim() ? detectChessFormat(value) : null); }; const ensureEvaluation = useCallback(async (fen: string) => { if (!stockfish) return null; if (evaluationCache.current[fen]) return evaluationCache.current[fen]; const result = await stockfish.evaluate(fen, 14); evaluationCache.current[fen] = result; setEvaluationVersion(v => v + 1); return result; }, [stockfish]); const handleLoadGame = () => { const trimmed = input.trim(); const format = detectChessFormat(trimmed); if (!trimmed || format === "invalid") { setError(t.analysis.importError); return; } loadGameFromPgnOrFen(trimmed); }; const loadGameFromPgnOrFen = (notation: string) => { const trimmed = notation.trim(); const format = detectChessFormat(trimmed); if (!trimmed || format === "invalid") { setError(t.analysis.importError); return; } try { const parsedGame = new Chess(); const nextSteps: MoveStep[] = []; let startFen = DEFAULT_START; if (format === "fen") { parsedGame.load(trimmed); startFen = parsedGame.fen(); } else { parsedGame.loadPgn(trimmed); const headers = parsedGame.header(); if (headers.FEN) { const base = new Chess(); base.load(headers.FEN); startFen = base.fen(); } else { parsedGame.reset(); startFen = parsedGame.fen(); } const replay = new Chess(); replay.load(startFen); const history = new Chess(); history.loadPgn(trimmed); history.history({ verbose: true }).forEach((move, idx) => { const before = replay.fen(); const applied = replay.move({ from: move.from, to: move.to, promotion: move.promotion || "q" }); if (applied) { nextSteps.push({ san: applied.san, color: applied.color === "w" ? "white" : "black", moveNumber: Math.floor(idx / 2) + 1, fenBefore: before, fenAfter: replay.fen(), }); } }); } evaluationCache.current = {}; setEvaluationVersion(v => v + 1); setInitialFen(startFen); setSteps(nextSteps); setCurrentIndex(0); setStepDetails({}); setComments({}); setError(null); ensureEvaluation(startFen); } catch (e) { console.error("Failed to load game", e); setError(t.analysis.importError); } }; const handleImportGame = (pgn: string) => { setInput(pgn); setDetectedFormat(detectChessFormat(pgn)); loadGameFromPgnOrFen(pgn); }; useEffect(() => { if (!stockfish || !currentFen) return; ensureEvaluation(currentFen); const currentStep = steps[currentIndex - 1]; if (currentStep) { ensureEvaluation(currentStep.fenBefore); } }, [stockfish, currentFen, steps, currentIndex, ensureEvaluation]); useEffect(() => { if (currentIndex === 0) return; const step = steps[currentIndex - 1]; if (!step) return; const evalBefore = evaluationCache.current[step.fenBefore]; const evalAfter = evaluationCache.current[step.fenAfter]; if (!evalBefore || !evalAfter) return; setStepDetails(prev => { if (prev[currentIndex]?.evalBefore && prev[currentIndex]?.evalAfter) return prev; const cpLoss = step.color === "white" ? evalBefore.score - evalAfter.score : evalAfter.score - evalBefore.score; const missedTactics = detectMissedTactics({ fen: step.fenBefore, playerColor: step.color, playerMoveSan: step.san, bestMoveUci: evalBefore.bestMove, cpLoss, }); const bestMoveSan = uciToSan(step.fenBefore, evalBefore.bestMove); return { ...prev, [currentIndex]: { evalBefore, evalAfter, cpLoss, missedTactics, bestMoveSan, } }; }); }, [currentIndex, steps, evaluationVersion]); useEffect(() => { if (!chatSession) return; if (currentIndex === 0) return; const step = steps[currentIndex - 1]; const details = stepDetails[currentIndex]; if (!step || !details?.evalBefore || !details?.evalAfter) return; if (comments[currentIndex]) return; let cancelled = false; setIsCommenting(true); const timeout = setTimeout(async () => { try { const delta = details.cpLoss ?? 0; const evalBefore = details.evalBefore!.score / 100; const evalAfter = details.evalAfter!.score / 100; const mateInfo = details.evalAfter!.mate !== null ? `Mate in ${details.evalAfter!.mate}` : "No mate detected"; const tactics = (details.missedTactics || []) .filter(t => t.tactic_type !== "none") .map(t => `${t.tactic_type}${t.material_delta ? ` (~${(t.material_delta / 100).toFixed(1)} pawns)` : ""}`) .join("; ") || "None"; const prompt = ` Analyze this move: DATA: - Move number: ${step.moveNumber} - Side to move: ${step.color} - Move played (SAN): ${step.san} - FEN before move: ${step.fenBefore} - FEN after move: ${step.fenAfter} - Evaluation before move: ${evalBefore.toFixed(2)} pawns - Evaluation after move: ${evalAfter.toFixed(2)} pawns - Best move suggestion: ${details.bestMoveSan ?? details.evalBefore!.bestMove} - Evaluation shift (centipawns): ${delta} - Possible Openings: ${possibleOpenings.length > 0 ? possibleOpenings.map(o => `${o.name} (${o.eco})`).join(', ') : "Unknown/Midgame"} - Missed tactics: ${tactics} - Mate hint: ${mateInfo} INSTRUCTIONS: - Be concise (3-4 sentences). - Mention whether the move improved or worsened the position and why. - Highlight any tactical ideas the player may have missed. - Refer to the player's side as ${step.color}. - Keep it educational and stay true to your personality tone.`; const result = await chatSession.sendMessage(prompt); const responseText = result.response.text(); if (!cancelled) { setComments(prev => ({ ...prev, [currentIndex]: responseText })); // Track debug entry addEntry({ type: 'analysis', action: `Move ${step.moveNumber} Analysis (${step.color})`, prompt, response: responseText, metadata: { moveNumber: step.moveNumber, san: step.san, color: step.color, fenBefore: step.fenBefore, fenAfter: step.fenAfter, cpLoss: delta, personality: selectedPersonality.name, language, } }); } } catch (err) { console.error("Commentary failed", err); } finally { if (!cancelled) setIsCommenting(false); } }, 400); return () => { cancelled = true; clearTimeout(timeout); setIsCommenting(false); }; }, [chatSession, currentIndex, stepDetails, steps, comments, possibleOpenings]); const formatEval = (evaluation?: StockfishEvaluation) => { if (!evaluation) return t.analysis.enginePending; if (evaluation.mate !== null) return `#${evaluation.mate}`; return `${evaluation.score >= 0 ? "+" : ""}${(evaluation.score / 100).toFixed(2)}`; }; const formatCpLoss = (cp?: number) => { if (cp === undefined) return t.analysis.enginePending; const pawns = (cp / 100).toFixed(2); return `${cp > 0 ? "+" : ""}${pawns}`; }; const currentDetails = currentIndex > 0 ? stepDetails[currentIndex] : undefined; const tacticSummary = (currentDetails?.missedTactics || []).filter(t => t.tactic_type !== "none"); return (
{/* Back Button */}

{t.analysis.modeTitle}

{t.analysis.modeDescription}