diff --git a/src/components/ChessGame.tsx b/src/components/ChessGame.tsx index e795535..4ea5901 100644 --- a/src/components/ChessGame.tsx +++ b/src/components/ChessGame.tsx @@ -13,7 +13,7 @@ import { SupportedLanguage } from "@/lib/i18n/translations"; import { lookupOpening, lookupPossibleOpenings, extractMoveSequenceFromPGN, OpeningMetadata } from "@/lib/openings"; import { GameAnalysisModal } from "./GameAnalysisModal"; import { GameOverModal, MoveHistoryItem } from "./GameOverModal"; -import { Brain, ArrowLeft } from "lucide-react"; +import { Brain, ArrowLeft, Download } from "lucide-react"; import { CapturedPieces } from "./CapturedPieces"; import { detectMissedTactics, uciToSan, DetectedTactic } from "@/lib/tacticDetection"; import { upsertSavedGame } from "@/lib/savedGames"; @@ -64,6 +64,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso // Game State const [playerColor, setPlayerColor] = useState<'white' | 'black'>(initialColor); const [showAnalysisModal, setShowAnalysisModal] = useState(false); + const [showDownloadModal, setShowDownloadModal] = useState(false); const [gameOverState, setGameOverState] = useState<{ result: string, winner: "White" | "Black" | "Draw" } | null>(null); const [moveHistory, setMoveHistory] = useState([]); const [selectedPersonality, setSelectedPersonality] = useState(initialPersonality); @@ -290,6 +291,13 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso return false; } + // Wait for pre-analysis (evalP0) to be available before allowing moves + // This ensures we can properly track move history with evaluations + if (!evalP0) { + console.log("Waiting for position analysis before move..."); + return false; + } + const move = { from: sourceSquare, to: targetSquare, @@ -468,6 +476,34 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso updateCapturedPieces(); }; + const handleDownloadPGN = () => { + const pgn = gameRef.current.pgn(); + const blob = new Blob([pgn], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `chess-game-${Date.now()}.pgn`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + setShowDownloadModal(false); + }; + + const handleDownloadFEN = () => { + const fen = gameRef.current.fen(); + const blob = new Blob([fen], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `chess-position-${Date.now()}.fen`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + setShowDownloadModal(false); + }; + // Determine material advantage // If Black lost more value, White has advantage const whiteAdvantage = materialScore.black - materialScore.white; @@ -627,12 +663,20 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso

Game History

- +
+ + +
@@ -715,6 +759,40 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso onNewGame={handleNewGame} /> )} + + {showDownloadModal && ( +
+
+
+

+ {t.analysis.downloadTitle} +

+ +
+
+ + +
+
+
+ )} ); } diff --git a/src/lib/i18n/translations.ts b/src/lib/i18n/translations.ts index 28e9ecf..e146fcc 100644 --- a/src/lib/i18n/translations.ts +++ b/src/lib/i18n/translations.ts @@ -130,6 +130,10 @@ export interface Translations { noApiKey: string; askFollowUp: string; possibleOpenings: string; + downloadGame: string; + downloadPGN: string; + downloadFEN: string; + downloadTitle: string; }; // API Key Input @@ -293,6 +297,10 @@ const en: Translations = { noApiKey: 'Please add an API key in settings to get opening explanations.', askFollowUp: 'Ask a follow-up question about this opening...', possibleOpenings: 'possible openings', + downloadGame: 'Download Game', + downloadPGN: 'Download as PGN', + downloadFEN: 'Download Current Position (FEN)', + downloadTitle: 'Export Game', }, apiKeyInput: { title: 'API Key Required', @@ -457,6 +465,10 @@ const de: Translations = { noApiKey: 'Bitte fügen Sie in den Einstellungen einen API-Schlüssel hinzu.', askFollowUp: 'Stellen Sie eine Folgefrage zu dieser Eröffnung...', possibleOpenings: 'mögliche Eröffnungen', + downloadGame: 'Partie herunterladen', + downloadPGN: 'Als PGN herunterladen', + downloadFEN: 'Aktuelle Position herunterladen (FEN)', + downloadTitle: 'Partie exportieren', }, apiKeyInput: { title: 'API-Schlüssel erforderlich', @@ -621,6 +633,10 @@ const fr: Translations = { noApiKey: 'Veuillez ajouter une clé API dans les paramètres.', askFollowUp: 'Posez une question sur cette ouverture...', possibleOpenings: 'ouvertures possibles', + downloadGame: 'Télécharger la partie', + downloadPGN: 'Télécharger en PGN', + downloadFEN: 'Télécharger la position actuelle (FEN)', + downloadTitle: 'Exporter la partie', }, apiKeyInput: { title: 'Clé API requise', @@ -785,6 +801,10 @@ const it: Translations = { noApiKey: 'Aggiungi una chiave API nelle impostazioni.', askFollowUp: 'Fai una domanda su questa apertura...', possibleOpenings: 'aperture possibili', + downloadGame: 'Scarica partita', + downloadPGN: 'Scarica come PGN', + downloadFEN: 'Scarica posizione attuale (FEN)', + downloadTitle: 'Esporta partita', }, apiKeyInput: { title: 'Chiave API richiesta', @@ -949,6 +969,10 @@ const pl: Translations = { noApiKey: 'Dodaj klucz API w ustawieniach.', askFollowUp: 'Zadaj pytanie uzupełniające o to otwarcie...', possibleOpenings: 'możliwe otwarcia', + downloadGame: 'Pobierz grę', + downloadPGN: 'Pobierz jako PGN', + downloadFEN: 'Pobierz aktualną pozycję (FEN)', + downloadTitle: 'Eksportuj grę', }, apiKeyInput: { title: 'Wymagany klucz API',