Merge remote changes and add evaluation bar to analysis page
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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { DebugProvider } from "@/contexts/DebugContext";
|
||||||
|
|
||||||
jest.mock("next/navigation", () => ({
|
jest.mock("next/navigation", () => ({
|
||||||
useRouter: jest.fn(() => ({
|
useRouter: jest.fn(() => ({
|
||||||
@@ -83,7 +84,11 @@ describe("AnalysisPage", () => {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const loadGame = () => {
|
const loadGame = () => {
|
||||||
render(<AnalysisPage />);
|
render(
|
||||||
|
<DebugProvider>
|
||||||
|
<AnalysisPage />
|
||||||
|
</DebugProvider>
|
||||||
|
);
|
||||||
const textarea = screen.getByPlaceholderText(/Paste PGN or FEN here/i);
|
const textarea = screen.getByPlaceholderText(/Paste PGN or FEN here/i);
|
||||||
fireEvent.change(textarea, { target: { value: samplePgn } });
|
fireEvent.change(textarea, { target: { value: samplePgn } });
|
||||||
fireEvent.click(screen.getByText(/Start Analysis/i));
|
fireEvent.click(screen.getByText(/Start Analysis/i));
|
||||||
@@ -125,7 +130,11 @@ describe("AnalysisPage", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("shows an error when the notation cannot be parsed", () => {
|
it("shows an error when the notation cannot be parsed", () => {
|
||||||
render(<AnalysisPage />);
|
render(
|
||||||
|
<DebugProvider>
|
||||||
|
<AnalysisPage />
|
||||||
|
</DebugProvider>
|
||||||
|
);
|
||||||
const textarea = screen.getByPlaceholderText(/Paste PGN or FEN here/i);
|
const textarea = screen.getByPlaceholderText(/Paste PGN or FEN here/i);
|
||||||
fireEvent.change(textarea, { target: { value: "invalid" } });
|
fireEvent.change(textarea, { target: { value: "invalid" } });
|
||||||
fireEvent.click(screen.getByText(/Start Analysis/i));
|
fireEvent.click(screen.getByText(/Start Analysis/i));
|
||||||
|
|||||||
+247
-30
@@ -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 } 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";
|
||||||
@@ -20,6 +20,7 @@ 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 { EvaluationBar } from "@/components/EvaluationBar";
|
import { EvaluationBar } from "@/components/EvaluationBar";
|
||||||
|
import { OpeningsModal } from "@/components/OpeningsModal";
|
||||||
|
|
||||||
interface MoveStep {
|
interface MoveStep {
|
||||||
san: string;
|
san: string;
|
||||||
@@ -65,14 +66,35 @@ export default function AnalysisPage() {
|
|||||||
const [comments, setComments] = useState<Record<number, string>>({});
|
const [comments, setComments] = useState<Record<number, string>>({});
|
||||||
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 [showOpeningsModal, setShowOpeningsModal] = useState(false);
|
||||||
|
const [playPersonality, setPlayPersonality] = useState<Personality>(PERSONALITIES[0]);
|
||||||
|
const [playColor, setPlayColor] = useState<"white" | "black">("white");
|
||||||
|
const [playStrength, setPlayStrength] = useState(15);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedKey = localStorage.getItem("gemini_api_key");
|
const storedKey = localStorage.getItem("gemini_api_key");
|
||||||
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(() => {
|
||||||
|
setPlayPersonality(selectedPersonality);
|
||||||
|
}, [selectedPersonality]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sf = new Stockfish();
|
const sf = new Stockfish();
|
||||||
setStockfish(sf);
|
setStockfish(sf);
|
||||||
@@ -219,6 +241,31 @@ 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 payload = {
|
||||||
|
fen: currentFen,
|
||||||
|
personalityId: playPersonality.id,
|
||||||
|
color: playColor,
|
||||||
|
stockfishDepth: playStrength,
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem("chess_tutor_pending_game", JSON.stringify(payload));
|
||||||
|
router.push("/");
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!stockfish || !currentFen) return;
|
if (!stockfish || !currentFen) return;
|
||||||
ensureEvaluation(currentFen);
|
ensureEvaluation(currentFen);
|
||||||
@@ -381,6 +428,8 @@ 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">
|
||||||
|
{/* Phase 1: Import View - shown when no game is loaded */}
|
||||||
|
{steps.length === 0 && (
|
||||||
<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">
|
<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">
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -389,17 +438,6 @@ INSTRUCTIONS:
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 dark:text-gray-300 mt-2">{t.analysis.modeDescription}</p>
|
<p className="text-gray-600 dark:text-gray-300 mt-2">{t.analysis.modeDescription}</p>
|
||||||
</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>
|
|
||||||
<select
|
|
||||||
value={orientation}
|
|
||||||
onChange={(e) => setOrientation(e.target.value as "white" | "black")}
|
|
||||||
className="p-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700"
|
|
||||||
>
|
|
||||||
<option value="white">White</option>
|
|
||||||
<option value="black">Black</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
@@ -463,7 +501,7 @@ INSTRUCTIONS:
|
|||||||
</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="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">
|
<div className="w-full max-w-md">
|
||||||
<Chessboard
|
<Chessboard
|
||||||
options={{
|
options={{
|
||||||
@@ -477,42 +515,112 @@ INSTRUCTIONS:
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="mt-4 text-sm text-gray-500 dark:text-gray-400 text-center">
|
||||||
|
{t.analysis.currentPosition}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Phase 2: Analysis View - shown when game is loaded */}
|
||||||
|
{steps.length > 0 && (
|
||||||
|
<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">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-2xl">{selectedPersonality.image}</span>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{t.analysis.modeTitle}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Coach: {selectedPersonality.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<RotateCcw size={16} />
|
||||||
|
{t.analysis.loadNewGame}
|
||||||
|
</button>
|
||||||
|
<select
|
||||||
|
value={orientation}
|
||||||
|
onChange={(e) => setOrientation(e.target.value as "white" | "black")}
|
||||||
|
className="p-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 text-sm"
|
||||||
|
>
|
||||||
|
<option value="white">White</option>
|
||||||
|
<option value="black">Black</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div className="w-full max-w-lg">
|
||||||
|
<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>
|
||||||
<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>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setPlayColor(orientation);
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<PlayCircle size={18} /> {t.analysis.playFromHere}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{/* Analysis Panels - only shown when game is loaded */}
|
||||||
|
{steps.length > 0 && (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<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">
|
<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">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">{t.analysis.title}</h2>
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white">{t.analysis.title}</h2>
|
||||||
{possibleOpenings.length > 0 && (
|
{possibleOpenings.length > 0 && (
|
||||||
<span className="text-sm text-blue-700 dark:text-blue-300">
|
<button
|
||||||
|
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"
|
||||||
|
>
|
||||||
{t.analysis.opening}: {possibleOpenings.length === 1
|
{t.analysis.opening}: {possibleOpenings.length === 1
|
||||||
? `${possibleOpenings[0].name} (${possibleOpenings[0].eco})`
|
? `${possibleOpenings[0].name} (${possibleOpenings[0].eco})`
|
||||||
: `${possibleOpenings.length} possible openings`
|
: `${possibleOpenings.length} ${t.analysis.possibleOpenings}`
|
||||||
}
|
}
|
||||||
</span>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{currentIndex === 0 ? (
|
{currentIndex === 0 ? (
|
||||||
@@ -538,16 +646,6 @@ INSTRUCTIONS:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Horizontal Evaluation Bar */}
|
|
||||||
<div className="w-full">
|
|
||||||
<EvaluationBar
|
|
||||||
score={currentDetails?.evalAfter?.score}
|
|
||||||
mate={currentDetails?.evalAfter?.mate}
|
|
||||||
isPlayerWhite={true}
|
|
||||||
orientation="horizontal"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold text-gray-800 dark:text-gray-100 mb-2">{t.analysis.missedTactics}</div>
|
<div className="font-semibold text-gray-800 dark:text-gray-100 mb-2">{t.analysis.missedTactics}</div>
|
||||||
{tacticSummary.length === 0 && (
|
{tacticSummary.length === 0 && (
|
||||||
@@ -571,6 +669,19 @@ INSTRUCTIONS:
|
|||||||
|
|
||||||
<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="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>
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white">{t.analysis.aiAnalysis}</h2>
|
||||||
|
|
||||||
|
{/* Horizontal Evaluation Bar */}
|
||||||
|
{currentIndex > 0 && currentDetails?.evalAfter && (
|
||||||
|
<div className="w-full mb-4">
|
||||||
|
<EvaluationBar
|
||||||
|
score={currentDetails.evalAfter.score}
|
||||||
|
mate={currentDetails.evalAfter.mate}
|
||||||
|
isPlayerWhite={true}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!apiKey && (
|
{!apiKey && (
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Please add an API key in settings to receive commentary.</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">Please add an API key in settings to receive commentary.</p>
|
||||||
)}
|
)}
|
||||||
@@ -597,6 +708,7 @@ INSTRUCTIONS:
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -608,6 +720,111 @@ INSTRUCTIONS:
|
|||||||
language={language}
|
language={language}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Openings Explorer Modal */}
|
||||||
|
{showOpeningsModal && possibleOpenings.length > 0 && (
|
||||||
|
<OpeningsModal
|
||||||
|
openings={possibleOpenings}
|
||||||
|
currentFen={currentFen}
|
||||||
|
language={language}
|
||||||
|
personality={selectedPersonality}
|
||||||
|
onClose={() => setShowOpeningsModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Play From Position Modal */}
|
||||||
|
{showPlayModal && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4 py-8">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl max-w-2xl w-full p-6 space-y-6 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white">{t.analysis.playFromHere}</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1">{t.analysis.playDescription}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPlayModal(false)}
|
||||||
|
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-200"
|
||||||
|
aria-label={t.common.close}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-gray-800 dark:text-gray-100 mb-2">{t.analysis.chooseOpponent}</p>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{PERSONALITIES.map(p => (
|
||||||
|
<button
|
||||||
|
key={p.id}
|
||||||
|
onClick={() => setPlayPersonality(p)}
|
||||||
|
className={`p-3 rounded-lg border flex items-center gap-2 ${playPersonality.id === p.id
|
||||||
|
? "border-blue-500 bg-blue-50 dark:bg-blue-900/30"
|
||||||
|
: "border-gray-200 dark:border-gray-700"}`}
|
||||||
|
>
|
||||||
|
<span className="text-xl">{p.image}</span>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-sm font-semibold text-gray-900 dark:text-white">{p.name}</div>
|
||||||
|
<div className="text-xs text-gray-600 dark:text-gray-300 line-clamp-2">{p.description}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-semibold text-gray-800 dark:text-gray-100">{t.analysis.chooseSide}</p>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{(["white", "black"] as const).map(color => (
|
||||||
|
<button
|
||||||
|
key={color}
|
||||||
|
onClick={() => setPlayColor(color)}
|
||||||
|
className={`py-2 px-3 rounded-lg border text-sm font-medium ${playColor === color
|
||||||
|
? "border-blue-500 bg-blue-50 dark:bg-blue-900/30"
|
||||||
|
: "border-gray-200 dark:border-gray-700"}`}
|
||||||
|
>
|
||||||
|
{color === "white" ? t.game.white : t.game.black}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-semibold text-gray-800 dark:text-gray-100">{t.analysis.chooseStrength}</p>
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg p-3">
|
||||||
|
<div className="text-sm text-gray-700 dark:text-gray-200 mb-1">{t.game.stockfishStrength}: {playStrength}</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="20"
|
||||||
|
value={playStrength}
|
||||||
|
onChange={(e) => setPlayStrength(parseInt(e.target.value))}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<div className="text-[11px] text-gray-500 dark:text-gray-400 mt-1">{t.game.depth}: {playStrength}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPlayModal(false)}
|
||||||
|
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
{t.common.cancel}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleStartGameFromPosition}
|
||||||
|
className="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 shadow"
|
||||||
|
>
|
||||||
|
{t.analysis.startPlay}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-1
@@ -4,7 +4,7 @@ import { useState, useEffect } from "react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import ChessGame from "@/components/ChessGame";
|
import ChessGame from "@/components/ChessGame";
|
||||||
import StartScreen from "@/components/StartScreen";
|
import StartScreen from "@/components/StartScreen";
|
||||||
import { Personality } from "@/lib/personalities";
|
import { Personality, PERSONALITIES } from "@/lib/personalities";
|
||||||
import { SavedGame, deleteSavedGame, loadSavedGames } from "@/lib/savedGames";
|
import { SavedGame, deleteSavedGame, loadSavedGames } from "@/lib/savedGames";
|
||||||
|
|
||||||
type ViewState = 'start' | 'game';
|
type ViewState = 'start' | 'game';
|
||||||
@@ -21,6 +21,7 @@ export default function Home() {
|
|||||||
initialPgn?: string;
|
initialPgn?: string;
|
||||||
initialPersonality: Personality;
|
initialPersonality: Personality;
|
||||||
initialColor: 'white' | 'black';
|
initialColor: 'white' | 'black';
|
||||||
|
initialStockfishDepth?: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const [savedGames, setSavedGames] = useState<SavedGame[]>([]);
|
const [savedGames, setSavedGames] = useState<SavedGame[]>([]);
|
||||||
@@ -35,6 +36,33 @@ export default function Home() {
|
|||||||
|
|
||||||
setSavedGames(loadSavedGames());
|
setSavedGames(loadSavedGames());
|
||||||
|
|
||||||
|
const pendingGameRaw = localStorage.getItem("chess_tutor_pending_game");
|
||||||
|
if (pendingGameRaw) {
|
||||||
|
try {
|
||||||
|
const pendingGame = JSON.parse(pendingGameRaw) as {
|
||||||
|
fen: string;
|
||||||
|
personalityId: string;
|
||||||
|
color: 'white' | 'black';
|
||||||
|
stockfishDepth?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const personality = PERSONALITIES.find(p => p.id === pendingGame.personalityId) || PERSONALITIES[0];
|
||||||
|
|
||||||
|
setGameProps({
|
||||||
|
gameId: crypto.randomUUID ? crypto.randomUUID() : `game-${Date.now()}`,
|
||||||
|
initialFen: pendingGame.fen,
|
||||||
|
initialPersonality: personality,
|
||||||
|
initialColor: pendingGame.color,
|
||||||
|
initialStockfishDepth: pendingGame.stockfishDepth,
|
||||||
|
});
|
||||||
|
setView('game');
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load pending game", err);
|
||||||
|
} finally {
|
||||||
|
localStorage.removeItem("chess_tutor_pending_game");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
@@ -98,6 +126,7 @@ export default function Home() {
|
|||||||
initialPgn={gameProps.initialPgn}
|
initialPgn={gameProps.initialPgn}
|
||||||
initialPersonality={gameProps.initialPersonality}
|
initialPersonality={gameProps.initialPersonality}
|
||||||
initialColor={gameProps.initialColor}
|
initialColor={gameProps.initialColor}
|
||||||
|
initialStockfishDepth={gameProps.initialStockfishDepth}
|
||||||
onBack={handleBackToMenu}
|
onBack={handleBackToMenu}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ interface ChessGameProps {
|
|||||||
initialPgn?: string;
|
initialPgn?: string;
|
||||||
initialPersonality: Personality;
|
initialPersonality: Personality;
|
||||||
initialColor: 'white' | 'black';
|
initialColor: 'white' | 'black';
|
||||||
|
initialStockfishDepth?: number;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ const PIECE_VALUES: Record<string, number> = {
|
|||||||
'k': 0
|
'k': 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ChessGame({ gameId, initialFen, initialPgn, initialPersonality, initialColor, onBack }: ChessGameProps) {
|
export default function ChessGame({ gameId, initialFen, initialPgn, initialPersonality, initialColor, initialStockfishDepth, onBack }: ChessGameProps) {
|
||||||
const gameRef = useRef(new Chess(initialFen || "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"));
|
const gameRef = useRef(new Chess(initialFen || "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"));
|
||||||
const [fen, setFen] = useState(gameRef.current.fen());
|
const [fen, setFen] = useState(gameRef.current.fen());
|
||||||
const [stockfish, setStockfish] = useState<Stockfish | null>(null);
|
const [stockfish, setStockfish] = useState<Stockfish | null>(null);
|
||||||
@@ -55,7 +56,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
|
|||||||
const [computerMove, setComputerMove] = useState<Move | null>(null);
|
const [computerMove, setComputerMove] = useState<Move | null>(null);
|
||||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||||
const [apiKey, setApiKey] = useState<string | null>(null);
|
const [apiKey, setApiKey] = useState<string | null>(null);
|
||||||
const [stockfishDepth, setStockfishDepth] = useState(15);
|
const [stockfishDepth, setStockfishDepth] = useState(initialStockfishDepth ?? 15);
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
const [language, setLanguage] = useState<SupportedLanguage>('en');
|
const [language, setLanguage] = useState<SupportedLanguage>('en');
|
||||||
@@ -108,6 +109,12 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
|
|||||||
return () => sf.terminate();
|
return () => sf.terminate();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof initialStockfishDepth === 'number') {
|
||||||
|
setStockfishDepth(initialStockfishDepth);
|
||||||
|
}
|
||||||
|
}, [initialStockfishDepth]);
|
||||||
|
|
||||||
// Load Settings & Initial State
|
// Load Settings & Initial State
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedKey = localStorage.getItem("gemini_api_key");
|
const storedKey = localStorage.getItem("gemini_api_key");
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
<div className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Navigate to analysis with this game's PGN
|
||||||
|
if (game.pgn) {
|
||||||
|
localStorage.setItem('chess_tutor_pending_analysis', game.pgn);
|
||||||
|
}
|
||||||
|
router.push('/analysis');
|
||||||
|
}}
|
||||||
|
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
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDeleteSavedGame(game.id);
|
onDeleteSavedGame(game.id);
|
||||||
}}
|
}}
|
||||||
aria-label={t.start.deleteGame}
|
aria-label={t.start.deleteGame}
|
||||||
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"
|
className="p-2 rounded-full bg-white dark:bg-gray-800 text-gray-500 hover:text-red-600 shadow"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</button>
|
</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
|
||||||
@@ -97,6 +98,12 @@ export interface Translations {
|
|||||||
evaluation: string;
|
evaluation: string;
|
||||||
bestMove: string;
|
bestMove: string;
|
||||||
aiAnalysis: string;
|
aiAnalysis: string;
|
||||||
|
playFromHere: string;
|
||||||
|
playDescription: string;
|
||||||
|
chooseOpponent: string;
|
||||||
|
chooseSide: string;
|
||||||
|
chooseStrength: string;
|
||||||
|
startPlay: string;
|
||||||
modeTitle: string;
|
modeTitle: string;
|
||||||
modeDescription: string;
|
modeDescription: string;
|
||||||
pasteLabel: string;
|
pasteLabel: string;
|
||||||
@@ -114,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
|
||||||
@@ -203,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',
|
||||||
@@ -244,6 +258,12 @@ const en: Translations = {
|
|||||||
evaluation: 'Evaluation',
|
evaluation: 'Evaluation',
|
||||||
bestMove: 'Best Move',
|
bestMove: 'Best Move',
|
||||||
aiAnalysis: 'AI Analysis',
|
aiAnalysis: 'AI Analysis',
|
||||||
|
playFromHere: 'Play from this position',
|
||||||
|
playDescription: 'Pick a character, side, and engine strength to continue playing from the current move.',
|
||||||
|
chooseOpponent: 'Choose your opponent',
|
||||||
|
chooseSide: 'Choose your color',
|
||||||
|
chooseStrength: 'Opponent strength',
|
||||||
|
startPlay: 'Start from here',
|
||||||
modeTitle: 'Analyze an Existing Game',
|
modeTitle: 'Analyze an Existing Game',
|
||||||
modeDescription: 'Upload a PGN or FEN and let your coach walk you through every move with engine-backed insights.',
|
modeDescription: 'Upload a PGN or FEN and let your coach walk you through every move with engine-backed insights.',
|
||||||
pasteLabel: 'PGN or FEN Input',
|
pasteLabel: 'PGN or FEN Input',
|
||||||
@@ -261,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',
|
||||||
@@ -351,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',
|
||||||
@@ -392,6 +419,12 @@ const de: Translations = {
|
|||||||
evaluation: 'Bewertung',
|
evaluation: 'Bewertung',
|
||||||
bestMove: 'Bester Zug',
|
bestMove: 'Bester Zug',
|
||||||
aiAnalysis: 'KI-Analyse',
|
aiAnalysis: 'KI-Analyse',
|
||||||
|
playFromHere: 'Von dieser Stellung spielen',
|
||||||
|
playDescription: 'Wähle Charakter, Farbe und Engine-Stärke, um ab dem aktuellen Zug weiterzuspielen.',
|
||||||
|
chooseOpponent: 'Gegner auswählen',
|
||||||
|
chooseSide: 'Wähle deine Farbe',
|
||||||
|
chooseStrength: 'Stärke des Gegners',
|
||||||
|
startPlay: 'Hier weiterspielen',
|
||||||
modeTitle: 'Bestehende Partie analysieren',
|
modeTitle: 'Bestehende Partie analysieren',
|
||||||
modeDescription: 'PGN oder FEN hochladen und vom Coach mit Engine-Unterstützung durch die Partie führen lassen.',
|
modeDescription: 'PGN oder FEN hochladen und vom Coach mit Engine-Unterstützung durch die Partie führen lassen.',
|
||||||
pasteLabel: 'PGN- oder FEN-Eingabe',
|
pasteLabel: 'PGN- oder FEN-Eingabe',
|
||||||
@@ -409,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',
|
||||||
@@ -499,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',
|
||||||
@@ -540,6 +580,12 @@ const fr: Translations = {
|
|||||||
evaluation: 'Évaluation',
|
evaluation: 'Évaluation',
|
||||||
bestMove: 'Meilleur coup',
|
bestMove: 'Meilleur coup',
|
||||||
aiAnalysis: 'Analyse IA',
|
aiAnalysis: 'Analyse IA',
|
||||||
|
playFromHere: 'Jouer depuis cette position',
|
||||||
|
playDescription: 'Choisissez un personnage, une couleur et la force du moteur pour continuer depuis ce coup.',
|
||||||
|
chooseOpponent: 'Choisir votre adversaire',
|
||||||
|
chooseSide: 'Choisissez votre couleur',
|
||||||
|
chooseStrength: 'Force de l’adversaire',
|
||||||
|
startPlay: 'Commencer ici',
|
||||||
modeTitle: 'Analyser une partie existante',
|
modeTitle: 'Analyser une partie existante',
|
||||||
modeDescription: 'Importez un PGN ou un FEN et laissez le coach commenter chaque coup avec l’aide du moteur.',
|
modeDescription: 'Importez un PGN ou un FEN et laissez le coach commenter chaque coup avec l’aide du moteur.',
|
||||||
pasteLabel: 'Saisie PGN ou FEN',
|
pasteLabel: 'Saisie PGN ou FEN',
|
||||||
@@ -557,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',
|
||||||
@@ -647,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',
|
||||||
@@ -688,6 +741,12 @@ const it: Translations = {
|
|||||||
evaluation: 'Valutazione',
|
evaluation: 'Valutazione',
|
||||||
bestMove: 'Mossa migliore',
|
bestMove: 'Mossa migliore',
|
||||||
aiAnalysis: 'Analisi IA',
|
aiAnalysis: 'Analisi IA',
|
||||||
|
playFromHere: 'Gioca da questa posizione',
|
||||||
|
playDescription: 'Scegli personaggio, colore e forza del motore per continuare da questa mossa.',
|
||||||
|
chooseOpponent: 'Scegli l’avversario',
|
||||||
|
chooseSide: 'Scegli il tuo colore',
|
||||||
|
chooseStrength: 'Forza dell’avversario',
|
||||||
|
startPlay: 'Inizia da qui',
|
||||||
modeTitle: 'Analizza una partita esistente',
|
modeTitle: 'Analizza una partita esistente',
|
||||||
modeDescription: 'Carica un PGN o un FEN e lascia che il coach commenti ogni mossa con il supporto del motore.',
|
modeDescription: 'Carica un PGN o un FEN e lascia che il coach commenti ogni mossa con il supporto del motore.',
|
||||||
pasteLabel: 'Input PGN o FEN',
|
pasteLabel: 'Input PGN o FEN',
|
||||||
@@ -705,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