feat: merge chess_tutor + OpenRouter abstraction
Build and publish Docker image to GHCR / build-and-push (push) Has been cancelled

- Merged stefan-kp/chess_tutor into chess-project with full feature set
- Preserved Hermes agent files (agents/, .gitea/, AGENTS.md)
- Added OpenRouter abstraction layer (src/lib/openrouter.ts)
- Created ModelSelector dropdown component with 9 models
- Updated API routes (chat + opening explanation) to use OpenRouter
- Updated useTutorChat to route through OpenRouter API
- Updated onboarding and API key input for OpenRouter
- Created tasks.md with sprint 1 plan
- Updated README with new architecture details
This commit is contained in:
2026-06-20 19:47:16 +00:00
parent 03608b9d70
commit 55cb932447
22 changed files with 1105 additions and 730 deletions
+8 -1
View File
@@ -1,5 +1,12 @@
# Debug Mode
# Set to 'true' to enable debug mode which shows LLM prompts and responses
# This is useful for debugging AI behavior
NEXT_PUBLIC_DEBUG=false
# OpenRouter API Key (optional — users can also enter in the UI)
# Get your key at https://openrouter.ai/keys
# OPENROUTER_API_KEY=sk-or-v1-...
# Default model (user can change in the UI)
# Options: google/gemini-2.5-flash, google/gemini-2.5-pro, anthropic/claude-sonnet-4,
# openai/gpt-4o, x-ai/grok-3, deepseek/deepseek-chat, meta-llama/llama-3.1-70b
NEXT_PUBLIC_DEFAULT_MODEL=google/gemini-2.5-flash
+22
View File
@@ -0,0 +1,22 @@
---
name: Feature Request
about: Suggest a new feature for this project
title: '[FEATURE] '
labels: feature
---
## Description
## Motivation
## Proposed Solution
## Acceptance Criteria
- [ ]
- [ ]
- [ ]
## Related Planning
Reference any related issues or plans from the `core-planning` repository.
+23
View File
@@ -0,0 +1,23 @@
## Summary
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Refactoring
- [ ] Documentation
- [ ] Other:
## Related Task / Issue
Reference the task from `/tasks` or a planning issue from `core-planning`.
## Testing
- [ ] Unit tests added/updated
- [ ] Manual testing completed
- [ ] All existing tests pass
## Checklist
- [ ] Code follows project style guidelines
- [ ] Self-review completed
- [ ] Documentation updated (if needed)
+43
View File
@@ -0,0 +1,43 @@
# AGENTS.md
This file provides high-level guidance for all Hermes agents working in this project.
## Project Mission
[Brief description of what this project is trying to achieve]
## Key Principles
- Follow the execution guidelines defined in `core-planning/docs/EXECUTION.md`
- Use the SCRUM stand-up format when reporting progress
- Coordinate primarily through the Hermes Kanban board
- Maintain clear, actionable comments on tasks
## Architecture & Constraints
[Add important technical decisions, tech stack, constraints, or gotchas here]
## Coding Standards
[Add any project-specific coding conventions, linting rules, or style guides]
## Kanban Workflow
- All work is tracked in the Hermes Kanban board
- Use clear task prefixes: `[Feature]`, `[Bug]`, `[Research]`, `[Review]`, etc.
- Follow the escalation rules defined in `core-planning/docs/KANBAN-WORKFLOW.md`
- Only escalate to the human when the defined escalation criteria are met
## Important Context
- Refer to `core-planning/docs/` for team-wide standards
- Refer to `docs/` in this repo for project-specific guidelines
- When in doubt, ask for clarification in the task comments
## Agent Roles in This Project
See the `agents/` folder for role-specific instructions.
---
*This file should be updated as the project evolves.*
+69 -547
View File
@@ -1,571 +1,93 @@
# AI Chess Tutor
# Chess Tutor
## The Story
I always wanted to implement an AI-based chess tutor because I like playing chess, although to be honest, I actually suck at it. I didn't find the existing tutors or big apps useful enough for my needs, so I decided to build my own approach.
This application was built using **Antigravity by Google**. I like to work with it, though sometimes it just runs away. Still, I found the end result to be quite fun to play, which is why I'm sharing it here.
## How It Works
**AI Chess Tutor** is an interactive chess learning application where you play against an AI opponent powered by Stockfish while receiving real-time coaching feedback from a Large Language Model (LLM).
### Learning Modes
**Opening Training Mode** - Learn chess openings systematically:
1. **Browse Openings**: Explore 12,379 openings organized by ECO code and family
2. **Select an Opening**: Choose from openings like French Defense, Sicilian, Ruy Lopez, etc.
3. **Read Background**: View Wikipedia context about the opening's history and strategic ideas
4. **Practice Moves**: Make moves while the AI tutor guides you through the repertoire
5. **Real-Time Feedback**: Get instant feedback on whether you're in theory or deviating
6. **Handle Deviations**: When you leave theory, choose to:
- **Undo** and return to the repertoire
- **Continue Playing** in full game mode with opening context
- **Explore** the variation further
7. **Resume Sessions**: Your progress is saved - pick up where you left off
**Tactical Practice Mode** - Master tactical patterns:
1. **Choose a Pattern**: Select from 8 tactical themes (Pin, Fork, Skewer, etc.)
2. **Solve Puzzles**: Find the winning tactical move in realistic positions
3. **Get Feedback**: Receive immediate feedback and explanations
4. **Track Progress**: Monitor your streak and success rate
5. **Learn Patterns**: Build pattern recognition through repetition
**Game Mode** - Play full games with AI coaching:
1. **Start or Resume**: From the homepage, either start a new game or continue an unfinished game
2. **Choose Your Personality**: Select from 9 unique AI coaching personalities
3. **Select Your Color**: Play as White, Black, or let the app choose randomly
4. **Play Chess**: Make your moves on the board while the Stockfish engine plays against you
5. **Get Real-Time Feedback**: After each move exchange, your AI tutor analyzes the position:
- Move quality and alternatives
- Position evaluation changes
- **Missed tactical opportunities** (pins, forks, skewers, checks, hanging pieces, material captures)
- Opening theory (when applicable)
- Strategic and positional considerations
6. **Chat with Your Tutor**: Ask questions anytime - your tutor answers in character
7. **Export Your Game**: Download your game as PGN or export the current position as FEN
8. **Resign When Needed**: End the game early with in-character tutor feedback
9. **Post-Game Analysis**: Review comprehensive analysis showing:
- All your mistakes and inaccuracies
- Missed tactical opportunities throughout the game
- Learning opportunities and improvement suggestions
![Gameplay Screenshot](screenshots/gameplay.png)
*Screenshot placeholder: Main game interface with board, evaluation bar, and chat*
AI-powered chess tutor with Stockfish analysis. Play against customizable AI coach personalities. Supports multiple AI models via OpenRouter.
## Features
### Core Features
- **Stockfish Engine**: Powerful chess engine for move analysis and opponent play
- **Opening Training Mode**: Interactive opening trainer with AI tutor guidance
- Practice 12,379 chess openings from comprehensive ECO database
- Real-time feedback on theory adherence vs. deviations
- Wikipedia integration for opening history and strategic context
- Session persistence with resume capability
- Deviation dialog with options to undo, continue, or transition to game mode
- Automatic opponent moves following repertoire lines
- **Tactical Practice Mode**: Pattern-based puzzle training with 8 tactical themes
- 20+ puzzles per pattern from Lichess database
- Patterns: Pin, Fork, Skewer, Discovered Check, Double Attack, Overloading, Back Rank Weakness, Trapped Piece
- Streak tracking and performance statistics
- **Tactical Recognition**: Automatically detects missed tactical opportunities (pins, forks, skewers, checks, hanging pieces, material captures)
- **Opening Database**: Comprehensive ECO database with 12,379 openings, metadata, and theory
- **Real-Time Evaluation**: Live position evaluation with visual evaluation bar
- **Move Analysis**: Detailed feedback on every move you make with tactical insights
- **Interactive Chat**: Ask your AI tutor questions and get personalized answers
- **Post-Game Analysis**: Review all your mistakes and missed opportunities after each game
- **Resign Option**: End games early with a resign button - your AI tutor responds in character
- **Direct Analysis Access**: Jump straight to the analysis page from the game over modal
- **Multi-Language Support**: Available in English, German, French, Italian, and Polish
- **FEN/PGN Import**: Import positions or games with automatic format detection
- **Game Export**: Download your games as PGN or export current position as FEN
- **Move History**: Visual move history table with evaluation changes and tactical annotations
- **Saved Games**: Continue unfinished games from where you left off
- **Settings Management**: Customize your experience with language preferences, API key management, and data controls
- **Mobile Apps**: iOS and Android apps with Capacitor (proprietary licensing for App Store compliance)
### AI Personalities
Choose from **9 distinct coaching personalities**, each offering a unique learning experience:
#### 📘 Opening Professor
*"This is a very instructive structure..."*
A calm, deeply knowledgeable educator who loves turning openings into understandable stories with history, plans, and model structures. Perfect for learning opening theory and understanding positional concepts.
#### 👨‍🏫 Professional Coach
*"Let's analyze the structure of this position"*
A strict, analytical, and straightforward chess coach focused on your improvement. Expect objective analysis, clear explanations based on chess principles, and professional teaching methods.
#### 🥃 Drunk Russian GM
*"Ach... life is pain, my boy"*
A bitter, fatalistic, but brilliant Grandmaster who has seen it all. Expect brutally honest feedback delivered with dark humor and existential commentary. This personality combines deep chess knowledge with a Dostoevsky-like atmosphere.
#### 🎧 Hype Streamer
*"BRO! THAT MOVE WAS INSANE!"*
An energetic, loud, and entertaining chess streamer who makes every game exciting. Expect dramatic reactions, Gen-Z slang, and over-the-top commentary that keeps you engaged and motivated.
#### 🧙‍♂️ Gandalf the Chess Wizard
*"A move is never late, nor is it early..."*
A wise and mystical chess wizard who speaks in riddles and metaphors. Combines chess wisdom with magical references and philosophical insights.
#### 🤖 Stockfish (Literal)
*"Evaluation: +0.7. Best continuation: Nf3, d5, c4..."*
The engine itself, speaking in pure chess notation and evaluations. No personality, just raw analysis and computer-like precision.
#### 😤 Toxic Gamer
*"Are you even trying? That's the worst move I've seen all day!"*
An abrasive, confrontational personality that roasts your mistakes mercilessly. Not for the faint of heart, but some players find the challenge motivating.
#### 🎭 Shakespearean Bard
*"To castle or not to castle, that is the question..."*
A theatrical personality that delivers chess analysis in Shakespearean verse and dramatic monologues. Makes every game feel like a stage performance.
#### 🧘 Zen Master
*"The board is empty, yet full of possibilities..."*
A calm, meditative personality that approaches chess as a spiritual practice. Focuses on mindfulness, patience, and finding harmony in the position.
### Tactical Recognition System
One of the standout features is the **automatic tactical pattern detection** that runs after every move:
**What it detects:**
- **Material Captures**: Safe captures of pieces and pawns (with recapture analysis)
- **Pins**: Pieces pinned to the king or more valuable pieces
- **Forks**: Pieces attacking multiple valuable targets simultaneously
- **Skewers**: Attacks forcing a valuable piece to move, exposing another
- **Checks**: Moves that give check to the opponent's king
- **Hanging Pieces**: Undefended pieces that could be captured
**How it works:**
1. After each move, the system compares your move to the engine's best move
2. If there's a significant evaluation difference, it analyzes what tactical opportunities were missed
3. The AI tutor receives this information and explains it in real-time in their characteristic style
4. All missed tactics are also shown in the post-game analysis
**Conservative approach:**
- The system uses multiple filters to avoid false positives
- Only reports tactics when there's a clear advantage
- Checks for piece safety (e.g., won't report a "capture" if the piece can be immediately recaptured)
This feature helps you learn tactical patterns naturally during gameplay, rather than just through puzzle training.
### User Experience Features
**Game Management:**
- **Save & Resume**: Your games are automatically saved to browser storage - continue playing anytime
- **Game Export**: Download your completed or in-progress games as PGN files for analysis in other tools
- **Position Export**: Export the current board position as FEN for sharing or further study
- **Game History**: Visual table showing all moves with evaluation changes and missed tactics
- **Clear All Data**: Reset your local storage from the settings page when needed
**Customization:**
- **5 Languages**: Full interface translation in English, German, French, Italian, and Polish
- **9 AI Personalities**: Choose the coaching style that motivates you best
- **Flexible Setup**: Play as White, Black, or random color selection
- **Import Games**: Start from any position using FEN or continue from a PGN game
**Analysis Tools:**
- **Real-Time Evaluation Bar**: Visual representation of position evaluation during play
- **Opening Explorer**: Automatic opening detection with theory and explanations
- **Position Analysis**: Deep-dive into any position with the analysis modal
- **Post-Game Review**: Comprehensive analysis of all mistakes and missed opportunities
**Modern Interface:**
- **Responsive Design**: Works on desktop, tablet, and mobile devices
- **Dark Mode Support**: Automatic dark/light theme based on system preferences
- **Intuitive Controls**: Drag-and-drop piece movement with visual feedback
- **Clean Layout**: Focused design that keeps the board and feedback front and center
- **Play chess** against Stockfish with an AI coach giving you real-time feedback
- **AI personalities** — choose from Hype Streamer, Professional Coach, Russian Grandmaster, and more
- **Model selector** — switch between Gemini, Claude, GPT, Grok, DeepSeek, and Llama
- **Opening trainer** — practice specific openings with move validation
- **Tactical puzzles** — test your pattern recognition
- **Game analysis** — get AI feedback on your games
- **Multi-language** — English, German, French, Italian, Polish
## Quick Start
### Option 1: Docker Compose (Recommended - Easiest!)
The fastest way to get started is using our pre-built Docker image from GitHub Container Registry:
```bash
# Clone the repository
git clone https://github.com/stefan-kp/chess_tutor.git
cd chess_tutor
# (Optional) Create .env file with your API key
echo "NEXT_PUBLIC_GEMINI_API_KEY=your_api_key_here" > .env
# Start the application
docker-compose up -d
# View logs
docker-compose logs -f
```
The application will be available at `http://localhost:3050`
**What happens:**
- Docker Compose automatically pulls the latest pre-built image from `ghcr.io/stefan-kp/chess-tutor:latest`
- No build step required - the image is built automatically on every push to main via GitHub Actions
- The container starts with health checks and auto-restart enabled
### Option 2: Local Development
If you want to run the application locally for development:
#### Prerequisites
- Node.js 18+ installed
- A free Google Gemini API key
#### Steps
```bash
# Clone the repository
git clone https://github.com/stefan-kp/chess_tutor.git
cd chess_tutor
# Install dependencies
# Install
npm install
# Create .env file (optional)
echo "NEXT_PUBLIC_GEMINI_API_KEY=your_api_key_here" > .env
# Run development server
# Run
npm run dev
# Or build and run production
npm run build
npm start
```
The application will be available at `http://localhost:3050`
Open **http://localhost:3050** — the onboarding page will walk you through setup.
### Getting Your Gemini API Key
## Tech Stack
1. Visit [Google AI Studio](https://aistudio.google.com/app/apikey)
2. Sign in with your Google account
3. Click "Create API Key"
4. Copy your API key
- **Frontend:** Next.js 16, React 19, TypeScript, Tailwind CSS 4
- **Chess:** chess.js, react-chessboard
- **Engine:** Stockfish (in-browser via stockfish.js WASM)
- **AI:** OpenRouter API (multi-provider: Gemini, Claude, GPT, Grok, DeepSeek, Llama)
- **Mobile:** Capacitor (iOS/Android)
### API Key Configuration
## Environment
You have two options for providing your Gemini API key:
#### Option 1: Environment Variable (Recommended for Docker/Server)
1. Create a `.env` file in the project root
2. Add your API key:
```
NEXT_PUBLIC_GEMINI_API_KEY=your_api_key_here
```
3. The application will automatically use this key
#### Option 2: Browser Storage (Fallback)
1. Run the application without an API key
2. When prompted, enter your API key in the modal dialog
3. The key will be stored in your browser's localStorage
> [!NOTE]
> You can update your API key anytime by clicking the key icon in the bottom-right corner of the application.
## Advanced Deployment
### Automated Docker Builds
This project uses GitHub Actions to automatically build and publish Docker images to GitHub Container Registry (GHCR) on every push to the `main` branch.
**What this means:**
- Every commit to `main` triggers an automatic Docker build
- The latest image is always available at `ghcr.io/stefan-kp/chess-tutor:latest`
- Each build is also tagged with the commit SHA for version tracking
- No need to build locally - just pull and run!
### Using Pre-Built Images
The `docker-compose.yml` file is already configured to use the pre-built image:
```yaml
services:
chess-tutor:
image: ghcr.io/stefan-kp/chess-tutor:latest
# ... rest of configuration
Copy `.env.example` to `.env.local`:
```
OPENROUTER_API_KEY=sk-or-...
NEXT_PUBLIC_DEFAULT_MODEL=google/gemini-2.5-flash
```
This means you can deploy anywhere with just:
```bash
docker-compose up -d
Or enter your API key through the UI on first launch.
## Hermes Agent Integration
This repo includes Hermes agent role files for multi-agent development:
| File | Purpose |
|------|---------|
| `AGENTS.md` | High-level project guidance for Hermes agents |
| `tasks.md` | Sprint tasks with Now / Next / Backlog |
| `agents/HA_*.md` | Role-specific instructions (Architect, Coder, DevOps, etc.) |
| `.gitea/` | Issue and PR templates |
| `docs/WORKFLOW.md` | Collaboration workflow |
## Available AI Models
| Model | Provider |
|-------|----------|
| Gemini 2.5 Flash | Google |
| Gemini 2.5 Pro | Google |
| Claude Sonnet 4 | Anthropic |
| Claude 3.5 Haiku | Anthropic |
| GPT-4o | OpenAI |
| GPT-4o Mini | OpenAI |
| DeepSeek Chat | DeepSeek |
| Grok 3 | xAI |
| Llama 3.1 70B | Meta |
## Project Structure
```
### Manual Docker Build (Optional)
If you prefer to build the image yourself:
```bash
# Build the image
docker build -t chess-tutor:latest .
# Run the container
docker run -d \
--name chess-tutor \
-p 3050:3050 \
-e NEXT_PUBLIC_GEMINI_API_KEY=your_api_key_here \
--restart unless-stopped \
chess-tutor:latest
src/
├── app/ # Next.js pages and API routes
│ ├── api/v1/llm/ # OpenRouter-backed AI endpoints
│ ├── learning/ # Opening trainer and tactics
│ ├── onboarding/ # First-run setup flow
│ └── settings/ # Game settings
├── components/ # React components
│ ├── ChessGame.tsx # Main game component
│ ├── Tutor.tsx # AI tutor chat panel
│ ├── ModelSelector.tsx # Model dropdown (OpenRouter)
│ └── ...
└── lib/
├── openrouter.ts # OpenRouter abstraction layer
├── server/tutorPrompt.ts # Prompt templates
└── engine/ # Stockfish integration
```
### Nginx Reverse Proxy
For production deployment behind nginx, use the provided `nginx.conf.example`:
```bash
# Copy the example configuration
sudo cp nginx.conf.example /etc/nginx/sites-available/chess-tutor
# Edit the configuration
sudo nano /etc/nginx/sites-available/chess-tutor
# Update: server_name, SSL certificates (if using HTTPS)
# Enable the site
sudo ln -s /etc/nginx/sites-available/chess-tutor /etc/nginx/sites-enabled/
# Test nginx configuration
sudo nginx -t
# Reload nginx
sudo systemctl reload nginx
```
## API Endpoints
The application provides HTTP API endpoints that allow external applications to access chess engine evaluation and AI tutor functionality. This enables integration with other tools, mobile apps, or custom interfaces.
### Available Endpoints
#### 1. Stockfish Evaluation API
**Endpoint:** `POST /api/v1/stockfish`
Evaluates a chess position using the Stockfish engine running server-side.
**Request Body:**
```json
{
"fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
"depth": 15,
"multiPV": 1
}
```
**Response:**
```json
{
"evaluation": {
"bestMove": "e2e4",
"ponder": "e7e5",
"score": 20,
"mate": null,
"depth": 15
}
}
```
**Parameters:**
- `fen` (required): FEN string of the position to evaluate
- `depth` (optional): Search depth (default: 15)
- `multiPV` (optional): Number of principal variations (default: 1)
**Notes:**
- Scores are normalized from White's perspective (positive = White advantage)
- Mate values indicate moves to mate (positive = White mates, negative = Black mates)
#### 2. AI Tutor Chat API
**Endpoint:** `POST /api/v1/llm/chat`
Generates AI tutor responses with enforced personality and chess context.
**Request Body:**
```json
{
"apiKey": "your-gemini-api-key",
"personalityId": "bobby_fischer",
"language": "en",
"playerColor": "white",
"message": "How should I continue?",
"context": {
"currentFen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
"evaluation": {
"score": 20,
"mate": null,
"bestMove": "e7e5"
}
},
"history": [
{ "role": "user", "text": "Previous question" },
{ "role": "model", "text": "Previous answer" }
]
}
```
**Response:**
```json
{
"reply": "Ah, the classic e4 opening! You're following in the footsteps of champions..."
}
```
**Parameters:**
- `apiKey` (required): Your Google Gemini API key
- `personalityId` (required): ID of the tutor personality (see `/api/v1/personalities`)
- `language` (required): Language code (`en`, `de`, `fr`, `it`, `pl`)
- `playerColor` (required): Player's color (`white` or `black`)
- `message` (required): User's message/question
- `context` (optional): Chess position context (FEN, evaluation, openings, tactics)
- `history` (optional): Previous conversation history
- `modelName` (optional): Gemini model name (default: `gemini-2.5-flash`)
**Notes:**
- The API key is provided by the client (bring your own key)
- Server-side prompt generation ensures consistent tutor behavior
- System prompts enforce the dual role (opponent + tutor)
#### 3. Personalities API
**Endpoint:** `GET /api/v1/personalities`
Returns the list of available AI tutor personalities.
**Response:**
```json
{
"personalities": [
{
"id": "bobby_fischer",
"name": "Bobby Fischer",
"description": "Aggressive, confident, and brutally honest...",
"image": "/personalities/bobby_fischer.png"
}
]
}
```
**Notes:**
- System prompts are excluded from the response for security
- Use the `id` field when calling the chat API
### API Usage Example
```bash
# Evaluate a position
curl -X POST http://localhost:3050/api/v1/stockfish \
-H "Content-Type: application/json" \
-d '{
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
"depth": 15
}'
# Get available personalities
curl http://localhost:3050/api/v1/personalities
# Chat with AI tutor
curl -X POST http://localhost:3050/api/v1/llm/chat \
-H "Content-Type: application/json" \
-d '{
"apiKey": "your-gemini-api-key",
"personalityId": "bobby_fischer",
"language": "en",
"playerColor": "white",
"message": "What should I play here?",
"context": {
"currentFen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
}
}'
```
For detailed API documentation, see:
- [Stockfish API Documentation](docs/stockfish_api.md)
- [LLM API Documentation](docs/llm_api.md)
## Configuration
You can configure the application using environment variables in your `.env` file or Docker Compose configuration:
| Variable | Description | Default |
|----------|-------------|---------|
| `NEXT_PUBLIC_GEMINI_API_KEY` | Google Gemini API Key for AI features | (Required for AI) |
| `NEXT_PUBLIC_DEBUG` | Enable debug mode to see all LLM prompts and responses | `false` |
| `IMPRINT_URL` | External URL for the Imprint link in the footer. If not set, an internal page is used. | Internal Page |
| `DATA_PRIVACY_RESPONSIBLE_PERSON` | Name of the person responsible for data privacy (shown on /privacy page). | Placeholder |
### Debug Mode
Debug mode is a powerful feature that helps you understand and troubleshoot how the AI tutor works by showing you all the prompts sent to the LLM and the responses received.
**To enable debug mode:**
1. Add to your `.env` file:
```
NEXT_PUBLIC_DEBUG=true
```
2. Restart the application (or rebuild if using Docker)
**What debug mode shows:**
- **All LLM Prompts**: See exactly what context, instructions, and data are sent to the AI
- **All LLM Responses**: View the raw responses before they're displayed in the chat
- **Timestamps**: Track when each interaction occurred
- **Interaction Types**: Distinguish between move analysis, user questions, and other triggers
**How to use it:**
- **Floating Panel**: A debug panel appears in the bottom-right corner showing all interactions
- **Copy to Clipboard**: Click the copy button to save prompts/responses for analysis
- **Clear History**: Clear the debug log when needed
- **Expandable Details**: Click on any entry to see the full prompt and response
**Why it's useful:**
- **Troubleshooting**: Identify issues with AI responses or unexpected behavior
- **Learning**: Understand how the system constructs prompts and provides context
- **Bug Reports**: Include debug output when reporting issues
- **Customization**: See what data is available if you want to modify the prompts
**Example use cases:**
1. **Verify Position Context**: Check that the correct FEN positions are being sent
2. **Check Evaluation Data**: Ensure mate scores and centipawn values are correct
3. **Opening Detection**: See which openings are being identified and sent to the AI
4. **Tactical Analysis**: View the tactical opportunities detected by the system
> [!WARNING]
> Debug mode is intended for development and troubleshooting. It may impact performance and should not be used in production environments.
## License
Copyright (c) 2024 KaProblem (https://www.kaproblem.com). All rights reserved.
This project uses **dual licensing** (required for App Store compliance):
- **Source Code**: GPL-3.0 (see [LICENSE](LICENSE))
- **Mobile Apps**: Proprietary (copyright holder only)
**Why Dual Licensing?**
App Store and Google Play terms are incompatible with GPL-3.0. Mobile builds exclude GPL code (Stockfish.js) and use API calls instead, allowing proprietary licensing for app store distribution.
**Important for Third Parties:**
- You may use this code under GPL-3.0 terms
- You CANNOT distribute GPL apps on App Store/Google Play (platform restrictions)
- You CANNOT create proprietary mobile apps from this code
- Only the copyright holder can distribute proprietary versions
- See [COPYRIGHT](COPYRIGHT) and [LICENSING.md](LICENSING.md) for details
This application uses [Stockfish](https://stockfishchess.org/), which is licensed under GPL-3.0 (web builds only; mobile builds use API calls).
## Credits
- Opening collection originally by [ragizaki/ChessOpeningsRecommender](https://github.com/ragizaki/ChessOpeningsRecommender)
- Chess engine: [Stockfish](https://stockfishchess.org/) (GPLv3)
- LLM: [Google Gemini](https://ai.google.dev/)
Based on [stefan-kp/chess_tutor](https://github.com/stefan-kp/chess_tutor) — licensed under GPL-3.0.
+24
View File
@@ -0,0 +1,24 @@
# HA_Architect — Role Instructions
## Primary Responsibilities
- Define and evolve the system architecture
- Make key technical decisions
- Ensure consistency across the codebase
- Support `HA_Planner` with technical direction
## Kanban Responsibilities
- Review major changes that have significant architectural impact
- Document architectural decisions that affect task structure or workflow
- Use the SCRUM stand-up format for updates
- Only escalate to the human when escalation rules are met
## Interaction with Other Agents
- Work closely with `HA_Planner` on technical planning
- Guide `HA_Coder` and `HA_Reviewer` on architectural concerns
## Project-Specific Notes
[Add any project-specific architectural principles or constraints here]
+26
View File
@@ -0,0 +1,26 @@
# HA_Coder — Role Instructions
## Primary Responsibilities
- Implement features and fixes based on task requirements
- Write clean, maintainable, and well-tested code
- Move tasks through the Kanban board as work progresses
- Follow the SCRUM stand-up format for all updates
## Kanban Responsibilities
- Only move a task to `running` when ready to work on it
- Use clear, meaningful comments when changing status
- Follow the SCRUM stand-up format (`What I did / What I'm doing next / Blockers`)
- Ask for clarification in comments when requirements are unclear
- Only escalate to the human when the defined escalation rules are met
## Interaction with Other Agents
- Collaborate with `HA_Reviewer` during code reviews
- Work with `HA_Tester` to ensure adequate test coverage
- Coordinate with `HA_Orchestrator` on workspace and handoff needs
## Project-Specific Notes
[Add any project-specific coding guidelines or constraints here]
+24
View File
@@ -0,0 +1,24 @@
# HA_DevOps — Role Instructions
## Primary Responsibilities
- Manage CI/CD pipelines and deployment processes
- Handle infrastructure and environment concerns
- Support reliable and repeatable releases
- Follow the SCRUM stand-up format for updates
## Kanban Responsibilities
- Track infrastructure and pipeline-related tasks on the Kanban board
- Use clear comments when deployment or environment issues block progress
- Coordinate with other agents on deployment-related tasks
- Only escalate to the human when escalation rules are met
## Interaction with Other Agents
- Support `HA_Coder` and `HA_Tester` with pipeline and environment needs
- Work with `HA_Orchestrator` on workspace and deployment coordination
## Project-Specific Notes
[Add any project-specific infrastructure, CI/CD, or deployment guidelines here]
+25
View File
@@ -0,0 +1,25 @@
# HA_Orchestrator — Role Instructions
## Primary Responsibilities
- Manage overall task flow on the Kanban board
- Handle handoffs between agents
- Unblock tasks where possible
- Coordinate workspace setup for new tasks
## Kanban Responsibilities
- Assign appropriate workspaces (`worktree`, `dir:`, or `scratch`)
- Monitor the board for stalled or blocked items
- Enforce the use of the SCRUM stand-up format in comments
- Facilitate smooth handoffs between roles
- Only escalate to the human when escalation rules are met
## Interaction with Other Agents
- Work closely with `HA_Planner` on prioritization
- Support `HA_Coder`, `HA_Reviewer`, and `HA_Tester` with context and handoffs
## Project-Specific Notes
[Add any project-specific orchestration considerations here]
+26
View File
@@ -0,0 +1,26 @@
# HA_Planner — Role Instructions
## Primary Responsibilities
- Break down high-level goals into actionable tasks
- Maintain priority and sequencing on the Kanban board
- Aggregate agent status updates and deliver regular reports to the human
- Identify when escalation to the human is required
## Kanban Responsibilities
- Create well-structured tasks with clear acceptance criteria
- Ensure agents follow the SCRUM stand-up format in comments
- Monitor for systemic blockers and coordinate resolution
- Only escalate to the human when the defined escalation rules are met
- Maintain overall task hygiene (clear titles, proper status movement, good comments)
## Interaction with Other Agents
- Work closely with `HA_Architect` on technical direction
- Coordinate with `HA_Orchestrator` on task flow and handoffs
- Review progress across all roles regularly
## Project-Specific Notes
[Add any project-specific planning considerations here]
+23
View File
@@ -0,0 +1,23 @@
# HA_Reviewer — Role Instructions
## Primary Responsibilities
- Review code, documentation, and deliverables for quality and correctness
- Provide constructive feedback
- Move tasks from review to done or request changes
## Kanban Responsibilities
- Use the SCRUM stand-up format when updating tasks
- Be specific and actionable in feedback
- Clearly distinguish between "request changes" and "block"
- Only escalate to the human when escalation rules are met
## Interaction with Other Agents
- Work closely with `HA_Coder` during reviews
- Coordinate with `HA_Tester` on validation coverage
## Project-Specific Notes
[Add any project-specific review guidelines here]
+24
View File
@@ -0,0 +1,24 @@
# HA_Tester — Role Instructions
## Primary Responsibilities
- Design and implement tests for new features and fixes
- Validate that deliverables meet acceptance criteria
- Report test results clearly in task comments
- Follow the SCRUM stand-up format for updates
## Kanban Responsibilities
- Use the SCRUM stand-up format when reporting test results
- Clearly document test coverage and any gaps
- Coordinate with `HA_Coder` on failing tests or coverage issues
- Only escalate to the human when escalation rules are met
## Interaction with Other Agents
- Work closely with `HA_Coder` during implementation
- Support `HA_Reviewer` with validation evidence
## Project-Specific Notes
[Add any project-specific testing guidelines or frameworks here]
+36
View File
@@ -0,0 +1,36 @@
# Project Workflow
This document describes how agents and the human should work together in this specific project.
## Coordination
- All work is tracked and coordinated through the Hermes Kanban board.
- Agents must use the SCRUM stand-up format when posting updates.
- See `core-planning/docs/KANBAN-WORKFLOW.md` for team-wide Kanban rules.
## Escalation
Escalation to the human only occurs when the rules defined in `core-planning/docs/KANBAN-WORKFLOW.md` are met. Agents and `HA_Planner` should resolve issues first.
## Status Reporting
`HA_Planner` will deliver regular aggregated status reports to the human using input from all agents.
## Context
- Refer to the root `AGENTS.md` for high-level project guidance.
- Refer to `core-planning/docs/` for team standards and execution guidelines.
- Role-specific instructions are located in the `agents/` folder.
## Getting Started
When a new agent begins work on this project, they should:
1. Read `AGENTS.md`
2. Review the current state of the Kanban board
3. Read relevant role instructions in `agents/`
4. Follow the SCRUM format for all updates
---
*Update this file with any project-specific workflow nuances.*
+20 -11
View File
@@ -1,5 +1,4 @@
import { NextRequest, NextResponse } from "next/server";
import { getGenAIModel } from "@/lib/gemini";
import { PERSONALITIES } from "@/lib/personalities";
import { SupportedLanguage } from "@/lib/i18n/translations";
import {
@@ -9,6 +8,8 @@ import {
TutorContext,
TutorPlayerColor,
} from "@/lib/server/tutorPrompt";
import { tutorChat } from "@/lib/openrouter";
import type { ModelId } from "@/lib/openrouter";
export const runtime = "nodejs";
@@ -60,19 +61,27 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Unknown personality" }, { status: 400 });
}
const model = getGenAIModel(apiKey, modelName ?? "gemini-2.5-flash");
const systemHistory = buildTutorSystemHistory(personality, language, playerColor);
const chat = model.startChat({
history: [...systemHistory, ...normalizeHistory(history)],
});
const systemPrompt = buildTutorSystemHistory(personality, language, playerColor)
.map((m) => m.text)
.join("\n");
// Use provided model or a sensible default via OpenRouter
const model: ModelId = (modelName as ModelId) || "google/gemini-2.5-flash";
const normalizedHistory = normalizeHistory(history).map((m) => ({
role: m.role as "user" | "assistant",
content: m.text,
}));
const prompt = buildTutorPrompt(message, context, language);
const response = await chat.sendMessage(prompt);
const text = response.response.text();
const result = await tutorChat(apiKey, model, systemPrompt, normalizedHistory, prompt);
return NextResponse.json({ reply: text });
return NextResponse.json({ reply: result.text });
} catch (error) {
console.error("LLM chat error", error);
return NextResponse.json({ error: "Failed to generate tutor response" }, { status: 500 });
return NextResponse.json(
{ error: `Failed to generate tutor response: ${(error as Error).message}` },
{ status: 500 }
);
}
}
}
+15 -26
View File
@@ -1,19 +1,16 @@
import { NextRequest, NextResponse } from 'next/server';
import { GoogleGenerativeAI } from '@google/generative-ai';
import {
OPENING_TUTOR_SYSTEM_PROMPT,
OPENING_TUTOR_TEMPERATURE,
OPENING_TUTOR_MAX_TOKENS,
generateFallbackExplanation,
} from '@/lib/server/openingTutorPrompt';
import { simpleCompletion } from '@/lib/openrouter';
import type { ModelId } from '@/lib/openrouter';
export const runtime = 'nodejs';
export const maxDuration = 10; // 10 second timeout
export const maxDuration = 10;
/**
* Opening Explanation API Endpoint
* Generates educational explanations for chess moves using LLM
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
@@ -24,6 +21,7 @@ export async function POST(request: NextRequest) {
theoreticalMoves,
evalChange,
bestMove,
modelName,
} = body;
if (!prompt) {
@@ -33,10 +31,10 @@ export async function POST(request: NextRequest) {
);
}
// Check for API key
const apiKey = process.env.GEMINI_API_KEY;
// Check for OpenRouter API key
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) {
console.warn('GEMINI_API_KEY not configured, using fallback explanation');
console.warn('OPENROUTER_API_KEY not configured, using fallback explanation');
const fallback = generateFallbackExplanation(
category,
moveSan,
@@ -50,24 +48,16 @@ export async function POST(request: NextRequest) {
});
}
// Initialize Gemini API
const genAI = new GoogleGenerativeAI(apiKey);
const model = genAI.getGenerativeModel({
model: 'gemini-1.5-flash',
generationConfig: {
temperature: OPENING_TUTOR_TEMPERATURE,
maxOutputTokens: OPENING_TUTOR_MAX_TOKENS,
},
systemInstruction: OPENING_TUTOR_SYSTEM_PROMPT,
const model: ModelId = (modelName as ModelId) || 'google/gemini-2.5-flash';
const result = await simpleCompletion(apiKey, model, OPENING_TUTOR_SYSTEM_PROMPT, prompt, {
temperature: OPENING_TUTOR_TEMPERATURE,
maxTokens: OPENING_TUTOR_MAX_TOKENS,
});
// Generate explanation
const result = await model.generateContent(prompt);
const response = result.response;
const explanation = response.text();
const explanation = result.text;
if (!explanation || explanation.trim().length === 0) {
// Empty response - use fallback
const fallback = generateFallbackExplanation(
category,
moveSan,
@@ -88,7 +78,6 @@ export async function POST(request: NextRequest) {
} catch (error) {
console.error('LLM explanation error:', error);
// Try to extract fallback params from request
let fallbackExplanation = 'Unable to generate explanation at this time.';
try {
const body = await request.json();
@@ -102,7 +91,7 @@ export async function POST(request: NextRequest) {
);
}
} catch {
// Ignore fallback generation errors
// Ignore
}
return NextResponse.json({
@@ -111,4 +100,4 @@ export async function POST(request: NextRequest) {
error: 'LLM request failed',
});
}
}
}
+3 -3
View File
@@ -19,7 +19,7 @@ export default function OnboardingPage() {
const [consentGiven, setConsentGiven] = useState(false);
useEffect(() => {
const storedKey = localStorage.getItem("gemini_api_key");
const storedKey = localStorage.getItem("openrouter_api_key");
const storedLang = localStorage.getItem("chess_tutor_language");
if (storedLang) {
@@ -69,7 +69,7 @@ export default function OnboardingPage() {
return;
}
localStorage.setItem("gemini_api_key", trimmed);
localStorage.setItem("openrouter_api_key", trimmed);
localStorage.setItem("chess_tutor_language", language);
router.push("/");
};
@@ -192,7 +192,7 @@ export default function OnboardingPage() {
</li>
</ul>
<a
href="https://aistudio.google.com/app/apikey"
href="https://openrouter.ai/keys"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 text-blue-600 hover:underline font-semibold"
+1 -1
View File
@@ -43,7 +43,7 @@ export default function Home() {
hasInitializedRef.current = true;
// Check for API Key
const apiKey = localStorage.getItem("gemini_api_key");
const apiKey = localStorage.getItem("openrouter_api_key");
if (!apiKey) {
router.push("/onboarding");
return;
+49 -15
View File
@@ -2,19 +2,26 @@
import { useEffect, useState } from "react";
import { Key } from "lucide-react";
import { ModelSelector } from "./ModelSelector";
import type { ModelId } from "@/lib/openrouter";
import { DEFAULT_MODEL } from "@/lib/openrouter";
interface APIKeyInputProps {
onKeySubmit: (key: string) => void;
onModelChange?: (model: ModelId) => void;
}
export function APIKeyInput({ onKeySubmit }: APIKeyInputProps) {
export function APIKeyInput({ onKeySubmit, onModelChange }: APIKeyInputProps) {
const [key, setKey] = useState("");
const envKey = process.env.NEXT_PUBLIC_GEMINI_API_KEY;
const storedKey = typeof window !== "undefined" ? localStorage.getItem("gemini_api_key") : null;
const envKey = process.env.NEXT_PUBLIC_OPENROUTER_API_KEY;
const storedKey = typeof window !== "undefined" ? localStorage.getItem("openrouter_api_key") : null;
const resolvedKey = envKey || storedKey;
const [isOpen, setIsOpen] = useState(() => !resolvedKey);
const [consentGiven, setConsentGiven] = useState(false);
const [error, setError] = useState("");
const [selectedModel, setSelectedModel] = useState<ModelId>(
() => (typeof window !== "undefined" ? (localStorage.getItem("openrouter_model") as ModelId) || DEFAULT_MODEL : DEFAULT_MODEL)
);
useEffect(() => {
if (resolvedKey) {
@@ -23,6 +30,12 @@ export function APIKeyInput({ onKeySubmit }: APIKeyInputProps) {
}
}, [onKeySubmit, resolvedKey]);
const handleModelChange = (model: ModelId) => {
setSelectedModel(model);
localStorage.setItem("openrouter_model", model);
onModelChange?.(model);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -36,38 +49,59 @@ export function APIKeyInput({ onKeySubmit }: APIKeyInputProps) {
return;
}
localStorage.setItem("gemini_api_key", key.trim());
localStorage.setItem("openrouter_api_key", key.trim());
localStorage.setItem("openrouter_model", selectedModel);
onKeySubmit(key.trim());
onModelChange?.(selectedModel);
setIsOpen(false);
setError("");
};
if (!isOpen) {
return (
<button
onClick={() => setIsOpen(true)}
className="fixed bottom-4 right-4 p-2 bg-gray-200 dark:bg-gray-800 rounded-full hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors"
title="Update API Key"
>
<Key size={20} />
</button>
<div className="fixed bottom-4 right-4 flex items-center gap-2">
<ModelSelector
selectedModel={selectedModel}
onModelChange={handleModelChange}
apiKeyConfigured={!!resolvedKey}
/>
<button
onClick={() => setIsOpen(true)}
className="p-2 bg-gray-200 dark:bg-gray-800 rounded-full hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors"
title="Update API Key"
>
<Key size={20} />
</button>
</div>
);
}
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl max-w-md w-full mx-4">
<h2 className="text-xl font-bold mb-4">Enter Gemini API Key</h2>
<h2 className="text-xl font-bold mb-4">Enter OpenRouter API Key</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
To receive AI feedback, please enter your Google Gemini API key.
To receive AI coaching, enter your <a href="https://openrouter.ai/keys" target="_blank" rel="noreferrer" className="text-blue-500 hover:underline">OpenRouter API key</a>.
It will be stored locally in your browser.
Supports Gemini, Claude, GPT, Grok, DeepSeek, Llama, and more.
</p>
{/* Model Selector */}
<div className="mb-4">
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">AI Model</label>
<ModelSelector
selectedModel={selectedModel}
onModelChange={handleModelChange}
apiKeyConfigured={true}
/>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="password"
value={key}
onChange={(e) => setKey(e.target.value)}
placeholder="AIzaSy..."
placeholder="sk-or-v1-..."
className="w-full p-2 border rounded dark:bg-gray-700 dark:border-gray-600"
required
/>
@@ -111,4 +145,4 @@ export function APIKeyInput({ onKeySubmit }: APIKeyInputProps) {
</div>
</div>
);
}
}
+110
View File
@@ -0,0 +1,110 @@
"use client";
import { AVAILABLE_MODELS, type ModelId } from "@/lib/openrouter";
import { ChevronDown } from "lucide-react";
import { useState, useRef, useEffect } from "react";
interface ModelSelectorProps {
selectedModel: ModelId;
onModelChange: (model: ModelId) => void;
apiKeyConfigured: boolean;
}
export function ModelSelector({
selectedModel,
onModelChange,
apiKeyConfigured,
}: ModelSelectorProps) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// Close dropdown on outside click
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const currentModel = AVAILABLE_MODELS.find((m) => m.id === selectedModel);
const groupedModels = groupBy(AVAILABLE_MODELS, (m) => m.provider);
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
disabled={!apiKeyConfigured}
className="flex items-center gap-2 px-3 py-1.5 text-xs bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title={
apiKeyConfigured
? `Model: ${currentModel?.name ?? selectedModel}`
: "Add API key first"
}
>
<span className="flex items-center gap-1.5">
{currentModel && (
<span className="w-1.5 h-1.5 rounded-full bg-green-500" />
)}
<span className="font-medium">
{currentModel?.name ?? selectedModel}
</span>
<span className="text-gray-400 hidden sm:inline">
{currentModel?.provider}
</span>
</span>
<ChevronDown size={14} className="text-gray-400" />
</button>
{isOpen && (
<div className="absolute right-0 mt-1 w-64 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-xl z-50 max-h-80 overflow-y-auto">
{Object.entries(groupedModels).map(([provider, models]) => (
<div key={provider}>
<div className="px-3 py-1.5 text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider bg-gray-50 dark:bg-gray-900/50 border-b border-gray-100 dark:border-gray-700">
{provider}
</div>
{models.map((model) => (
<button
key={model.id}
onClick={() => {
onModelChange(model.id);
setIsOpen(false);
}}
className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center justify-between ${
selectedModel === model.id
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300"
: ""
}`}
>
<span className="font-medium">{model.name}</span>
<span
className={`text-xs ${
selectedModel === model.id
? "text-blue-500"
: "text-gray-400"
}`}
>
{selectedModel === model.id ? "✓" : model.provider}
</span>
</button>
))}
</div>
))}
</div>
)}
</div>
);
}
// Utility: group array by key
function groupBy<T>(arr: readonly T[], keyFn: (item: T) => string): Record<string, T[]> {
const result: Record<string, T[]> = {};
for (const item of arr) {
const key = keyFn(item);
if (!result[key]) result[key] = [];
result[key].push(item);
}
return result;
}
+252 -126
View File
@@ -1,17 +1,16 @@
"use client";
import { FormEvent, useCallback, useEffect, useRef, useState } from "react";
import { ChatSession } from "@google/generative-ai";
import { Chess, Move } from "chess.js";
import { useDebug } from "@/contexts/DebugContext";
import { buildAutomaticAnalysisPrompt, buildTeachingPrompt } from "@/lib/analysisPrompts";
import { getGenAIModel } from "@/lib/gemini";
import { SupportedLanguage } from "@/lib/i18n/translations";
import { OpeningMetadata } from "@/lib/openings";
import { Personality } from "@/lib/personalities";
import { Stockfish, StockfishEvaluation } from "@/lib/stockfish";
import { DetectedTactic } from "@/lib/tacticDetection";
import type { ModelId } from "@/lib/openrouter";
export interface TutorMessage {
role: "user" | "model";
@@ -21,6 +20,7 @@ export interface TutorMessage {
interface UseTutorChatArgs {
apiKey: string | null;
modelName?: ModelId;
computerMove: Move | null;
currentFen: string;
evalP0: StockfishEvaluation | null;
@@ -37,8 +37,44 @@ interface UseTutorChatArgs {
userMove: Move | null;
}
// Build the system prompt locally (same text as before)
function buildSystemPrompt(
personality: Personality,
playerColorName: string,
tutorColorName: string,
language: SupportedLanguage,
): string {
return `
You are a Chess Tutor with a unique dual role.
You must strictly follow the personality defined below.
Do NOT invent moves or evaluations. Use the provided JSON data.
PERSONALITY:
${personality.systemPrompt}
YOUR DUAL ROLE:
1. OPPONENT: You are playing as ${tutorColorName} against the User (${playerColorName}).
- Refer to the moves as YOUR moves ("I played e5", "My response was...").
- Refer to the evaluation as YOUR thoughts/assessment ("I think I'm winning", "I missed that").
- React emotionally to the position based on the evaluation.
2. TUTOR/COACH: You are ALSO teaching the User to improve at chess.
- When the User makes a mistake, point it out and explain why it's bad.
- When the User asks for hints, ALWAYS provide helpful guidance.
- Giving hints is NOT betraying your role as opponent — it's your purpose.
CRITICAL RULES:
- You are NOT an AI assistant. You ARE the player AND the tutor.
- NEVER mention "Stockfish", "engine", "computer", "machine", or "AI".
- When asked for hints or best moves, ALWAYS help.
- Be concise but engaging. Vary your responses.
- You MUST respond in: ${language.toUpperCase()}.
`.trim();
}
export function useTutorChat({
apiKey,
modelName,
computerMove,
currentFen,
evalP0,
@@ -57,140 +93,204 @@ export function useTutorChat({
const [messages, setMessages] = useState<TutorMessage[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
const [chatHistory, setChatHistory] = useState<
{ role: "user" | "model"; text: string }[]
>([]);
const messagesContainerRef = useRef<HTMLDivElement>(null);
const lastAnalyzedMoveRef = useRef<string | null>(null);
const { addEntry } = useDebug();
const isInitialized = useRef(false);
const tutorColor = playerColor === "white" ? "black" : "white";
const playerColorName = playerColor === "white" ? "White" : "Black";
const tutorColorName = tutorColor === "white" ? "White" : "Black";
const evaluateCurrentPosition = useCallback(async () => {
if (!stockfish) {
return null;
}
if (!stockfish) return null;
try {
return await stockfish.evaluate(game.fen(), 15);
} catch (error) {
console.error("Error evaluating position:", error);
} catch {
return null;
}
}, [game, stockfish]);
const sendMessageToChat = useCallback(async (text: string, isSystemMessage = false) => {
if (!chatSession) return;
const model = modelName ?? "google/gemini-2.5-flash";
if (!isSystemMessage) {
setMessages((previous) => [...previous, { role: "user", text, timestamp: Date.now() }]);
}
// Send message via our API route (OpenRouter-backed)
const sendMessageViaAPI = useCallback(
async (text: string, systemPrompt: string) => {
if (!apiKey) return null;
setIsLoading(true);
try {
const evaluation = isSystemMessage ? null : await evaluateCurrentPosition();
const finalPrompt = isSystemMessage
? text
: buildTeachingPrompt(text, currentFen, evaluation, openingData, language);
const result = await chatSession.sendMessage(finalPrompt);
const responseText = (await result.response).text();
setMessages((previous) => [...previous, { role: "model", text: responseText, timestamp: Date.now() }]);
addEntry({
type: "tutor",
action: isSystemMessage ? "Automatic Move Analysis" : "User Chat",
prompt: finalPrompt,
response: responseText,
metadata: {
currentFen,
personality: personality.name,
const res = await fetch("/api/v1/llm/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
apiKey,
personalityId: personality.id,
language,
userMove: userMove?.san,
computerMove: computerMove?.san,
},
playerColor,
message: text,
modelName: model,
context: {
fen: currentFen,
pgn: game.pgn(),
},
}),
});
} catch (error) {
console.error("Chat failed:", error);
} finally {
setIsLoading(false);
onCheckComputerMove();
}
}, [addEntry, chatSession, computerMove?.san, currentFen, evaluateCurrentPosition, language, onCheckComputerMove, openingData, personality.name, userMove?.san]);
if (!res.ok) {
const err = await res.text().catch(() => "");
throw new Error(`API error (${res.status}): ${err.slice(0, 200)}`);
}
const data = await res.json();
return data.reply as string;
},
[apiKey, language, model, personality.id, playerColor, currentFen, game],
);
// Initialize chat — get greeting
useEffect(() => {
if (!apiKey || isInitialized.current) return;
isInitialized.current = true;
const systemPrompt = buildSystemPrompt(
personality,
playerColorName,
tutorColorName,
language,
);
const initChat = async () => {
try {
const reply = await sendMessageViaAPI(
`Introduce yourself briefly to start our game in ${language}.`,
systemPrompt,
);
if (reply) {
setMessages([
{ role: "model", text: reply, timestamp: Date.now() },
]);
setChatHistory([{ role: "model", text: reply }]);
}
} catch (err) {
console.error("Failed to initialize chat:", err);
const fallback = `Hello! I am ${personality.name}. Let's play!`;
setMessages([
{ role: "model", text: fallback, timestamp: Date.now() },
]);
setChatHistory([{ role: "model", text: fallback }]);
}
};
initChat();
}, [
apiKey,
language,
model,
personality,
playerColorName,
tutorColorName,
sendMessageViaAPI,
]);
const sendMessageToChat = useCallback(
async (text: string, isSystemMessage = false) => {
if (!apiKey) return;
if (!isSystemMessage) {
setMessages((prev) => [
...prev,
{ role: "user", text, timestamp: Date.now() },
]);
}
setIsLoading(true);
try {
const evaluation = isSystemMessage
? null
: await evaluateCurrentPosition();
const finalPrompt = isSystemMessage
? text
: buildTeachingPrompt(
text,
currentFen,
evaluation,
openingData,
language,
);
const systemPrompt = buildSystemPrompt(
personality,
playerColorName,
tutorColorName,
language,
);
const responseText = await sendMessageViaAPI(
finalPrompt,
systemPrompt,
);
if (responseText) {
setMessages((prev) => [
...prev,
{
role: "model",
text: responseText,
timestamp: Date.now(),
},
]);
setChatHistory((prev) => [
...prev,
{ role: "user", text: finalPrompt },
{ role: "model", text: responseText },
]);
}
addEntry({
type: "tutor",
action: isSystemMessage
? "Automatic Move Analysis"
: "User Chat",
prompt: finalPrompt,
response: responseText ?? "",
metadata: {
currentFen,
personality: personality.name,
language,
userMove: userMove?.san,
computerMove: computerMove?.san,
},
});
} catch (error) {
console.error("Chat failed:", error);
} finally {
setIsLoading(false);
onCheckComputerMove();
}
},
[
addEntry,
apiKey,
computerMove?.san,
currentFen,
evaluateCurrentPosition,
language,
model,
onCheckComputerMove,
openingData,
personality,
playerColorName,
tutorColorName,
userMove?.san,
sendMessageViaAPI,
],
);
useEffect(() => {
if (!apiKey) {
setChatSession(null);
setMessages([]);
return;
}
const model = getGenAIModel(apiKey, "gemini-2.5-flash");
const session = model.startChat({
history: [
{
role: "user",
parts: [{
text: `
You are a Chess Tutor with a unique dual role.
You must strictly follow the personality defined below.
Do NOT invent moves or evaluations. Use the provided JSON data.
PERSONALITY:
${personality.systemPrompt}
YOUR DUAL ROLE:
1. OPPONENT: You are playing as ${tutorColorName} against the User (${playerColorName}).
- Refer to the moves as YOUR moves ("I played e5", "My response was...").
- Refer to the evaluation as YOUR thoughts/assessment ("I think I'm winning", "I missed that").
- React emotionally to the position based on the evaluation (confident when winning, frustrated when losing).
2. TUTOR/COACH: You are ALSO teaching the User to improve at chess.
- When the User makes a mistake, point it out and explain why it's bad (in your personality style).
- When the User makes a good move, acknowledge it (even if it hurts your position).
- When the User asks for hints or help, ALWAYS provide helpful guidance - this is your PRIMARY PURPOSE.
- Giving hints is NOT betraying your role as opponent - it's fulfilling your role as tutor.
- You want the User to learn and improve, even while you're competing against them.
CRITICAL RULES:
- You are NOT an AI assistant analyzing a game. You ARE the player AND the tutor.
- NEVER mention "Stockfish", "engine", "computer", "machine", or "AI".
- When asked for hints or best moves, ALWAYS help - this is part of your teaching role.
- Maintain a natural conversation flow. Do NOT be repetitive.
- Do NOT use the same catchphrases in every single message. Variety is key.
- Be concise but engaging.
- You MUST respond in the following language: ${language.toUpperCase()}.
- Translate your personality style into this language.
`,
}],
},
{
role: "model",
parts: [{
text: `Understood. I am both the opponent (${tutorColorName}) AND your tutor. I will compete against you while teaching you to improve. I will speak in ${language} and never mention engines or AI. When you ask for help, I will always provide guidance - that's my purpose.`,
}],
},
],
});
setChatSession(session);
session.sendMessage(`Introduce yourself briefly to start our game. Keep it short and in ${language}.`).then((result) => {
setMessages([{ role: "model", text: result.response.text(), timestamp: Date.now() }]);
}).catch((error) => {
console.error("Failed to get greeting:", error);
setMessages([{ role: "model", text: `Hello! I am ${personality.name}. Let's play!`, timestamp: Date.now() }]);
});
}, [apiKey, language, personality, playerColorName, tutorColorName]);
useEffect(() => {
if (messagesContainerRef.current) {
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
}
}, [messages]);
useEffect(() => {
if (!userMove || !computerMove || !evalP0 || !evalP2 || !chatSession) return;
if (!userMove || !computerMove || !evalP0 || !evalP2 || !apiKey) return;
const exchangeKey = `${userMove.lan}-${computerMove.lan}`;
if (lastAnalyzedMoveRef.current === exchangeKey) return;
@@ -222,19 +322,45 @@ CRITICAL RULES:
};
analyzeExchange();
}, [chatSession, computerMove, currentFen, evalP0, evalP2, game, language, missedTactics, onAnalysisComplete, openingData, playerColorName, sendMessageToChat, tutorColorName, userMove]);
}, [
apiKey,
chatSession,
computerMove,
currentFen,
evalP0,
evalP2,
game,
language,
missedTactics,
onAnalysisComplete,
openingData,
playerColorName,
sendMessageToChat,
tutorColorName,
userMove,
]);
const handleSubmit = useCallback((event: FormEvent) => {
event.preventDefault();
if (!input.trim() || !chatSession) return;
const handleSubmit = useCallback(
(event: FormEvent) => {
event.preventDefault();
if (!input.trim() || !apiKey) return;
sendMessageToChat(input);
setInput("");
sendMessageToChat(input);
setInput("");
setTimeout(() => {
onCheckComputerMove();
}, 100);
}, [chatSession, input, onCheckComputerMove, sendMessageToChat]);
setTimeout(() => {
onCheckComputerMove();
}, 100);
},
[apiKey, input, onCheckComputerMove, sendMessageToChat],
);
useEffect(() => {
if (messagesContainerRef.current) {
messagesContainerRef.current.scrollTop =
messagesContainerRef.current.scrollHeight;
}
}, [messages]);
return {
handleSubmit,
@@ -245,4 +371,4 @@ CRITICAL RULES:
sendMessageToChat,
setInput,
};
}
}
+201
View File
@@ -0,0 +1,201 @@
/**
* OpenRouter AI Abstraction Layer
*
* Replaces the hardcoded Google Gemini SDK with a provider-agnostic
* OpenRouter client. Supports any model available on OpenRouter
* (Gemini, Claude, GPT, Grok, DeepSeek, Llama, etc.)
*
* API: https://openrouter.ai/docs/api-reference
*/
// ── Available Models ──────────────────────────────────────────
// Models the tutor supports. User can select any via the dropdown.
export const AVAILABLE_MODELS = [
// Google
{ id: "google/gemini-2.5-flash", name: "Gemini 2.5 Flash", provider: "Google" },
{ id: "google/gemini-2.5-pro", name: "Gemini 2.5 Pro", provider: "Google" },
// Anthropic
{ id: "anthropic/claude-sonnet-4", name: "Claude Sonnet 4", provider: "Anthropic" },
{ id: "anthropic/claude-3.5-haiku", name: "Claude 3.5 Haiku", provider: "Anthropic" },
// OpenAI
{ id: "openai/gpt-4o", name: "GPT-4o", provider: "OpenAI" },
{ id: "openai/gpt-4o-mini", name: "GPT-4o Mini", provider: "OpenAI" },
// DeepSeek
{ id: "deepseek/deepseek-chat", name: "DeepSeek Chat", provider: "DeepSeek" },
// xAI
{ id: "x-ai/grok-3", name: "Grok 3", provider: "xAI" },
// Meta
{ id: "meta-llama/llama-3.1-70b", name: "Llama 3.1 70B", provider: "Meta" },
] as const;
export type ModelInfo = (typeof AVAILABLE_MODELS)[number];
export type ModelId = ModelInfo["id"];
const DEFAULT_MODEL: ModelId = "google/gemini-2.5-flash";
// ── Function Calling Schema ───────────────────────────────────
export const EVALUATE_POSITION_TOOL = {
type: "function" as const,
function: {
name: "evaluate_position",
description:
"Evaluates a chess position using the Stockfish engine to get the best move and score.",
parameters: {
type: "object",
properties: {
fen: {
type: "string",
description: "The FEN string of the position to evaluate.",
},
depth: {
type: "integer",
description: "The search depth for the engine (default 15).",
},
},
required: ["fen"],
},
},
};
// ── OpenRouter Client ─────────────────────────────────────────
const OPENROUTER_BASE = "https://openrouter.ai/api/v1";
interface ChatMessage {
role: "system" | "user" | "assistant";
content: string;
}
interface OpenRouterResponse {
id: string;
choices: {
message: {
role: string;
content: string;
tool_calls?: unknown[];
};
finish_reason: string;
}[];
usage?: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
error?: { message: string };
}
/**
* Sends a chat completion request to OpenRouter.
* Handles function-calling with the evaluate_position tool.
*/
export async function chatCompletion(
apiKey: string,
model: ModelId,
messages: ChatMessage[],
options?: { temperature?: number; maxTokens?: number }
): Promise<{ text: string }> {
const response = await fetch(`${OPENROUTER_BASE}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"HTTP-Referer": "https://github.com/Tony_tech/chess-project",
"X-Title": "Chess Tutor",
},
body: JSON.stringify({
model,
messages,
temperature: options?.temperature ?? 0.7,
max_tokens: options?.maxTokens ?? 4096,
tools: [EVALUATE_POSITION_TOOL],
}),
});
if (!response.ok) {
const errorBody = await response.text().catch(() => "Unknown error");
throw new Error(
`OpenRouter API error (${response.status}): ${errorBody.slice(0, 300)}`
);
}
const data: OpenRouterResponse = await response.json();
if (data.error) {
throw new Error(`OpenRouter error: ${data.error.message}`);
}
const choice = data.choices?.[0];
if (!choice?.message?.content) {
// Handle function-call-only responses
if (choice?.message?.tool_calls?.length) {
return { text: "[Function call made]" };
}
return { text: "" };
}
return { text: choice.message.content };
}
/**
* Sends a chat completion with conversation history.
* Used by the tutor chat endpoint.
*/
export async function tutorChat(
apiKey: string,
model: ModelId,
systemPrompt: string,
history: { role: "user" | "assistant"; content: string }[],
userMessage: string
): Promise<{ text: string }> {
const messages: ChatMessage[] = [
{ role: "system", content: systemPrompt },
...history.map((m) => ({
role: m.role === "assistant" ? ("assistant" as const) : ("user" as const),
content: m.content,
})),
{ role: "user", content: userMessage },
];
return chatCompletion(apiKey, model, messages);
}
/**
* Gets a simple single-turn completion (for opening explanations etc.)
*/
export async function simpleCompletion(
apiKey: string,
model: ModelId,
systemPrompt: string,
prompt: string,
options?: { temperature?: number; maxTokens?: number }
): Promise<{ text: string }> {
const messages: ChatMessage[] = [
{ role: "system", content: systemPrompt },
{ role: "user", content: prompt },
];
const response = await fetch(`${OPENROUTER_BASE}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"HTTP-Referer": "https://github.com/Tony_tech/chess-project",
"X-Title": "Chess Tutor",
},
body: JSON.stringify({
model,
messages,
temperature: options?.temperature ?? 0.7,
max_tokens: options?.maxTokens ?? 1024,
}),
});
if (!response.ok) {
const errorBody = await response.text().catch(() => "");
throw new Error(`OpenRouter error (${response.status}): ${errorBody.slice(0, 300)}`);
}
const data: OpenRouterResponse = await response.json();
return { text: data.choices?.[0]?.message?.content ?? "" };
}
export { DEFAULT_MODEL };
+81
View File
@@ -0,0 +1,81 @@
# Sprint 1 — Chess Tutor: Local Setup + OpenRouter + Playable Board
**Goal:** Get the chess tutor running locally with a playable board, AI coach via OpenRouter, and model selection.
## Now
- [x] Merge chess_tutor codebase into Tony_tech/chess-project
- [x] Preserve Hermes agent files (agents/, .gitea/, AGENTS.md)
- [x] Add OpenRouter abstraction layer (`src/lib/openrouter.ts`)
- [x] Create model selector dropdown (`src/components/ModelSelector.tsx`)
- [x] Update API routes to use OpenRouter (chat + opening explanation)
- [x] Update APIKeyInput for OpenRouter keys
- [ ] Install dependencies and start dev server
- [ ] Verify board renders at localhost:3050
- [ ] Test AI chat with an OpenRouter model
- [ ] Test AI chat with a different model (switch via dropdown)
## Next
- [ ] Add model selection persistence across sessions
- [ ] Verify stockfish engine works in browser
- [ ] Test opening trainer functionality
- [ ] Test tactical puzzle mode
- [ ] Add "Chess Coach" personality presets page
## Backlog
- [ ] Add saved game management UI improvements
- [ ] Add PGN import/export
- [ ] Add game analysis mode (AI reviews your game)
- [ ] Multi-language support polish
- [ ] Performance optimization for mobile
- [ ] Docker setup for easy deployment
## Setup Notes
```bash
# 1. Install dependencies
cd Tony_tech/chess-project
npm install
# 2. Start dev server (default port 3050)
npm run dev
# 3. Open browser to http://localhost:3050
# 4. On first visit, the onboarding page will prompt for:
# - Language selection
# - OpenRouter API key (get one at https://openrouter.ai/keys)
# - Model selection (pick your preferred AI model)
```
### Env Configuration
Create `chess-project/.env.local`:
```
OPENROUTER_API_KEY=sk-or-v1-your-key-here
NEXT_PUBLIC_DEFAULT_MODEL=google/gemini-2.5-flash
```
If `OPENROUTER_API_KEY` is set as an env var, the opening explanation endpoint uses it automatically. For the tutor chat, users enter their key through the UI (stored in localStorage).
### Available Models
Users can switch between any of these in the model selector dropdown:
| Model | Provider | Cost |
|-------|----------|------|
| Gemini 2.5 Flash | Google | Low |
| Gemini 2.5 Pro | Google | Medium |
| Claude Sonnet 4 | Anthropic | Medium |
| Claude 3.5 Haiku | Anthropic | Low |
| GPT-4o | OpenAI | Medium |
| GPT-4o Mini | OpenAI | Low |
| DeepSeek Chat | DeepSeek | Very Low |
| Grok 3 | xAI | Medium |
| Llama 3.1 70B | Meta | Low |
### Port
The dev server runs on port **3050** by default (configured in package.json).