analysis
This commit is contained in:
Generated
+20
@@ -112,6 +112,7 @@
|
|||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -699,6 +700,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@@ -722,6 +724,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -743,6 +746,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/accessibility": "^3.1.1",
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@@ -2945,6 +2949,7 @@
|
|||||||
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -2954,6 +2959,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
|
||||||
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
|
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -2964,6 +2970,7 @@
|
|||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@@ -3051,6 +3058,7 @@
|
|||||||
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
|
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.47.0",
|
"@typescript-eslint/scope-manager": "8.47.0",
|
||||||
"@typescript-eslint/types": "8.47.0",
|
"@typescript-eslint/types": "8.47.0",
|
||||||
@@ -3587,6 +3595,7 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -4107,6 +4116,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.8.25",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
@@ -5085,6 +5095,7 @@
|
|||||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -5270,6 +5281,7 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -7080,6 +7092,7 @@
|
|||||||
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "30.2.0",
|
"@jest/core": "30.2.0",
|
||||||
"@jest/types": "30.2.0",
|
"@jest/types": "30.2.0",
|
||||||
@@ -8066,6 +8079,7 @@
|
|||||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssstyle": "^4.2.1",
|
"cssstyle": "^4.2.1",
|
||||||
"data-urls": "^5.0.0",
|
"data-urls": "^5.0.0",
|
||||||
@@ -10119,6 +10133,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -10146,6 +10161,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -11307,6 +11323,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -11498,6 +11515,7 @@
|
|||||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cspotcode/source-map-support": "^0.8.0",
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
"@tsconfig/node10": "^1.0.7",
|
"@tsconfig/node10": "^1.0.7",
|
||||||
@@ -11688,6 +11706,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -12420,6 +12439,7 @@
|
|||||||
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
|
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
+258
-169
@@ -3,7 +3,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Chess } from "chess.js";
|
import { Chess } from "chess.js";
|
||||||
import { Chessboard } from "react-chessboard";
|
import { Chessboard } from "react-chessboard";
|
||||||
import { Brain, ChevronLeft, ChevronRight, Loader2, ArrowLeft, Download, PlayCircle } from "lucide-react";
|
import { Brain, ChevronLeft, ChevronRight, Loader2, ArrowLeft, Download, PlayCircle, Upload, RotateCcw } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
@@ -19,6 +19,7 @@ import { ChatSession } from "@google/generative-ai";
|
|||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { useDebug } from "@/contexts/DebugContext";
|
import { useDebug } from "@/contexts/DebugContext";
|
||||||
import { GameImportModal } from "@/components/GameImportModal";
|
import { GameImportModal } from "@/components/GameImportModal";
|
||||||
|
import { OpeningsModal } from "@/components/OpeningsModal";
|
||||||
|
|
||||||
interface MoveStep {
|
interface MoveStep {
|
||||||
san: string;
|
san: string;
|
||||||
@@ -65,6 +66,7 @@ export default function AnalysisPage() {
|
|||||||
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
||||||
const [showImportModal, setShowImportModal] = useState(false);
|
const [showImportModal, setShowImportModal] = useState(false);
|
||||||
const [showPlayModal, setShowPlayModal] = useState(false);
|
const [showPlayModal, setShowPlayModal] = useState(false);
|
||||||
|
const [showOpeningsModal, setShowOpeningsModal] = useState(false);
|
||||||
const [playPersonality, setPlayPersonality] = useState<Personality>(PERSONALITIES[0]);
|
const [playPersonality, setPlayPersonality] = useState<Personality>(PERSONALITIES[0]);
|
||||||
const [playColor, setPlayColor] = useState<"white" | "black">("white");
|
const [playColor, setPlayColor] = useState<"white" | "black">("white");
|
||||||
const [playStrength, setPlayStrength] = useState(15);
|
const [playStrength, setPlayStrength] = useState(15);
|
||||||
@@ -74,6 +76,18 @@ export default function AnalysisPage() {
|
|||||||
const storedLang = localStorage.getItem("chess_tutor_language");
|
const storedLang = localStorage.getItem("chess_tutor_language");
|
||||||
if (storedKey) setApiKey(storedKey);
|
if (storedKey) setApiKey(storedKey);
|
||||||
if (storedLang) setLanguage(storedLang as SupportedLanguage);
|
if (storedLang) setLanguage(storedLang as SupportedLanguage);
|
||||||
|
|
||||||
|
// Check for pending analysis from saved game
|
||||||
|
const pendingAnalysis = localStorage.getItem("chess_tutor_pending_analysis");
|
||||||
|
if (pendingAnalysis) {
|
||||||
|
localStorage.removeItem("chess_tutor_pending_analysis");
|
||||||
|
setInput(pendingAnalysis);
|
||||||
|
setDetectedFormat(detectChessFormat(pendingAnalysis));
|
||||||
|
// Load the game after a short delay to ensure stockfish is ready
|
||||||
|
setTimeout(() => {
|
||||||
|
loadGameFromPgnOrFen(pendingAnalysis);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -226,6 +240,19 @@ IMPORTANT:
|
|||||||
loadGameFromPgnOrFen(pgn);
|
loadGameFromPgnOrFen(pgn);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResetAnalysis = () => {
|
||||||
|
setInput("");
|
||||||
|
setDetectedFormat(null);
|
||||||
|
setSteps([]);
|
||||||
|
setCurrentIndex(0);
|
||||||
|
setStepDetails({});
|
||||||
|
setComments({});
|
||||||
|
setInitialFen(DEFAULT_START);
|
||||||
|
evaluationCache.current = {};
|
||||||
|
setEvaluationVersion(v => v + 1);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handleStartGameFromPosition = () => {
|
const handleStartGameFromPosition = () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
fen: currentFen,
|
fen: currentFen,
|
||||||
@@ -400,90 +427,137 @@ INSTRUCTIONS:
|
|||||||
|
|
||||||
<main className="flex-grow w-full flex justify-center px-4 py-8">
|
<main className="flex-grow w-full flex justify-center px-4 py-8">
|
||||||
<div className="w-full max-w-6xl space-y-8">
|
<div className="w-full max-w-6xl space-y-8">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 md:p-8 border border-gray-200 dark:border-gray-700">
|
{/* Phase 1: Import View - shown when no game is loaded */}
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
{steps.length === 0 && (
|
||||||
<div>
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 md:p-8 border border-gray-200 dark:border-gray-700">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<Brain className="text-purple-600" /> {t.analysis.modeTitle}
|
<div>
|
||||||
</h1>
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
<p className="text-gray-600 dark:text-gray-300 mt-2">{t.analysis.modeDescription}</p>
|
<Brain className="text-purple-600" /> {t.analysis.modeTitle}
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mt-2">{t.analysis.modeDescription}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">{t.analysis.orientation}</label>
|
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<select
|
<div className="space-y-4">
|
||||||
value={orientation}
|
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300">{t.analysis.pasteLabel}</label>
|
||||||
onChange={(e) => setOrientation(e.target.value as "white" | "black")}
|
<textarea
|
||||||
className="p-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700"
|
value={input}
|
||||||
>
|
onChange={(e) => handleInputChange(e.target.value)}
|
||||||
<option value="white">White</option>
|
placeholder={t.analysis.pastePlaceholder}
|
||||||
<option value="black">Black</option>
|
className="w-full p-3 border rounded-lg dark:bg-gray-700 dark:border-gray-600 font-mono text-sm min-h-[180px]"
|
||||||
</select>
|
/>
|
||||||
|
{detectedFormat && (
|
||||||
|
<p className="text-xs text-gray-500">Detected: {detectedFormat.toUpperCase()}</p>
|
||||||
|
)}
|
||||||
|
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-semibold text-gray-700 dark:text-gray-300">{t.analysis.chooseCoach}</p>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{PERSONALITIES.map(p => (
|
||||||
|
<button
|
||||||
|
key={p.id}
|
||||||
|
onClick={() => setSelectedPersonality(p)}
|
||||||
|
className={`p-3 rounded-lg border flex items-center gap-2 ${selectedPersonality.id === p.id
|
||||||
|
? "border-purple-500 bg-purple-50 dark:bg-purple-900/20"
|
||||||
|
: "border-gray-200 dark:border-gray-700"}`}
|
||||||
|
>
|
||||||
|
<span className="text-xl">{p.image}</span>
|
||||||
|
<span className="text-sm text-left text-gray-800 dark:text-gray-100">{p.name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleLoadGame}
|
||||||
|
className="w-full py-3 bg-purple-600 text-white rounded-xl hover:bg-purple-700 font-semibold shadow-lg"
|
||||||
|
>
|
||||||
|
{t.analysis.startButton}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Import from Online Platforms */}
|
||||||
|
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2 text-center">
|
||||||
|
Or import from online platforms
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowImportModal(true)}
|
||||||
|
className="py-2 px-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-medium text-sm flex items-center justify-center gap-2 shadow"
|
||||||
|
>
|
||||||
|
<Download size={16} />
|
||||||
|
Chess.com
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowImportModal(true)}
|
||||||
|
className="py-2 px-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium text-sm flex items-center justify-center gap-2 shadow"
|
||||||
|
>
|
||||||
|
<Download size={16} />
|
||||||
|
Lichess
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-900 rounded-xl p-4 flex flex-col items-center justify-center border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<Chessboard
|
||||||
|
options={{
|
||||||
|
position: currentFen,
|
||||||
|
boardOrientation: orientation,
|
||||||
|
allowDragging: false,
|
||||||
|
darkSquareStyle: { backgroundColor: '#779954' },
|
||||||
|
lightSquareStyle: { backgroundColor: '#e9edcc' },
|
||||||
|
animationDurationInMs: 200,
|
||||||
|
boardStyle: { borderRadius: "12px", boxShadow: "0 8px 30px rgba(0,0,0,0.12)" }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-sm text-gray-500 dark:text-gray-400 text-center">
|
||||||
|
{t.analysis.currentPosition}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
|
{/* Phase 2: Analysis View - shown when game is loaded */}
|
||||||
<div className="space-y-4">
|
{steps.length > 0 && (
|
||||||
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300">{t.analysis.pasteLabel}</label>
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 md:p-8 border border-gray-200 dark:border-gray-700">
|
||||||
<textarea
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||||
value={input}
|
<div className="flex items-center gap-3">
|
||||||
onChange={(e) => handleInputChange(e.target.value)}
|
<span className="text-2xl">{selectedPersonality.image}</span>
|
||||||
placeholder={t.analysis.pastePlaceholder}
|
<div>
|
||||||
className="w-full p-3 border rounded-lg dark:bg-gray-700 dark:border-gray-600 font-mono text-sm min-h-[180px]"
|
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||||
/>
|
{t.analysis.modeTitle}
|
||||||
{detectedFormat && (
|
</h1>
|
||||||
<p className="text-xs text-gray-500">Detected: {detectedFormat.toUpperCase()}</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
)}
|
Coach: {selectedPersonality.name}
|
||||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
</p>
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-sm font-semibold text-gray-700 dark:text-gray-300">{t.analysis.chooseCoach}</p>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{PERSONALITIES.map(p => (
|
|
||||||
<button
|
|
||||||
key={p.id}
|
|
||||||
onClick={() => setSelectedPersonality(p)}
|
|
||||||
className={`p-3 rounded-lg border flex items-center gap-2 ${selectedPersonality.id === p.id
|
|
||||||
? "border-purple-500 bg-purple-50 dark:bg-purple-900/20"
|
|
||||||
: "border-gray-200 dark:border-gray-700"}`}
|
|
||||||
>
|
|
||||||
<span className="text-xl">{p.image}</span>
|
|
||||||
<span className="text-sm text-left text-gray-800 dark:text-gray-100">{p.name}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="flex items-center gap-3">
|
||||||
onClick={handleLoadGame}
|
<button
|
||||||
className="w-full py-3 bg-purple-600 text-white rounded-xl hover:bg-purple-700 font-semibold shadow-lg"
|
onClick={handleResetAnalysis}
|
||||||
>
|
className="px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center gap-2"
|
||||||
{t.analysis.startButton}
|
>
|
||||||
</button>
|
<RotateCcw size={16} />
|
||||||
|
{t.analysis.loadNewGame}
|
||||||
{/* Import from Online Platforms */}
|
</button>
|
||||||
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
|
<select
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2 text-center">
|
value={orientation}
|
||||||
Or import from online platforms
|
onChange={(e) => setOrientation(e.target.value as "white" | "black")}
|
||||||
</p>
|
className="p-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 text-sm"
|
||||||
<div className="grid grid-cols-2 gap-2">
|
>
|
||||||
<button
|
<option value="white">White</option>
|
||||||
onClick={() => setShowImportModal(true)}
|
<option value="black">Black</option>
|
||||||
className="py-2 px-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-medium text-sm flex items-center justify-center gap-2 shadow"
|
</select>
|
||||||
>
|
|
||||||
<Download size={16} />
|
|
||||||
Chess.com
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowImportModal(true)}
|
|
||||||
className="py-2 px-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium text-sm flex items-center justify-center gap-2 shadow"
|
|
||||||
>
|
|
||||||
<Download size={16} />
|
|
||||||
Lichess
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-xl p-4 flex flex-col items-center gap-3 border border-gray-200 dark:border-gray-700">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-lg">
|
||||||
<Chessboard
|
<Chessboard
|
||||||
options={{
|
options={{
|
||||||
position: currentFen,
|
position: currentFen,
|
||||||
@@ -499,124 +573,128 @@ INSTRUCTIONS:
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentIndex(i => Math.max(0, i - 1))}
|
onClick={() => setCurrentIndex(i => Math.max(0, i - 1))}
|
||||||
className="p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50"
|
className="p-2 rounded-lg bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-600 disabled:opacity-50"
|
||||||
disabled={currentIndex === 0}
|
disabled={currentIndex === 0}
|
||||||
aria-label={t.analysis.previous}
|
aria-label={t.analysis.previous}
|
||||||
>
|
>
|
||||||
<ChevronLeft size={20} />
|
<ChevronLeft size={20} />
|
||||||
</button>
|
</button>
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
<div className="text-sm text-gray-600 dark:text-gray-300 min-w-[100px] text-center">
|
||||||
{t.analysis.step} {currentIndex} / {steps.length}
|
{t.analysis.step} {currentIndex} / {steps.length}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentIndex(i => Math.min(steps.length, i + 1))}
|
onClick={() => setCurrentIndex(i => Math.min(steps.length, i + 1))}
|
||||||
className="p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50"
|
className="p-2 rounded-lg bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-600 disabled:opacity-50"
|
||||||
disabled={currentIndex >= steps.length}
|
disabled={currentIndex >= steps.length}
|
||||||
aria-label={t.analysis.next}
|
aria-label={t.analysis.next}
|
||||||
>
|
>
|
||||||
<ChevronRight size={20} />
|
<ChevronRight size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<button
|
||||||
<button
|
onClick={() => {
|
||||||
onClick={() => {
|
setPlayColor(orientation);
|
||||||
setPlayColor(orientation);
|
setShowPlayModal(true);
|
||||||
setShowPlayModal(true);
|
}}
|
||||||
}}
|
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg shadow hover:bg-blue-700 transition-colors"
|
||||||
className="mt-3 inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg shadow hover:bg-blue-700 transition-colors w-full justify-center"
|
>
|
||||||
>
|
<PlayCircle size={18} /> {t.analysis.playFromHere}
|
||||||
<PlayCircle size={18} /> {t.analysis.playFromHere}
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
{/* Analysis Panels - only shown when game is loaded */}
|
||||||
<div className="lg:col-span-2 bg-white dark:bg-gray-800 rounded-2xl shadow border border-gray-200 dark:border-gray-700 p-6 space-y-4">
|
{steps.length > 0 && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">{t.analysis.title}</h2>
|
<div className="lg:col-span-2 bg-white dark:bg-gray-800 rounded-2xl shadow border border-gray-200 dark:border-gray-700 p-6 space-y-4">
|
||||||
{possibleOpenings.length > 0 && (
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm text-blue-700 dark:text-blue-300">
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white">{t.analysis.title}</h2>
|
||||||
{t.analysis.opening}: {possibleOpenings.length === 1
|
{possibleOpenings.length > 0 && (
|
||||||
? `${possibleOpenings[0].name} (${possibleOpenings[0].eco})`
|
<button
|
||||||
: `${possibleOpenings.length} possible openings`
|
onClick={() => setShowOpeningsModal(true)}
|
||||||
}
|
className="text-sm text-blue-700 dark:text-blue-300 hover:text-blue-900 dark:hover:text-blue-100 hover:underline cursor-pointer transition-colors"
|
||||||
</span>
|
>
|
||||||
|
{t.analysis.opening}: {possibleOpenings.length === 1
|
||||||
|
? `${possibleOpenings[0].name} (${possibleOpenings[0].eco})`
|
||||||
|
: `${possibleOpenings.length} ${t.analysis.possibleOpenings}`
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{currentIndex === 0 ? (
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">{t.analysis.currentPosition}</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex flex-wrap gap-4 text-sm text-gray-700 dark:text-gray-200">
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold">{t.analysis.step} {currentIndex}</div>
|
||||||
|
<div>{steps[currentIndex - 1]?.san}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold">{t.analysis.evaluation}</div>
|
||||||
|
<div>{formatEval(currentDetails?.evalAfter)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold">{t.analysis.cpLoss}</div>
|
||||||
|
<div>{formatCpLoss(currentDetails?.cpLoss)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold">{t.analysis.bestMove}</div>
|
||||||
|
<div>{currentDetails?.bestMoveSan || t.analysis.enginePending}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-800 dark:text-gray-100 mb-2">{t.analysis.missedTactics}</div>
|
||||||
|
{tacticSummary.length === 0 && (
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">{t.analysis.none}</p>
|
||||||
|
)}
|
||||||
|
{tacticSummary.length > 0 && (
|
||||||
|
<ul className="list-disc pl-5 space-y-1 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{tacticSummary.map((tactic, idx) => (
|
||||||
|
<li key={`${tactic.move}-${idx}`}>
|
||||||
|
{tactic.tactic_type}
|
||||||
|
{tactic.material_delta ? ` (~${(tactic.material_delta / 100).toFixed(1)} pawns)` : ""}
|
||||||
|
{tactic.affected_squares ? ` on ${tactic.affected_squares.join(", ")}` : ""}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{currentIndex === 0 ? (
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">{t.analysis.currentPosition}</p>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex flex-wrap gap-4 text-sm text-gray-700 dark:text-gray-200">
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold">{t.analysis.step} {currentIndex}</div>
|
|
||||||
<div>{steps[currentIndex - 1]?.san}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold">{t.analysis.evaluation}</div>
|
|
||||||
<div>{formatEval(currentDetails?.evalAfter)}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold">{t.analysis.cpLoss}</div>
|
|
||||||
<div>{formatCpLoss(currentDetails?.cpLoss)}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold">{t.analysis.bestMove}</div>
|
|
||||||
<div>{currentDetails?.bestMoveSan || t.analysis.enginePending}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow border border-gray-200 dark:border-gray-700 p-6 space-y-4">
|
||||||
<div className="font-semibold text-gray-800 dark:text-gray-100 mb-2">{t.analysis.missedTactics}</div>
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white">{t.analysis.aiAnalysis}</h2>
|
||||||
{tacticSummary.length === 0 && (
|
{!apiKey && (
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t.analysis.none}</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">Please add an API key in settings to receive commentary.</p>
|
||||||
|
)}
|
||||||
|
{currentIndex === 0 && (
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">{t.analysis.currentPosition}</p>
|
||||||
|
)}
|
||||||
|
{currentIndex > 0 && (
|
||||||
|
<div className="min-h-[140px]">
|
||||||
|
{isCommenting && (
|
||||||
|
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-300">
|
||||||
|
<Loader2 className="animate-spin" size={18} />
|
||||||
|
<span>{t.analysis.coachPending}</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{tacticSummary.length > 0 && (
|
{!isCommenting && comments[currentIndex] && (
|
||||||
<ul className="list-disc pl-5 space-y-1 text-sm text-gray-700 dark:text-gray-300">
|
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||||
{tacticSummary.map((tactic, idx) => (
|
<ReactMarkdown>{comments[currentIndex]}</ReactMarkdown>
|
||||||
<li key={`${tactic.move}-${idx}`}>
|
</div>
|
||||||
{tactic.tactic_type}
|
)}
|
||||||
{tactic.material_delta ? ` (~${(tactic.material_delta / 100).toFixed(1)} pawns)` : ""}
|
{!isCommenting && !comments[currentIndex] && (
|
||||||
{tactic.affected_squares ? ` on ${tactic.affected_squares.join(", ")}` : ""}
|
<p className="text-sm text-gray-600 dark:text-gray-400">{t.analysis.coachPending}</p>
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow border border-gray-200 dark:border-gray-700 p-6 space-y-4">
|
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">{t.analysis.aiAnalysis}</h2>
|
|
||||||
{!apiKey && (
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Please add an API key in settings to receive commentary.</p>
|
|
||||||
)}
|
|
||||||
{currentIndex === 0 && (
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t.analysis.currentPosition}</p>
|
|
||||||
)}
|
|
||||||
{currentIndex > 0 && (
|
|
||||||
<div className="min-h-[140px]">
|
|
||||||
{isCommenting && (
|
|
||||||
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-300">
|
|
||||||
<Loader2 className="animate-spin" size={18} />
|
|
||||||
<span>{t.analysis.coachPending}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isCommenting && comments[currentIndex] && (
|
|
||||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
|
||||||
<ReactMarkdown>{comments[currentIndex]}</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isCommenting && !comments[currentIndex] && (
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t.analysis.coachPending}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -629,6 +707,17 @@ INSTRUCTIONS:
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Openings Explorer Modal */}
|
||||||
|
{showOpeningsModal && possibleOpenings.length > 0 && (
|
||||||
|
<OpeningsModal
|
||||||
|
openings={possibleOpenings}
|
||||||
|
currentFen={currentFen}
|
||||||
|
language={language}
|
||||||
|
personality={selectedPersonality}
|
||||||
|
onClose={() => setShowOpeningsModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Play From Position Modal */}
|
{/* Play From Position Modal */}
|
||||||
{showPlayModal && (
|
{showPlayModal && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4 py-8">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4 py-8">
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { X, Loader2, Send, BookOpen } from "lucide-react";
|
||||||
|
import { OpeningMetadata } from "@/lib/openings";
|
||||||
|
import { getGenAIModel } from "@/lib/gemini";
|
||||||
|
import { ChatSession } from "@google/generative-ai";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { SupportedLanguage, translations } from "@/lib/i18n/translations";
|
||||||
|
import { Personality } from "@/lib/personalities";
|
||||||
|
|
||||||
|
interface OpeningsModalProps {
|
||||||
|
openings: OpeningMetadata[];
|
||||||
|
currentFen: string;
|
||||||
|
language: SupportedLanguage;
|
||||||
|
personality: Personality;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpeningExplanation {
|
||||||
|
content: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
chatSession?: ChatSession;
|
||||||
|
messages: { role: "user" | "assistant"; content: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OpeningsModal({
|
||||||
|
openings,
|
||||||
|
currentFen,
|
||||||
|
language,
|
||||||
|
personality,
|
||||||
|
onClose,
|
||||||
|
}: OpeningsModalProps) {
|
||||||
|
const t = translations[language];
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const [explanations, setExplanations] = useState<Record<number, OpeningExplanation>>({});
|
||||||
|
const [followUpInput, setFollowUpInput] = useState("");
|
||||||
|
const [isSending, setIsSending] = useState(false);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [explanations, activeTab]);
|
||||||
|
|
||||||
|
// Generate explanation when tab is clicked
|
||||||
|
const generateExplanation = async (index: number) => {
|
||||||
|
if (explanations[index]?.content || explanations[index]?.isLoading) return;
|
||||||
|
|
||||||
|
const opening = openings[index];
|
||||||
|
const apiKey = typeof window !== "undefined" ? localStorage.getItem("gemini_api_key") : null;
|
||||||
|
if (!apiKey) return;
|
||||||
|
|
||||||
|
setExplanations(prev => ({
|
||||||
|
...prev,
|
||||||
|
[index]: { content: "", isLoading: true, messages: [] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const model = getGenAIModel(apiKey);
|
||||||
|
const chat = model.startChat({
|
||||||
|
history: [],
|
||||||
|
generationConfig: { maxOutputTokens: 1024 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const prompt = `You are ${personality.name}, a chess coach with this style: "${personality.systemPrompt}".
|
||||||
|
|
||||||
|
The player is analyzing a game and has reached this position (FEN): ${currentFen}
|
||||||
|
|
||||||
|
The opening being played is: ${opening.name} (ECO: ${opening.eco})
|
||||||
|
Move sequence: ${opening.moves}
|
||||||
|
|
||||||
|
Please provide a brief but insightful explanation about this opening. Cover:
|
||||||
|
1. What strategy is White pursuing with this opening?
|
||||||
|
2. What is Black's typical response and counter-strategy?
|
||||||
|
3. How do these strategies pair against each other? Is this a good matchup for one side?
|
||||||
|
4. One interesting historical fact or famous game featuring this opening (keep it brief)
|
||||||
|
|
||||||
|
Be conversational and engaging, matching your personality. Keep your response concise (about 150-200 words).
|
||||||
|
Respond in ${language === 'de' ? 'German' : language === 'fr' ? 'French' : language === 'it' ? 'Italian' : 'English'}.`;
|
||||||
|
|
||||||
|
const result = await chat.sendMessage(prompt);
|
||||||
|
const responseText = result.response.text();
|
||||||
|
|
||||||
|
setExplanations(prev => ({
|
||||||
|
...prev,
|
||||||
|
[index]: {
|
||||||
|
content: responseText,
|
||||||
|
isLoading: false,
|
||||||
|
chatSession: chat,
|
||||||
|
messages: [{ role: "assistant", content: responseText }]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to generate opening explanation:", err);
|
||||||
|
setExplanations(prev => ({
|
||||||
|
...prev,
|
||||||
|
[index]: { content: "Failed to generate explanation. Please check your API key.", isLoading: false, messages: [] }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate explanation for first tab on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (openings.length > 0) {
|
||||||
|
generateExplanation(0);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle tab change
|
||||||
|
const handleTabChange = (index: number) => {
|
||||||
|
setActiveTab(index);
|
||||||
|
setFollowUpInput("");
|
||||||
|
generateExplanation(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle follow-up question
|
||||||
|
const handleFollowUp = async () => {
|
||||||
|
if (!followUpInput.trim() || isSending) return;
|
||||||
|
|
||||||
|
const currentExplanation = explanations[activeTab];
|
||||||
|
if (!currentExplanation?.chatSession) return;
|
||||||
|
|
||||||
|
const userMessage = followUpInput.trim();
|
||||||
|
setFollowUpInput("");
|
||||||
|
setIsSending(true);
|
||||||
|
|
||||||
|
// Add user message immediately
|
||||||
|
setExplanations(prev => ({
|
||||||
|
...prev,
|
||||||
|
[activeTab]: {
|
||||||
|
...prev[activeTab],
|
||||||
|
messages: [...prev[activeTab].messages, { role: "user", content: userMessage }]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await currentExplanation.chatSession.sendMessage(userMessage);
|
||||||
|
const responseText = result.response.text();
|
||||||
|
|
||||||
|
setExplanations(prev => ({
|
||||||
|
...prev,
|
||||||
|
[activeTab]: {
|
||||||
|
...prev[activeTab],
|
||||||
|
messages: [...prev[activeTab].messages, { role: "assistant", content: responseText }]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to send follow-up:", err);
|
||||||
|
} finally {
|
||||||
|
setIsSending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentOpening = openings[activeTab];
|
||||||
|
const currentExplanation = explanations[activeTab];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[85vh] flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BookOpen className="text-purple-600" size={24} />
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{t.analysis.openingsExplorer || "Opening Explorer"}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<X size={20} className="text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex overflow-x-auto border-b border-gray-200 dark:border-gray-700 px-4">
|
||||||
|
{openings.map((opening, index) => (
|
||||||
|
<button
|
||||||
|
key={`${opening.eco}-${index}`}
|
||||||
|
onClick={() => handleTabChange(index)}
|
||||||
|
className={`px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 transition-colors ${activeTab === index
|
||||||
|
? "border-purple-600 text-purple-600"
|
||||||
|
: "border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{opening.eco}: {opening.name.length > 20 ? opening.name.substring(0, 20) + "..." : opening.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
|
{currentOpening && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Opening Info */}
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||||
|
<h3 className="font-bold text-blue-900 dark:text-blue-100 text-lg">
|
||||||
|
{currentOpening.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-blue-700 dark:text-blue-300 mt-1">
|
||||||
|
ECO: {currentOpening.eco} • {currentOpening.moves}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages/Explanation */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{currentExplanation?.isLoading && !currentExplanation.content ? (
|
||||||
|
<div className="flex items-center gap-3 text-gray-600 dark:text-gray-300 py-8 justify-center">
|
||||||
|
<Loader2 className="animate-spin" size={24} />
|
||||||
|
<span>{t.analysis.generatingExplanation || "Generating explanation..."}</span>
|
||||||
|
</div>
|
||||||
|
) : currentExplanation?.messages.length > 0 ? (
|
||||||
|
currentExplanation.messages.map((msg, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`p-4 rounded-lg ${msg.role === "user"
|
||||||
|
? "bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 ml-8"
|
||||||
|
: "bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{msg.role === "user" ? (
|
||||||
|
<p className="text-gray-800 dark:text-gray-200">{msg.content}</p>
|
||||||
|
) : (
|
||||||
|
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||||
|
<ReactMarkdown>{msg.content}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 text-center py-8">
|
||||||
|
{t.analysis.noApiKey || "Please add an API key in settings to get opening explanations."}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Follow-up Input */}
|
||||||
|
{currentExplanation?.chatSession && (
|
||||||
|
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={followUpInput}
|
||||||
|
onChange={(e) => setFollowUpInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && handleFollowUp()}
|
||||||
|
placeholder={t.analysis.askFollowUp || "Ask a follow-up question about this opening..."}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
disabled={isSending}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleFollowUp}
|
||||||
|
disabled={!followUpInput.trim() || isSending}
|
||||||
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isSending ? <Loader2 className="animate-spin" size={18} /> : <Send size={18} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Settings, ChevronDown, ChevronUp, Brain, Trash2 } from "lucide-react";
|
import { Settings, ChevronDown, ChevronUp, Brain, Trash2, BarChart2 } from "lucide-react";
|
||||||
import { Personality, PERSONALITIES } from "@/lib/personalities";
|
import { Personality, PERSONALITIES } from "@/lib/personalities";
|
||||||
import { useTranslation } from "@/lib/i18n/useTranslation";
|
import { useTranslation } from "@/lib/i18n/useTranslation";
|
||||||
import { SupportedLanguage } from "@/lib/i18n/translations";
|
import { SupportedLanguage } from "@/lib/i18n/translations";
|
||||||
@@ -145,16 +145,32 @@ export default function StartScreen({ onStartGame, onResumeGame, savedGames, onD
|
|||||||
onClick={() => onResumeGame(game)}
|
onClick={() => onResumeGame(game)}
|
||||||
className="group relative bg-gray-50 dark:bg-gray-700 p-4 rounded-xl border border-gray-200 dark:border-gray-600 hover:border-blue-400 dark:hover:border-blue-300 shadow-sm hover:shadow-md transition-all cursor-pointer"
|
className="group relative bg-gray-50 dark:bg-gray-700 p-4 rounded-xl border border-gray-200 dark:border-gray-600 hover:border-blue-400 dark:hover:border-blue-300 shadow-sm hover:shadow-md transition-all cursor-pointer"
|
||||||
>
|
>
|
||||||
<button
|
<div className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
onClick={(e) => {
|
<button
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
onDeleteSavedGame(game.id);
|
e.stopPropagation();
|
||||||
}}
|
// Navigate to analysis with this game's PGN
|
||||||
aria-label={t.start.deleteGame}
|
if (game.pgn) {
|
||||||
className="absolute top-2 right-2 p-2 rounded-full bg-white dark:bg-gray-800 text-gray-500 hover:text-red-600 shadow opacity-0 group-hover:opacity-100 transition-opacity"
|
localStorage.setItem('chess_tutor_pending_analysis', game.pgn);
|
||||||
>
|
}
|
||||||
<Trash2 size={16} />
|
router.push('/analysis');
|
||||||
</button>
|
}}
|
||||||
|
aria-label={t.start.analyzeThisGame}
|
||||||
|
className="p-2 rounded-full bg-white dark:bg-gray-800 text-gray-500 hover:text-purple-600 shadow"
|
||||||
|
>
|
||||||
|
<BarChart2 size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteSavedGame(game.id);
|
||||||
|
}}
|
||||||
|
aria-label={t.start.deleteGame}
|
||||||
|
className="p-2 rounded-full bg-white dark:bg-gray-800 text-gray-500 hover:text-red-600 shadow"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="bg-[#779954] p-[2px] rounded-sm">
|
<div className="bg-[#779954] p-[2px] rounded-sm">
|
||||||
<Chessboard
|
<Chessboard
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export interface Translations {
|
|||||||
evaluationLabel: string;
|
evaluationLabel: string;
|
||||||
noEvaluation: string;
|
noEvaluation: string;
|
||||||
deleteGame: string;
|
deleteGame: string;
|
||||||
|
analyzeThisGame: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Game
|
// Game
|
||||||
@@ -120,6 +121,12 @@ export interface Translations {
|
|||||||
enginePending: string;
|
enginePending: string;
|
||||||
coachPending: string;
|
coachPending: string;
|
||||||
importError: string;
|
importError: string;
|
||||||
|
loadNewGame: string;
|
||||||
|
openingsExplorer: string;
|
||||||
|
generatingExplanation: string;
|
||||||
|
noApiKey: string;
|
||||||
|
askFollowUp: string;
|
||||||
|
possibleOpenings: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// API Key Input
|
// API Key Input
|
||||||
@@ -209,6 +216,7 @@ const en: Translations = {
|
|||||||
evaluationLabel: 'Evaluation',
|
evaluationLabel: 'Evaluation',
|
||||||
noEvaluation: 'No evaluation yet',
|
noEvaluation: 'No evaluation yet',
|
||||||
deleteGame: 'Delete game',
|
deleteGame: 'Delete game',
|
||||||
|
analyzeThisGame: 'Analyze this game',
|
||||||
},
|
},
|
||||||
game: {
|
game: {
|
||||||
playingAs: 'Playing as',
|
playingAs: 'Playing as',
|
||||||
@@ -273,6 +281,12 @@ const en: Translations = {
|
|||||||
enginePending: 'Running engine evaluation...',
|
enginePending: 'Running engine evaluation...',
|
||||||
coachPending: 'Coach is preparing feedback...',
|
coachPending: 'Coach is preparing feedback...',
|
||||||
importError: 'Could not load that PGN or FEN. Please check the notation.',
|
importError: 'Could not load that PGN or FEN. Please check the notation.',
|
||||||
|
loadNewGame: 'Load New Game',
|
||||||
|
openingsExplorer: 'Opening Explorer',
|
||||||
|
generatingExplanation: 'Generating explanation...',
|
||||||
|
noApiKey: 'Please add an API key in settings to get opening explanations.',
|
||||||
|
askFollowUp: 'Ask a follow-up question about this opening...',
|
||||||
|
possibleOpenings: 'possible openings',
|
||||||
},
|
},
|
||||||
apiKeyInput: {
|
apiKeyInput: {
|
||||||
title: 'API Key Required',
|
title: 'API Key Required',
|
||||||
@@ -363,6 +377,7 @@ const de: Translations = {
|
|||||||
evaluationLabel: 'Bewertung',
|
evaluationLabel: 'Bewertung',
|
||||||
noEvaluation: 'Keine Bewertung',
|
noEvaluation: 'Keine Bewertung',
|
||||||
deleteGame: 'Partie löschen',
|
deleteGame: 'Partie löschen',
|
||||||
|
analyzeThisGame: 'Diese Partie analysieren',
|
||||||
},
|
},
|
||||||
game: {
|
game: {
|
||||||
playingAs: 'Spielst als',
|
playingAs: 'Spielst als',
|
||||||
@@ -427,6 +442,12 @@ const de: Translations = {
|
|||||||
enginePending: 'Engine-Bewertung läuft...',
|
enginePending: 'Engine-Bewertung läuft...',
|
||||||
coachPending: 'Coach bereitet Feedback vor...',
|
coachPending: 'Coach bereitet Feedback vor...',
|
||||||
importError: 'PGN oder FEN konnte nicht geladen werden. Bitte Notation prüfen.',
|
importError: 'PGN oder FEN konnte nicht geladen werden. Bitte Notation prüfen.',
|
||||||
|
loadNewGame: 'Neue Partie laden',
|
||||||
|
openingsExplorer: 'Eröffnungs-Explorer',
|
||||||
|
generatingExplanation: 'Erklärung wird generiert...',
|
||||||
|
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',
|
||||||
},
|
},
|
||||||
apiKeyInput: {
|
apiKeyInput: {
|
||||||
title: 'API-Schlüssel erforderlich',
|
title: 'API-Schlüssel erforderlich',
|
||||||
@@ -517,6 +538,7 @@ const fr: Translations = {
|
|||||||
evaluationLabel: 'Évaluation',
|
evaluationLabel: 'Évaluation',
|
||||||
noEvaluation: 'Pas d\'évaluation',
|
noEvaluation: 'Pas d\'évaluation',
|
||||||
deleteGame: 'Supprimer la partie',
|
deleteGame: 'Supprimer la partie',
|
||||||
|
analyzeThisGame: 'Analyser cette partie',
|
||||||
},
|
},
|
||||||
game: {
|
game: {
|
||||||
playingAs: 'Jouant',
|
playingAs: 'Jouant',
|
||||||
@@ -581,6 +603,12 @@ const fr: Translations = {
|
|||||||
enginePending: 'Évaluation du moteur en cours...',
|
enginePending: 'Évaluation du moteur en cours...',
|
||||||
coachPending: 'Le coach prépare son retour...',
|
coachPending: 'Le coach prépare son retour...',
|
||||||
importError: 'Impossible de charger ce PGN ou FEN. Merci de vérifier la notation.',
|
importError: 'Impossible de charger ce PGN ou FEN. Merci de vérifier la notation.',
|
||||||
|
loadNewGame: 'Charger nouvelle partie',
|
||||||
|
openingsExplorer: 'Explorateur d\'ouvertures',
|
||||||
|
generatingExplanation: 'Génération de l\'explication...',
|
||||||
|
noApiKey: 'Veuillez ajouter une clé API dans les paramètres.',
|
||||||
|
askFollowUp: 'Posez une question sur cette ouverture...',
|
||||||
|
possibleOpenings: 'ouvertures possibles',
|
||||||
},
|
},
|
||||||
apiKeyInput: {
|
apiKeyInput: {
|
||||||
title: 'Clé API requise',
|
title: 'Clé API requise',
|
||||||
@@ -671,6 +699,7 @@ const it: Translations = {
|
|||||||
evaluationLabel: 'Valutazione',
|
evaluationLabel: 'Valutazione',
|
||||||
noEvaluation: 'Nessuna valutazione',
|
noEvaluation: 'Nessuna valutazione',
|
||||||
deleteGame: 'Elimina partita',
|
deleteGame: 'Elimina partita',
|
||||||
|
analyzeThisGame: 'Analizza questa partita',
|
||||||
},
|
},
|
||||||
game: {
|
game: {
|
||||||
playingAs: 'Giocando',
|
playingAs: 'Giocando',
|
||||||
@@ -735,6 +764,12 @@ const it: Translations = {
|
|||||||
enginePending: 'Valutazione del motore in corso...',
|
enginePending: 'Valutazione del motore in corso...',
|
||||||
coachPending: 'Il coach sta preparando il feedback...',
|
coachPending: 'Il coach sta preparando il feedback...',
|
||||||
importError: 'Impossibile caricare questo PGN o FEN. Controlla la notazione.',
|
importError: 'Impossibile caricare questo PGN o FEN. Controlla la notazione.',
|
||||||
|
loadNewGame: 'Carica nuova partita',
|
||||||
|
openingsExplorer: 'Esplora aperture',
|
||||||
|
generatingExplanation: 'Generazione spiegazione...',
|
||||||
|
noApiKey: 'Aggiungi una chiave API nelle impostazioni.',
|
||||||
|
askFollowUp: 'Fai una domanda su questa apertura...',
|
||||||
|
possibleOpenings: 'aperture possibili',
|
||||||
},
|
},
|
||||||
apiKeyInput: {
|
apiKeyInput: {
|
||||||
title: 'Chiave API richiesta',
|
title: 'Chiave API richiesta',
|
||||||
|
|||||||
Reference in New Issue
Block a user