working app

This commit is contained in:
Stefan
2025-11-23 10:51:00 +01:00
parent f6480a43c6
commit 30f1e9e3a5
22 changed files with 106271 additions and 63 deletions
+38
View File
@@ -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] }],
});
}
+36
View File
@@ -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;
}
+45
View File
@@ -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: "👨‍🏫"
}
];
+90
View File
@@ -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();
}
}