working app
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Stockfish, StockfishEvaluation } from "@/lib/stockfish";
|
||||
import { OpeningMetadata, lookupOpening } from "@/lib/openings";
|
||||
import { getGenAIModel } from "@/lib/gemini";
|
||||
import { Loader2, X, Brain, Trophy, AlertTriangle } from "lucide-react";
|
||||
|
||||
interface GameAnalysisModalProps {
|
||||
fen: string;
|
||||
stockfish: Stockfish | null;
|
||||
apiKey: string | null;
|
||||
language: 'en' | 'de' | 'fr' | 'it';
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function GameAnalysisModal({ fen, stockfish, apiKey, language, onClose }: GameAnalysisModalProps) {
|
||||
const [evaluation, setEvaluation] = useState<StockfishEvaluation | null>(null);
|
||||
const [opening, setOpening] = useState<OpeningMetadata | null>(null);
|
||||
const [summary, setSummary] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const analyze = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// 1. Stockfish Evaluation
|
||||
let evalResult: StockfishEvaluation | null = null;
|
||||
if (stockfish) {
|
||||
evalResult = await stockfish.evaluate(fen, 15); // Quick depth
|
||||
setEvaluation(evalResult);
|
||||
}
|
||||
|
||||
// 2. Opening Lookup
|
||||
const openingData = lookupOpening(fen);
|
||||
setOpening(openingData);
|
||||
|
||||
// 3. LLM Summary
|
||||
if (apiKey && evalResult) {
|
||||
const model = getGenAIModel(apiKey, "gemini-2.5-flash");
|
||||
const prompt = `
|
||||
You are a Chess Grandmaster Analyst.
|
||||
Analyze this position for the user.
|
||||
|
||||
DATA:
|
||||
- FEN: ${fen}
|
||||
- Evaluation: ${evalResult.score} cp (positive = White advantage, negative = Black advantage)
|
||||
- Mate in: ${evalResult.mate ?? "N/A"}
|
||||
- Best Move: ${evalResult.bestMove}
|
||||
- Opening: ${openingData ? `${openingData.name} (${openingData.eco})` : "Unknown/Midgame"}
|
||||
|
||||
INSTRUCTIONS:
|
||||
1. Summarize who is winning and why (based on score).
|
||||
2. Identify the key strategic factors (space, piece activity, king safety).
|
||||
3. Mention the opening if relevant.
|
||||
4. Keep it concise (max 3-4 sentences).
|
||||
5. Respond in ${language.toUpperCase()}.
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Plain text paragraph.
|
||||
`;
|
||||
|
||||
const result = await model.generateContent(prompt);
|
||||
setSummary(result.response.text());
|
||||
} else if (!apiKey) {
|
||||
setSummary("Please provide an API Key to get an AI summary.");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Analysis failed:", e);
|
||||
setSummary("Failed to generate analysis.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
analyze();
|
||||
}, [fen, stockfish, apiKey, language]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl max-w-lg w-full overflow-hidden border border-gray-200 dark:border-gray-700 animate-in fade-in zoom-in duration-200">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center bg-gray-50 dark:bg-gray-900">
|
||||
<h2 className="text-lg font-bold flex items-center gap-2 text-gray-900 dark:text-white">
|
||||
<Brain className="text-purple-600" />
|
||||
Game Analysis
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 space-y-4">
|
||||
<Loader2 className="animate-spin text-purple-600" size={48} />
|
||||
<p className="text-gray-500">Analyzing position...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Evaluation Score */}
|
||||
<div className="flex items-center justify-between p-4 bg-gray-100 dark:bg-gray-700 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Evaluation</p>
|
||||
<p className={`text-2xl font-bold ${(evaluation?.score || 0) > 0 ? "text-green-600" : (evaluation?.score || 0) < 0 ? "text-red-600" : "text-gray-600"
|
||||
}`}>
|
||||
{evaluation?.mate
|
||||
? `Mate in ${evaluation.mate}`
|
||||
: `${(evaluation?.score || 0) > 0 ? "+" : ""}${(evaluation?.score || 0) / 100}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Best Move</p>
|
||||
<p className="text-xl font-mono font-bold text-gray-900 dark:text-white">
|
||||
{evaluation?.bestMove}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Opening Info */}
|
||||
{opening && (
|
||||
<div className="p-4 border border-blue-200 bg-blue-50 dark:bg-blue-900/20 dark:border-blue-800 rounded-lg">
|
||||
<h3 className="font-semibold text-blue-800 dark:text-blue-300 mb-1">Opening Identified</h3>
|
||||
<p className="text-blue-900 dark:text-blue-100">{opening.name} ({opening.eco})</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Summary */}
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<Trophy size={16} className="text-yellow-500" />
|
||||
Coach's Summary
|
||||
</h3>
|
||||
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg text-gray-800 dark:text-gray-200 leading-relaxed">
|
||||
{summary}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user