feat: Add chat session to analysis page

- Import ChatSession from @google/generative-ai
- Add chatSession state to maintain conversation history
- Initialize chat session with personality and language context
- Replace stateless generateContent() with chatSession.sendMessage()
- Add FEN before/after to move analysis prompts
- Chat history is preserved when navigating between moves
- LLM can now provide context-aware commentary across the game
This commit is contained in:
Stefan
2025-11-27 12:36:40 +01:00
parent a9a5366832
commit aa389613cb
+43 -7
View File
@@ -14,6 +14,7 @@ import { detectChessFormat, ChessFormat } from "@/lib/chessFormatDetector";
import { detectMissedTactics, DetectedTactic, uciToSan } from "@/lib/tacticDetection"; import { detectMissedTactics, DetectedTactic, uciToSan } from "@/lib/tacticDetection";
import { lookupOpening } from "@/lib/openings"; import { lookupOpening } from "@/lib/openings";
import { getGenAIModel } from "@/lib/gemini"; import { getGenAIModel } from "@/lib/gemini";
import { ChatSession } from "@google/generative-ai";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
interface MoveStep { interface MoveStep {
@@ -56,6 +57,7 @@ export default function AnalysisPage() {
const [stepDetails, setStepDetails] = useState<Record<number, StepDetails>>({}); const [stepDetails, setStepDetails] = useState<Record<number, StepDetails>>({});
const [isCommenting, setIsCommenting] = useState(false); const [isCommenting, setIsCommenting] = useState(false);
const [comments, setComments] = useState<Record<number, string>>({}); const [comments, setComments] = useState<Record<number, string>>({});
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
useEffect(() => { useEffect(() => {
const storedKey = localStorage.getItem("gemini_api_key"); const storedKey = localStorage.getItem("gemini_api_key");
@@ -70,6 +72,40 @@ export default function AnalysisPage() {
return () => sf.terminate(); return () => sf.terminate();
}, []); }, []);
// Initialize chat session for conversational analysis
useEffect(() => {
if (apiKey) {
const model = getGenAIModel(apiKey, "gemini-2.5-flash");
const session = model.startChat({
history: [
{
role: "user",
parts: [{
text: `You are ${selectedPersonality.name}. You will analyze a chess game move by move.
Stay in character and maintain your personality throughout the analysis.
Language: ${language.toUpperCase()}.
IMPORTANT:
- You are analyzing moves sequentially
- Each move you analyze builds on the previous context
- If the user navigates backwards or forwards, you will see the move number
- Provide educational commentary in your characteristic style
- Be concise (3-4 sentences per move)
- Focus on what the move accomplishes and what was missed`
}]
},
{
role: "model",
parts: [{
text: `Understood. I am ${selectedPersonality.name}, and I will analyze this game move by move in ${language}, maintaining my personality while providing educational insights. I'll keep track of the game's progression and provide context-aware commentary.`
}]
}
]
});
setChatSession(session);
}
}, [apiKey, selectedPersonality, language]);
const currentFen = useMemo(() => { const currentFen = useMemo(() => {
if (currentIndex === 0) return initialFen; if (currentIndex === 0) return initialFen;
return steps[currentIndex - 1]?.fenAfter || initialFen; return steps[currentIndex - 1]?.fenAfter || initialFen;
@@ -199,7 +235,7 @@ export default function AnalysisPage() {
}, [currentIndex, steps, evaluationVersion]); }, [currentIndex, steps, evaluationVersion]);
useEffect(() => { useEffect(() => {
if (!apiKey) return; if (!chatSession) return;
if (currentIndex === 0) return; if (currentIndex === 0) return;
const step = steps[currentIndex - 1]; const step = steps[currentIndex - 1];
const details = stepDetails[currentIndex]; const details = stepDetails[currentIndex];
@@ -210,7 +246,6 @@ export default function AnalysisPage() {
setIsCommenting(true); setIsCommenting(true);
const timeout = setTimeout(async () => { const timeout = setTimeout(async () => {
try { try {
const model = getGenAIModel(apiKey, "gemini-2.5-flash");
const delta = details.cpLoss ?? 0; const delta = details.cpLoss ?? 0;
const evalBefore = details.evalBefore!.score / 100; const evalBefore = details.evalBefore!.score / 100;
const evalAfter = details.evalAfter!.score / 100; const evalAfter = details.evalAfter!.score / 100;
@@ -221,14 +256,14 @@ export default function AnalysisPage() {
.join("; ") || "None"; .join("; ") || "None";
const prompt = ` const prompt = `
You are ${selectedPersonality.name}. Stay in character. Analyze this move:
Language: ${language.toUpperCase()}.
Explain the move that was just played.
DATA: DATA:
- Move number: ${step.moveNumber} - Move number: ${step.moveNumber}
- Side to move: ${step.color} - Side to move: ${step.color}
- Move played (SAN): ${step.san} - Move played (SAN): ${step.san}
- FEN before move: ${step.fenBefore}
- FEN after move: ${step.fenAfter}
- Evaluation before move: ${evalBefore.toFixed(2)} pawns - Evaluation before move: ${evalBefore.toFixed(2)} pawns
- Evaluation after move: ${evalAfter.toFixed(2)} pawns - Evaluation after move: ${evalAfter.toFixed(2)} pawns
- Best move suggestion: ${details.bestMoveSan ?? details.evalBefore!.bestMove} - Best move suggestion: ${details.bestMoveSan ?? details.evalBefore!.bestMove}
@@ -244,7 +279,7 @@ INSTRUCTIONS:
- Refer to the player's side as ${step.color}. - Refer to the player's side as ${step.color}.
- Keep it educational and stay true to your personality tone.`; - Keep it educational and stay true to your personality tone.`;
const result = await model.generateContent(prompt); const result = await chatSession.sendMessage(prompt);
if (!cancelled) { if (!cancelled) {
setComments(prev => ({ ...prev, [currentIndex]: result.response.text() })); setComments(prev => ({ ...prev, [currentIndex]: result.response.text() }));
} }
@@ -258,8 +293,9 @@ INSTRUCTIONS:
return () => { return () => {
cancelled = true; cancelled = true;
clearTimeout(timeout); clearTimeout(timeout);
setIsCommenting(false);
}; };
}, [apiKey, currentIndex, stepDetails, steps, comments, selectedPersonality, language, openingInfo]); }, [chatSession, currentIndex, stepDetails, steps, comments, openingInfo]);
const formatEval = (evaluation?: StockfishEvaluation) => { const formatEval = (evaluation?: StockfishEvaluation) => {
if (!evaluation) return t.analysis.enginePending; if (!evaluation) return t.analysis.enginePending;