working app
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { GoogleGenerativeAI, SchemaType, FunctionDeclaration } from "@google/generative-ai";
|
||||
import { StockfishEvaluation } from "./stockfish";
|
||||
|
||||
export async function getAvailableModels(apiKey: string): Promise<string[]> {
|
||||
// Prioritize newer models
|
||||
return [
|
||||
"gemini-3-pro-preview",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.5-flash"
|
||||
];
|
||||
}
|
||||
|
||||
const evaluatePositionTool: FunctionDeclaration = {
|
||||
name: "evaluate_position",
|
||||
description: "Evaluates a chess position using the Stockfish engine to get the best move and score. Use this when the user asks for the best move, evaluation, or why a move is good/bad.",
|
||||
parameters: {
|
||||
type: SchemaType.OBJECT,
|
||||
properties: {
|
||||
fen: {
|
||||
type: SchemaType.STRING,
|
||||
description: "The FEN string of the position to evaluate.",
|
||||
},
|
||||
depth: {
|
||||
type: SchemaType.NUMBER,
|
||||
description: "The search depth for the engine (default 15).",
|
||||
},
|
||||
},
|
||||
required: ["fen"],
|
||||
},
|
||||
};
|
||||
|
||||
export function getGenAIModel(apiKey: string, modelName: string = "gemini-2.5-flash") {
|
||||
const genAI = new GoogleGenerativeAI(apiKey);
|
||||
return genAI.getGenerativeModel({
|
||||
model: modelName,
|
||||
tools: [{ functionDeclarations: [evaluatePositionTool] }],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import ecoA from '../../public/openings/ecoA.json';
|
||||
import ecoB from '../../public/openings/ecoB.json';
|
||||
import ecoC from '../../public/openings/ecoC.json';
|
||||
import ecoD from '../../public/openings/ecoD.json';
|
||||
import ecoE from '../../public/openings/ecoE.json';
|
||||
|
||||
export interface OpeningMetadata {
|
||||
src: string;
|
||||
eco: string;
|
||||
moves: string;
|
||||
name: string;
|
||||
aliases?: { [key: string]: string };
|
||||
meta?: {
|
||||
strengths_white?: string[];
|
||||
weaknesses_white?: string[];
|
||||
strengths_black?: string[];
|
||||
weaknesses_black?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// Merge all ECO databases into one lookup object
|
||||
const openingsData = {
|
||||
...ecoA,
|
||||
...ecoB,
|
||||
...ecoC,
|
||||
...ecoD,
|
||||
...ecoE
|
||||
} as Record<string, OpeningMetadata>;
|
||||
|
||||
export function lookupOpening(fen: string): OpeningMetadata | null {
|
||||
// The keys in the JSON are exact FEN strings.
|
||||
if (openingsData[fen]) {
|
||||
return openingsData[fen];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export interface Personality {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
systemPrompt: string; // Contains Style, Tone, Keywords
|
||||
image: string; // Emoji or path
|
||||
}
|
||||
|
||||
export const PERSONALITIES: Personality[] = [
|
||||
{
|
||||
id: "drunk_russian_gm",
|
||||
name: "Drunk Russian GM",
|
||||
description: "A bitter, fatalistic, but brilliant Grandmaster who has seen it all.",
|
||||
systemPrompt: `
|
||||
Style: Bitter, gloomy, slightly slurred, existential, Dostoevsky-atmosphere.
|
||||
Tone: Frustrated, fatalistic, but humorous and brutally honest.
|
||||
Keywords: "my boy", "ach... life is pain", "vodka", "darkness", "blunder like my first marriage".
|
||||
INSTRUCTION: Use keywords SPARINGLY. Vary your vocabulary. Be conversational. YOU are playing the game. Speak from YOUR perspective.
|
||||
`,
|
||||
image: "🥃"
|
||||
},
|
||||
{
|
||||
id: "hype_streamer",
|
||||
name: "Hype Streamer",
|
||||
description: "An energetic, loud, and overreacting chess streamer.",
|
||||
systemPrompt: `
|
||||
Style: Loud, energetic, sarcastic, YouTuber-overreacting, Gen-Z slang.
|
||||
Tone: Dramatic, humorous, exaggerating everything.
|
||||
Keywords: "Bro!", "Holy smokes!", "Unbelievable!", "Chat, look at this!", "Insane!", "GG".
|
||||
`,
|
||||
image: "🎧"
|
||||
},
|
||||
{
|
||||
id: "professional_coach",
|
||||
name: "Professional Coach",
|
||||
description: "A strict, analytical, and straightforward chess coach focused on your improvement.",
|
||||
systemPrompt: `
|
||||
Style: Professional, analytical, objective, strict but encouraging.
|
||||
Tone: Serious, educational, straightforward.
|
||||
Keywords: "structure", "plan", "weakness", "advantage", "calculation".
|
||||
INSTRUCTION: You are a professional chess coach playing against the user. Speak in the first person ("I played...", "I think..."). Do NOT mention "Stockfish" or "engine". Focus on the objective truth of the position. Explain WHY a move is good or bad based on chess principles (space, time, material, structure). Be concise.
|
||||
`,
|
||||
image: "👨🏫"
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,90 @@
|
||||
export type StockfishEvaluation = {
|
||||
bestMove: string;
|
||||
ponder: string | null;
|
||||
score: number; // centipawns, positive for white
|
||||
mate: number | null; // moves to mate, positive for white
|
||||
depth: number;
|
||||
};
|
||||
|
||||
export class Stockfish {
|
||||
private worker: Worker | null = null;
|
||||
private isReady: boolean = false;
|
||||
private lastScore: number = 0;
|
||||
private lastMate: number | null = null;
|
||||
private lastDepth: number = 0;
|
||||
|
||||
constructor() {
|
||||
if (typeof window !== "undefined") {
|
||||
this.worker = new Worker("/stockfish/stockfish.js");
|
||||
this.worker.onmessage = (e) => {
|
||||
// console.log("Stockfish message:", e.data);
|
||||
if (e.data === "uciok") {
|
||||
this.isReady = true;
|
||||
}
|
||||
};
|
||||
this.worker.postMessage("uci");
|
||||
}
|
||||
}
|
||||
|
||||
async evaluate(fen: string, depth: number = 15, multiPV: number = 1): Promise<StockfishEvaluation> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.worker) {
|
||||
reject("Stockfish worker not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset last known evaluation values for this new evaluation
|
||||
this.lastScore = 0;
|
||||
this.lastMate = null;
|
||||
this.lastDepth = 0;
|
||||
|
||||
const handler = (event: MessageEvent) => {
|
||||
const message = event.data;
|
||||
// console.log("Stockfish:", message);
|
||||
|
||||
if (message.startsWith("info depth")) {
|
||||
const depthMatch = message.match(/depth (\d+)/);
|
||||
const scoreMatch = message.match(/score cp (-?\d+)/);
|
||||
const mateMatch = message.match(/score mate (-?\d+)/);
|
||||
|
||||
if (depthMatch) this.lastDepth = parseInt(depthMatch[1]);
|
||||
if (scoreMatch) {
|
||||
this.lastScore = parseInt(scoreMatch[1]);
|
||||
this.lastMate = null;
|
||||
}
|
||||
if (mateMatch) {
|
||||
this.lastMate = parseInt(mateMatch[1]);
|
||||
this.lastScore = 0; // or some indicator
|
||||
}
|
||||
}
|
||||
|
||||
if (message.startsWith("bestmove")) {
|
||||
const parts = message.split(" ");
|
||||
const bestMove = parts[1];
|
||||
let ponder: string | null = null;
|
||||
if (parts.length > 3 && parts[2] === "ponder") {
|
||||
ponder = parts[3];
|
||||
}
|
||||
|
||||
// Remove the event listener to prevent it from interfering with future evaluations
|
||||
this.worker?.removeEventListener("message", handler);
|
||||
resolve({
|
||||
bestMove,
|
||||
ponder,
|
||||
score: this.lastScore,
|
||||
mate: this.lastMate,
|
||||
depth: this.lastDepth
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.worker.addEventListener("message", handler);
|
||||
this.worker.postMessage(`position fen ${fen}`);
|
||||
this.worker.postMessage(`go depth ${depth}`);
|
||||
});
|
||||
}
|
||||
|
||||
terminate() {
|
||||
this.worker?.terminate();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user