This commit is contained in:
Stefan
2025-11-27 21:23:54 +01:00
parent 502f7e28ea
commit 3e8c16b9c2
4 changed files with 590 additions and 12 deletions
+66 -12
View File
@@ -3,7 +3,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Chess } from "chess.js";
import { Chessboard } from "react-chessboard";
import { Brain, ChevronLeft, ChevronRight, Loader2, ArrowLeft } from "lucide-react";
import { Brain, ChevronLeft, ChevronRight, Loader2, ArrowLeft, Download } from "lucide-react";
import { useRouter } from "next/navigation";
import Header from "@/components/Header";
@@ -18,6 +18,7 @@ import { getGenAIModel } from "@/lib/gemini";
import { ChatSession } from "@google/generative-ai";
import ReactMarkdown from "react-markdown";
import { useDebug } from "@/contexts/DebugContext";
import { GameImportModal } from "@/components/GameImportModal";
interface MoveStep {
san: string;
@@ -62,6 +63,7 @@ export default function AnalysisPage() {
const [isCommenting, setIsCommenting] = useState(false);
const [comments, setComments] = useState<Record<number, string>>({});
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
const [showImportModal, setShowImportModal] = useState(false);
useEffect(() => {
const storedKey = localStorage.getItem("gemini_api_key");
@@ -144,6 +146,18 @@ IMPORTANT:
return;
}
loadGameFromPgnOrFen(trimmed);
};
const loadGameFromPgnOrFen = (notation: string) => {
const trimmed = notation.trim();
const format = detectChessFormat(trimmed);
if (!trimmed || format === "invalid") {
setError(t.analysis.importError);
return;
}
try {
const parsedGame = new Chess();
const nextSteps: MoveStep[] = [];
@@ -198,6 +212,12 @@ IMPORTANT:
}
};
const handleImportGame = (pgn: string) => {
setInput(pgn);
setDetectedFormat(detectChessFormat(pgn));
loadGameFromPgnOrFen(pgn);
};
useEffect(() => {
if (!stockfish || !currentFen) return;
ensureEvaluation(currentFen);
@@ -417,20 +437,45 @@ INSTRUCTIONS:
>
{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 gap-3 border border-gray-200 dark:border-gray-700">
<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 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>
<div className="flex items-center gap-4">
<button
onClick={() => setCurrentIndex(i => Math.max(0, i - 1))}
@@ -543,6 +588,15 @@ INSTRUCTIONS:
</div>
</div>
</main>
{/* Game Import Modal */}
{showImportModal && (
<GameImportModal
onClose={() => setShowImportModal(false)}
onSelectGame={handleImportGame}
language={language}
/>
)}
</div>
);
}
+59
View File
@@ -11,15 +11,21 @@ export default function SettingsPage() {
const router = useRouter();
const [apiKey, setApiKey] = useState("");
const [language, setLanguage] = useState<SupportedLanguage>('en');
const [chesscomUsername, setChesscomUsername] = useState("");
const [lichessUsername, setLichessUsername] = useState("");
const [mounted, setMounted] = useState(false);
// Load settings on mount
useEffect(() => {
const storedKey = localStorage.getItem("gemini_api_key");
const storedLang = localStorage.getItem("chess_tutor_language");
const storedChesscomUsername = localStorage.getItem("chesscom_username");
const storedLichessUsername = localStorage.getItem("lichess_username");
if (storedKey) setApiKey(storedKey);
if (storedLang) setLanguage(storedLang as SupportedLanguage);
if (storedChesscomUsername) setChesscomUsername(storedChesscomUsername);
if (storedLichessUsername) setLichessUsername(storedLichessUsername);
setMounted(true);
}, []);
@@ -35,6 +41,19 @@ export default function SettingsPage() {
localStorage.setItem("chess_tutor_language", language);
// Save online platform usernames
if (chesscomUsername.trim()) {
localStorage.setItem("chesscom_username", chesscomUsername.trim());
} else {
localStorage.removeItem("chesscom_username");
}
if (lichessUsername.trim()) {
localStorage.setItem("lichess_username", lichessUsername.trim());
} else {
localStorage.removeItem("lichess_username");
}
// Go back to home
router.push("/");
};
@@ -99,6 +118,46 @@ export default function SettingsPage() {
</p>
</div>
</div>
{/* Online Platform Usernames */}
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Online Platform Integration
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Save your usernames to quickly import games from Chess.com and Lichess in the Analysis page.
</p>
<div className="space-y-4">
{/* Chess.com Username */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Chess.com Username
</label>
<input
type="text"
value={chesscomUsername}
onChange={(e) => setChesscomUsername(e.target.value)}
placeholder="Enter your Chess.com username"
className="w-full p-3 border rounded-lg dark:bg-gray-700 dark:border-gray-600 text-gray-900 dark:text-white focus:ring-2 focus:ring-green-500 outline-none transition-all"
/>
</div>
{/* Lichess Username */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Lichess Username
</label>
<input
type="text"
value={lichessUsername}
onChange={(e) => setLichessUsername(e.target.value)}
placeholder="Enter your Lichess username"
className="w-full p-3 border rounded-lg dark:bg-gray-700 dark:border-gray-600 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 outline-none transition-all"
/>
</div>
</div>
</div>
</div>
<div className="pt-6 border-t border-gray-200 dark:border-gray-700 flex justify-end">