import React, { useState, useEffect, useRef } from 'react'; import { Chess } from 'chess.js'; import { PenTool, Trash2, Check, RefreshCw, X, AlertCircle, Loader2, Copy, Type, List, Bot, Undo2, Lightbulb, Settings, GraduationCap } from 'lucide-react'; // --- Assets & Constants --- const PIECE_URLS = { 'w-p': 'https://upload.wikimedia.org/wikipedia/commons/4/45/Chess_plt45.svg', 'w-n': 'https://upload.wikimedia.org/wikipedia/commons/7/70/Chess_nlt45.svg', 'w-b': 'https://upload.wikimedia.org/wikipedia/commons/b/b1/Chess_blt45.svg', 'w-r': 'https://upload.wikimedia.org/wikipedia/commons/7/72/Chess_rlt45.svg', 'w-q': 'https://upload.wikimedia.org/wikipedia/commons/1/15/Chess_qlt45.svg', 'w-k': 'https://upload.wikimedia.org/wikipedia/commons/4/42/Chess_klt45.svg', 'b-p': 'https://upload.wikimedia.org/wikipedia/commons/c/c7/Chess_pdt45.svg', 'b-n': 'https://upload.wikimedia.org/wikipedia/commons/e/ef/Chess_ndt45.svg', 'b-b': 'https://upload.wikimedia.org/wikipedia/commons/9/98/Chess_bdt45.svg', 'b-r': 'https://upload.wikimedia.org/wikipedia/commons/f/ff/Chess_rdt45.svg', 'b-q': 'https://upload.wikimedia.org/wikipedia/commons/4/47/Chess_qdt45.svg', 'b-k': 'https://upload.wikimedia.org/wikipedia/commons/f/f0/Chess_kdt45.svg', }; const FILES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; const RANKS = ['8', '7', '6', '5', '4', '3', '2', '1']; const ENGINE_FENS = { 1: '4k3/8/8/8/8/8/PPPPPPPP/RNBQKBNR w KQ - 0 1', 2: '8/8/8/8/4k3/8/8/R3K2R w KQ - 0 1', 3: '8/8/8/8/4k3/8/8/Q3K3 w - - 0 1' }; // --- Audio Assets & Sound Engine --- const AUDIO_ASSETS = { move: new Audio('https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/move-self.mp3'), capture: new Audio('https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/capture.mp3'), check: new Audio('https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/move-check.mp3'), castle: new Audio('https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/castle.mp3'), start: new Audio('https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/game-start.mp3'), end: new Audio('https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/game-end.mp3'), }; const playSound = (moveObj, isGameOver = false) => { try { let soundType = 'move'; if (isGameOver) soundType = 'end'; else if (!moveObj) soundType = 'start'; else if (moveObj.san.includes('+') || moveObj.san.includes('#')) soundType = 'check'; else if (moveObj?.flags?.includes('c') || moveObj?.flags?.includes('e')) soundType = 'capture'; else if (moveObj?.flags?.includes('k') || moveObj?.flags?.includes('q')) soundType = 'castle'; const sound = AUDIO_ASSETS[soundType]; if (sound) { const clone = sound.cloneNode(true); clone.volume = 0.6; clone.play().catch(() => {}); } } catch (e) { console.error("Audio playback error:", e); } }; export default function App() { const [engineLevel, setEngineLevel] = useState(0); // 0: Off, 1: Level 1, 2: Level 2, 3: Level 3 const [game, setGame] = useState(new Chess()); const [fen, setFen] = useState(game.fen()); const [history, setHistory] = useState([]); // UI & Layout State const [isLearnMode, setIsLearnMode] = useState(false); const [showSettings, setShowSettings] = useState(false); const [showManualControls, setShowManualControls] = useState(false); const [showHistory, setShowHistory] = useState(false); const [processingType, setProcessingType] = useState(null); const [confirmModal, setConfirmModal] = useState({ isOpen: false, text: '', error: '' }); const [autoSubmit, setAutoSubmit] = useState(true); const [inlineError, setInlineError] = useState(''); const [copied, setCopied] = useState(false); const [showAmbiguityHelp, setShowAmbiguityHelp] = useState(false); const [hintMove, setHintMove] = useState(null); // Learn Mode specific state const [selectedSquare, setSelectedSquare] = useState(null); const [taughtMove, setTaughtMove] = useState(null); // { san: 'Nf3', color: 'w', trigger: timestamp } // Input Modes const [inputMode, setInputMode] = useState('draw'); // 'draw' | 'text' const [textInput, setTextInput] = useState(''); // Canvas & Logic Refs const canvasRef = useRef(null); const ctxRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); const drawTimeoutRef = useRef(null); const hasDrawnRef = useRef(false); const gameRef = useRef(game); const drawSequenceRef = useRef(0); const apiKey = ""; useEffect(() => { gameRef.current = game; }, [game]); // Handle global "N" hotkey for toggling modes useEffect(() => { const handleKeyDown = (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (e.key.toLowerCase() === 'n') { setInputMode(prev => prev === 'draw' ? 'text' : 'draw'); setInlineError(''); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, []); // Robust Canvas Initialization & Resizing useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; // Canvas always renders now, so it shouldn't be null const initCanvas = () => { const parent = canvas.parentElement; if (!parent) return; const rect = parent.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { canvas.width = rect.width * 2; canvas.height = rect.height * 2; const ctx = canvas.getContext('2d'); ctx.scale(2, 2); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = '#1e293b'; ctx.lineWidth = 8; ctxRef.current = ctx; ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, rect.width, rect.height); } }; const initialTimer = setTimeout(initCanvas, 50); const observer = new ResizeObserver(initCanvas); if (canvas.parentElement) observer.observe(canvas.parentElement); return () => { clearTimeout(initialTimer); observer.disconnect(); }; }, [inputMode]); // Auto-dismiss the learning animation useEffect(() => { let timer; if (taughtMove) { timer = setTimeout(() => { setTaughtMove(null); }, 3500); // Overlay stays for 3.5 seconds } return () => clearTimeout(timer); }, [taughtMove]); const isGameOver = typeof game.isGameOver === 'function' ? game.isGameOver() : game.game_over(); const isCheckmate = typeof game.isCheckmate === 'function' ? game.isCheckmate() : game.in_checkmate(); const isEngineThinking = engineLevel > 0 && game.turn() === 'b' && !isGameOver; // --- Engine Logic --- useEffect(() => { if (isEngineThinking) { const timer = setTimeout(() => { const moves = gameRef.current.moves({ verbose: true }); if (moves.length > 0) { let selectedMove; if (engineLevel === 3) { let bestScore = -Infinity; let bestMoves = []; moves.forEach(m => { let score = 0; const toFileIdx = m.to.charCodeAt(0) - 'a'.charCodeAt(0); const toRankIdx = parseInt(m.to[1]); const fromRankIdx = parseInt(m.from[1]); const fileDist = Math.abs(toFileIdx - 3.5); const rankDist = Math.abs(toRankIdx - 4.5); score -= (fileDist + rankDist); if (toRankIdx === fromRankIdx) score += 5; else if (toRankIdx > fromRankIdx) score -= 10; if (toRankIdx === 8) score -= 50; if (score > bestScore) { bestScore = score; bestMoves = [m]; } else if (score === bestScore) { bestMoves.push(m); } }); selectedMove = bestMoves[Math.floor(Math.random() * bestMoves.length)]; } else if (engineLevel === 2) { const sidewaysMoves = moves.filter(m => m.from[1] === m.to[1]); if (sidewaysMoves.length > 0) { selectedMove = sidewaysMoves[Math.floor(Math.random() * sidewaysMoves.length)]; } else { selectedMove = moves[Math.floor(Math.random() * moves.length)]; } } else { selectedMove = moves[Math.floor(Math.random() * moves.length)]; } const tempGame = new Chess(ENGINE_FENS[engineLevel]); const pastMoves = gameRef.current.history(); pastMoves.forEach(m => tempGame.move(m)); const moveObj = tempGame.move(selectedMove.san); setGame(tempGame); setFen(tempGame.fen()); setHistory(tempGame.history({ verbose: true })); if (isLearnMode) { setTaughtMove({ san: moveObj.san, color: moveObj.color, trigger: Date.now() }); } const isNowGameOver = typeof tempGame.isGameOver === 'function' ? tempGame.isGameOver() : tempGame.game_over(); playSound(moveObj, isNowGameOver); } }, 600); return () => clearTimeout(timer); } }, [fen, engineLevel, isEngineThinking, isLearnMode]); // --- Play on Board (Learn Mode) Logic --- const handleBoardMove = (fromSq, toSq) => { if (!isLearnMode || isEngineThinking || isGameOver) return false; const moves = game.moves({ verbose: true }); const move = moves.find(m => m.from === fromSq && m.to === toSq); if (move) { const tempGame = engineLevel > 0 ? new Chess(ENGINE_FENS[engineLevel]) : new Chess(); const pastMoves = game.history(); pastMoves.forEach(m => tempGame.move(m)); // Auto promote to Queen in learn mode const moveObj = tempGame.move({ from: fromSq, to: toSq, promotion: 'q' }); setGame(tempGame); setFen(tempGame.fen()); setHistory(tempGame.history({ verbose: true })); setSelectedSquare(null); setHintMove(null); // Trigger animation setTaughtMove({ san: moveObj.san, color: moveObj.color, trigger: Date.now() }); const isNowGameOver = typeof tempGame.isGameOver === 'function' ? tempGame.isGameOver() : tempGame.game_over(); playSound(moveObj, isNowGameOver); return true; } return false; }; const handleSquareClick = (squareId) => { if (!isLearnMode || isEngineThinking || isGameOver) return; setTaughtMove(null); // Clear active animation if they are engaging again if (selectedSquare) { if (handleBoardMove(selectedSquare, squareId)) { return; // Move successful } } // Select the piece if they click it const piece = game.get(squareId); if (piece && piece.color === game.turn()) { setSelectedSquare(squareId); } else { setSelectedSquare(null); } }; // --- Notation Input Logic --- const attemptMove = (rawInput) => { if (!rawInput) return false; let input = rawInput.trim(); if (/^[1-8]{4}[qrbn]?$/i.test(input)) { const fromFile = String.fromCharCode('a'.charCodeAt(0) + parseInt(input[0]) - 1); const fromRank = input[1]; const toFile = String.fromCharCode('a'.charCodeAt(0) + parseInt(input[2]) - 1); const toRank = input[3]; const promo = input[4] ? input[4].toLowerCase() : ''; input = `${fromFile}${fromRank}${toFile}${toRank}${promo}`; } try { const tempGame = engineLevel > 0 ? new Chess(ENGINE_FENS[engineLevel]) : new Chess(); const pastMoves = gameRef.current.history(); pastMoves.forEach(m => tempGame.move(m)); let moveObj = null; if (/^[a-h][1-8][a-h][1-8][qrbn]?$/i.test(input)) { const from = input.substring(0, 2).toLowerCase(); const to = input.substring(2, 4).toLowerCase(); const promotion = input[4] ? input[4].toLowerCase() : 'q'; try { moveObj = tempGame.move({ from, to, promotion }); } catch(e) { /* ignore */ } } if (!moveObj) { try { moveObj = tempGame.move(input); } catch (e) { /* ignore */ } } if (!moveObj && typeof tempGame.move === 'function') { try { moveObj = tempGame.move(input, { sloppy: true }); } catch(e) { /* ignore */ } } if (!moveObj && typeof tempGame.move === 'function') { try { moveObj = tempGame.move({ san: input }); } catch(e) { /* ignore */ } } if (moveObj) { setGame(tempGame); setFen(tempGame.fen()); setHistory(tempGame.history({ verbose: true })); setHintMove(null); if (isLearnMode) { setTaughtMove({ san: moveObj.san, color: moveObj.color, trigger: Date.now() }); } const isNowGameOver = typeof tempGame.isGameOver === 'function' ? tempGame.isGameOver() : tempGame.game_over(); playSound(moveObj, isNowGameOver); return true; } } catch (e) { console.error("Attempt move failed deeply:", e); } return false; }; const handleReset = () => { const newGame = engineLevel > 0 ? new Chess(ENGINE_FENS[engineLevel]) : new Chess(); setGame(newGame); setFen(newGame.fen()); setHistory([]); clearCanvas(); setTextInput(''); setInlineError(''); setHintMove(null); setSelectedSquare(null); setTaughtMove(null); playSound(null); }; const handleModeToggle = () => { const newLevel = (engineLevel + 1) % 4; setEngineLevel(newLevel); const newGame = newLevel > 0 ? new Chess(ENGINE_FENS[newLevel]) : new Chess(); setGame(newGame); setFen(newGame.fen()); setHistory([]); clearCanvas(); setTextInput(''); setInlineError(''); setHintMove(null); setSelectedSquare(null); setTaughtMove(null); playSound(null); }; const handleLearnModeToggle = () => { const newLearnState = !isLearnMode; setIsLearnMode(newLearnState); setSelectedSquare(null); setTaughtMove(null); if (!newLearnState) { setTimeout(() => clearCanvas(), 100); } }; const handleUndo = () => { const pastMoves = gameRef.current.history(); if (pastMoves.length === 0 || isEngineThinking) return; const tempGame = engineLevel > 0 ? new Chess(ENGINE_FENS[engineLevel]) : new Chess(); let movesToKeep = pastMoves.length - 1; if (engineLevel > 0 && gameRef.current.turn() === 'w' && pastMoves.length >= 2) { movesToKeep = pastMoves.length - 2; } for (let i = 0; i < movesToKeep; i++) { tempGame.move(pastMoves[i]); } setGame(tempGame); setFen(tempGame.fen()); setHistory(tempGame.history({ verbose: true })); clearCanvas(); setTextInput(''); setInlineError(''); setHintMove(null); setSelectedSquare(null); setTaughtMove(null); playSound({ san: '', flags: '' }); }; const handleHint = () => { if (gameRef.current.turn() !== 'w' || isEngineThinking || isGameOver) return; const legalMoves = gameRef.current.moves({ verbose: true }); if (legalMoves.length === 0) return; let bestMove = legalMoves[0]; let bestScore = -Infinity; legalMoves.forEach(m => { const temp = new Chess(gameRef.current.fen()); temp.move(m.san); let score = 0; if (typeof temp.isCheckmate === 'function' ? temp.isCheckmate() : temp.in_checkmate()) { score += 10000; } else if (typeof temp.isDraw === 'function' ? temp.isDraw() : temp.in_draw()) { score -= 10000; } else { const blackMoves = temp.moves(); score -= blackMoves.length * 10; if (typeof temp.inCheck === 'function' ? temp.inCheck() : temp.in_check()) score += 5; if (m.flags.includes('c') || m.flags.includes('e')) score += 20; const board = temp.board(); let bkR = 0, bkF = 0; board.forEach((row, r) => row.forEach((p, f) => { if (p && p.type === 'k' && p.color === 'b') { bkR = r; bkF = f; } })); const toR = 8 - parseInt(m.to[1]); const toF = m.to.charCodeAt(0) - 97; score -= (Math.abs(toR - bkR) + Math.abs(toF - bkF)); } score += Math.random(); if (score > bestScore) { bestScore = score; bestMove = m; } }); setHintMove({ from: bestMove.from, to: bestMove.to }); }; const handleCopyPgn = () => { const pgnStr = gameRef.current.pgn(); if (!pgnStr) return; const textArea = document.createElement("textarea"); textArea.value = pgnStr; document.body.appendChild(textArea); textArea.select(); document.execCommand("copy"); document.body.removeChild(textArea); setCopied(true); setTimeout(() => setCopied(false), 2000); }; const getMoveErrorMessage = (input, recognized = false, gameInstance) => { if (!input) return "Could not recognize move."; const cleanInput = input.trim().replace(/[+#]$/, ''); if (/^[a-h](x[a-h])?[18]$/i.test(cleanInput)) { return `Missing promotion! Notate it as ${cleanInput}=Q or ${cleanInput}Q.`; } if (gameInstance) { const match = cleanInput.match(/^([KQRBN])?([a-h][1-8]|[a-h]|[1-8])?x?([a-h][1-8])/i); if (match) { const piece = match[1] ? match[1].toLowerCase() : 'p'; const dest = match[3].toLowerCase(); const legalMoves = gameInstance.moves({ verbose: true }); const matchingMoves = legalMoves.filter(m => m.piece === piece && m.to === dest); if (matchingMoves.length > 1) { return `Ambiguous move! Multiple pieces can move to ${dest}.`; } } } return recognized ? `Recognized "${input}", but it's invalid.` : `Invalid move: ${input}`; }; const handleTextSubmit = () => { if (!textInput.trim() || isEngineThinking) return; if (attemptMove(textInput)) { setTextInput(''); setInlineError(''); } else { setInlineError(getMoveErrorMessage(textInput, false, gameRef.current)); } }; // --- Drawing Logic --- const getCoordinates = (e) => { const canvas = canvasRef.current; const rect = canvas.getBoundingClientRect(); const clientX = e.touches && e.touches.length > 0 ? e.touches[0].clientX : e.clientX; const clientY = e.touches && e.touches.length > 0 ? e.touches[0].clientY : e.clientY; return { x: clientX - rect.left, y: clientY - rect.top }; }; const startDrawing = (e) => { if (e.cancelable) e.preventDefault(); if (isEngineThinking) return; if (drawTimeoutRef.current) clearTimeout(drawTimeoutRef.current); drawSequenceRef.current += 1; setIsDrawing(true); hasDrawnRef.current = true; setInlineError(''); setProcessingType(null); setHintMove(null); setTaughtMove(null); // Dismiss lesson automatically const { x, y } = getCoordinates(e); ctxRef.current.beginPath(); ctxRef.current.moveTo(x, y); }; const draw = (e) => { if (!isDrawing) return; if (e.cancelable) e.preventDefault(); if (drawTimeoutRef.current) clearTimeout(drawTimeoutRef.current); const { x, y } = getCoordinates(e); ctxRef.current.lineTo(x, y); ctxRef.current.stroke(); }; const stopDrawing = () => { if (!isDrawing) return; ctxRef.current.closePath(); setIsDrawing(false); const shouldAutoSubmit = !showManualControls || autoSubmit; if (shouldAutoSubmit && hasDrawnRef.current && !isEngineThinking) { if (drawTimeoutRef.current) clearTimeout(drawTimeoutRef.current); drawTimeoutRef.current = setTimeout(() => { processNotation(true); }, 800); } }; const clearCanvas = () => { const canvas = canvasRef.current; const ctx = ctxRef.current; if (ctx && canvas) { ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); } hasDrawnRef.current = false; if (drawTimeoutRef.current) clearTimeout(drawTimeoutRef.current); setInlineError(''); setProcessingType(null); drawSequenceRef.current += 1; setTaughtMove(null); }; // --- OCR Logic --- const fetchGeminiOCR = async (base64Data, retries = 5) => { const prompt = "Read the handwritten chess notation in this image. Respond ONLY with the standard algebraic chess notation (e.g., e4, Nf3, O-O, exd5). Do not include any other text, markdown, or punctuation. If you are unsure, provide your best single guess."; const payload = { contents: [{ role: "user", parts: [ { text: prompt }, { inlineData: { mimeType: "image/png", data: base64Data } } ] }] }; const delays = [1000, 2000, 4000, 8000, 16000]; for (let i = 0; i < retries; i++) { try { const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const result = await response.json(); const text = result.candidates?.[0]?.content?.parts?.[0]?.text; return text?.trim() || ""; } catch (error) { if (i === retries - 1) throw error; await new Promise(res => setTimeout(res, delays[i])); } } }; const processNotation = async (isAuto = false) => { const canvas = canvasRef.current; if (!canvas || !hasDrawnRef.current || isEngineThinking) return; if (drawTimeoutRef.current) clearTimeout(drawTimeoutRef.current); const currentSeq = drawSequenceRef.current; setProcessingType(isAuto ? 'auto' : 'manual'); setInlineError(''); const dataUrl = canvas.toDataURL('image/png'); const base64Data = dataUrl.split(',')[1]; try { const text = await fetchGeminiOCR(base64Data); if (currentSeq !== drawSequenceRef.current) return; let parsed = text.replace(/```[a-zA-Z]*\n?/g, '').replace(/```/g, '').trim(); parsed = parsed.split(/\s+/)[0]; parsed = parsed.replace(/0/g, 'O'); parsed = parsed.replace(/[^a-zA-Z0-9\+\-\=\#xO]/g, ''); setProcessingType(null); if (isAuto) { if (parsed && attemptMove(parsed)) { setConfirmModal({ isOpen: false, text: '', error: '' }); clearCanvas(); } else { setInlineError(parsed ? getMoveErrorMessage(parsed, true, gameRef.current) : 'Could not recognize move.'); } } else { if (parsed && attemptMove(parsed)) { setConfirmModal({ isOpen: false, text: '', error: '' }); clearCanvas(); } else { setConfirmModal({ isOpen: true, text: parsed, error: parsed ? getMoveErrorMessage(parsed, false, gameRef.current) : 'Could not read text.' }); } } } catch (error) { if (currentSeq !== drawSequenceRef.current) return; console.error("OCR Failed:", error); setProcessingType(null); if (isAuto) { setInlineError('Failed to read drawing.'); } else { setConfirmModal({ isOpen: true, text: '', error: 'Could not read text. Please type manually.' }); } } }; const submitMove = (e) => { e.preventDefault(); const moveStr = confirmModal.text.trim(); if (!moveStr) { setConfirmModal(prev => ({ ...prev, error: 'Please enter a move.' })); return; } if (attemptMove(moveStr)) { setConfirmModal({ isOpen: false, text: '', error: '' }); clearCanvas(); } else { setConfirmModal(prev => ({ ...prev, error: getMoveErrorMessage(moveStr, false, gameRef.current) })); } }; // --- Board Rendering Helper --- const getBoardGrid = () => { const board = game.board(); const legalMovesForSelected = selectedSquare ? game.moves({ verbose: true, square: selectedSquare }) : []; const legalSquares = legalMovesForSelected.map(m => m.to); return RANKS.map((rank, rIdx) => FILES.map((file, fIdx) => { const piece = board[rIdx][fIdx]; const isLight = (rIdx + fIdx) % 2 === 0; const id = `${file}${rank}`; return { id, isLight, piece, isSelected: selectedSquare === id, isLegalDest: legalSquares.includes(id) }; }) ).flat(); }; const getMovePairs = () => { const pairs = []; for (let i = 0; i < history.length; i += 2) { pairs.push({ white: history[i], black: history[i + 1] || null }); } return pairs; }; return (
{/* Mac Window Container */}
{/* Titlebar */}
CHESSPANDA NOTATER
{/* Controls */}
{/* Main Content Area */}
{/* Center Column: Board & Drawing Pad */}
{/* Chessboard */}
{/* Top Spacer */}
X
{/* Ranks (1-8) - LEFT */}
{RANKS.map(r =>
{r}
)}
{/* Board */}
{getBoardGrid().map((sq) => (
handleSquareClick(sq.id)} onDragOver={(e) => { if (!isLearnMode) return; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }} onDrop={(e) => { if (!isLearnMode) return; e.preventDefault(); const fromId = e.dataTransfer.getData('text/plain'); if (fromId && fromId !== sq.id) { handleBoardMove(fromId, sq.id); } }} className={`relative flex items-center justify-center ${sq.isLight ? 'bg-[#FFFFFF]' : 'bg-[#5C9CE6]'} ${isLearnMode && !isGameOver && !isEngineThinking ? 'cursor-pointer hover:opacity-90' : ''}`} > {sq.isSelected && (
)} {sq.isLegalDest && (
)} {sq.piece && ( {`${sq.piece.color} { if (!isLearnMode || isEngineThinking || isGameOver || sq.piece.color !== game.turn()) { e.preventDefault(); return; } e.dataTransfer.setData('text/plain', sq.id); e.dataTransfer.effectAllowed = 'move'; setSelectedSquare(sq.id); setTaughtMove(null); // Dismiss previous lesson }} /> )}
))} {/* SVG Arrow Overlay for Hint Move */} {hintMove && ( )}
{/* Right Spacer */}
{/* Files (a-h) - BOTTOM */}
{FILES.map(f =>
{f}
)}
{isGameOver && (
Game Over! {isCheckmate ? 'Checkmate!' : 'Draw / Stalemate!'}
)} {/* Drawing/Input Area (Notation Pad) */}

setInputMode(prev => prev === 'draw' ? 'text' : 'draw')} title={isLearnMode ? "Learn Mode Active (Toggle Input Mode)" : "Toggle Input Mode (N)"} > {isLearnMode ? : inputMode === 'draw' ? : } {isLearnMode ? 'Learn to Notate' : 'Notation Pad'} {isEngineThinking ? ( Engine moving... ) : processingType === 'auto' && ( reading... )}

{engineLevel > 0 && ( )}
{/* Contextual Pad Container */}
{/* Background & Content based on mode */} {inputMode === 'draw' ? (
{processingType === 'manual' && (
Reading handwriting...
)}
) : (
{ setTextInput(e.target.value); setInlineError(''); setHintMove(null); setTaughtMove(null); // Dismiss lesson on typing }} onKeyDown={(e) => { if (e.key === 'Enter') handleTextSubmit(); }} disabled={isEngineThinking} placeholder="Type move (e.g. e4, d2d4, 7163)" className="w-full h-full text-center text-3xl sm:text-4xl lg:text-5xl font-semibold text-gray-800 bg-transparent focus:outline-none placeholder:text-gray-300 placeholder:font-normal placeholder:text-xl sm:placeholder:text-2xl" />
)} {/* Learn Mode Idle Overlay */} {isLearnMode && !taughtMove && history.length === 0 && inputMode === 'draw' && (

Draw, Type, or Play

)} {/* Taught Move Overlay (Learn Mode only) */} {isLearnMode && taughtMove && (
setTaughtMove(null)} className="absolute inset-0 bg-[#F8F9FA]/95 backdrop-blur-sm z-40 flex flex-col items-center justify-center p-6 animate-in fade-in duration-300 cursor-pointer" > {taughtMove.color === 'w' ? 'White played' : 'Black played'}
{taughtMove.san}
)}
{/* Inline Error */} {inlineError && typeof inlineError === 'string' && (
{inlineError} {inlineError.includes('Ambiguous') && ( )}
)} {/* Controls Below Pad */} {showManualControls && (
)}
{/* Move History Drawer (Slide-over) */} {showHistory && (
setShowHistory(false)} /> )}

Move History

{history.length === 0 ? (
No moves recorded yet.
) : (
{getMovePairs().map((turn, i) => (
{i + 1}.
{turn.white.san}
{turn.black ? turn.black.san : ''}
))}
)}
{/* Confirmation/Correction Modal */} {confirmModal.isOpen && (

Confirm Move

setConfirmModal(prev => ({ ...prev, text: e.target.value, error: '' }))} className="w-full text-center text-2xl font-semibold text-gray-800 bg-gray-50 border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-[#007AFF] focus:ring-1 focus:ring-[#007AFF] transition-all" placeholder="e.g. Nf3" /> {confirmModal.error && (
{confirmModal.error}
{confirmModal.error.includes('Ambiguous') && ( )}
)}
)} {/* Ambiguous Moves Help Modal */} {showAmbiguityHelp && (

How to Notate Ambiguous Moves

When two (or more) identical pieces can move to the same square, you must specify which piece is moving by adding an extra letter or number.

1. Different Columns (Files)

If the pieces are on different columns, use the starting letter.
Ndf3 (Knight on the 'd' file moves to f3)

2. Different Rows (Ranks)

If they are on the same column but different rows, use the starting number.
R1e4 (Rook on the 1st rank moves to e4)

3. Both Different

Rarely, you might need the exact starting square.
Qh4e1 (Queen on h4 moves to e1)

)} {/* Settings Modal */} {showSettings && (

Settings

)}
); }