From 55cb93244742ef1299760ca74061be2f40253b71 Mon Sep 17 00:00:00 2001 From: Leonard Date: Sat, 20 Jun 2026 19:47:16 +0000 Subject: [PATCH] feat: merge chess_tutor + OpenRouter abstraction - 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 --- .env.example | 9 +- .gitea/ISSUE_TEMPLATE/feature.md | 22 + .gitea/pull_request_template.md | 23 + AGENTS.md | 43 ++ README.md | 616 ++---------------- agents/HA_Architect.md | 24 + agents/HA_Coder.md | 26 + agents/HA_DevOps.md | 24 + agents/HA_Orchestrator.md | 25 + agents/HA_Planner.md | 26 + agents/HA_Reviewer.md | 23 + agents/HA_Tester.md | 24 + docs/WORKFLOW.md | 36 + src/app/api/v1/llm/chat/route.ts | 31 +- .../api/v1/llm/opening-explanation/route.ts | 41 +- src/app/onboarding/page.tsx | 6 +- src/app/page.tsx | 2 +- src/components/APIKeyInput.tsx | 64 +- src/components/ModelSelector.tsx | 110 ++++ src/components/useTutorChat.ts | 378 +++++++---- src/lib/openrouter.ts | 201 ++++++ tasks.md | 81 +++ 22 files changed, 1105 insertions(+), 730 deletions(-) create mode 100644 .gitea/ISSUE_TEMPLATE/feature.md create mode 100644 .gitea/pull_request_template.md create mode 100644 AGENTS.md create mode 100644 agents/HA_Architect.md create mode 100644 agents/HA_Coder.md create mode 100644 agents/HA_DevOps.md create mode 100644 agents/HA_Orchestrator.md create mode 100644 agents/HA_Planner.md create mode 100644 agents/HA_Reviewer.md create mode 100644 agents/HA_Tester.md create mode 100644 docs/WORKFLOW.md create mode 100644 src/components/ModelSelector.tsx create mode 100644 src/lib/openrouter.ts create mode 100644 tasks.md diff --git a/.env.example b/.env.example index 06b1178..6c3db42 100644 --- a/.env.example +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/.gitea/ISSUE_TEMPLATE/feature.md b/.gitea/ISSUE_TEMPLATE/feature.md new file mode 100644 index 0000000..a2f08b2 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature.md @@ -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. \ No newline at end of file diff --git a/.gitea/pull_request_template.md b/.gitea/pull_request_template.md new file mode 100644 index 0000000..1fa86f8 --- /dev/null +++ b/.gitea/pull_request_template.md @@ -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) \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8eb8c13 --- /dev/null +++ b/AGENTS.md @@ -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.* \ No newline at end of file diff --git a/README.md b/README.md index b8e08c2..080d87b 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file diff --git a/agents/HA_Architect.md b/agents/HA_Architect.md new file mode 100644 index 0000000..ec61681 --- /dev/null +++ b/agents/HA_Architect.md @@ -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] \ No newline at end of file diff --git a/agents/HA_Coder.md b/agents/HA_Coder.md new file mode 100644 index 0000000..a741980 --- /dev/null +++ b/agents/HA_Coder.md @@ -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] \ No newline at end of file diff --git a/agents/HA_DevOps.md b/agents/HA_DevOps.md new file mode 100644 index 0000000..3bdac2c --- /dev/null +++ b/agents/HA_DevOps.md @@ -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] \ No newline at end of file diff --git a/agents/HA_Orchestrator.md b/agents/HA_Orchestrator.md new file mode 100644 index 0000000..16117db --- /dev/null +++ b/agents/HA_Orchestrator.md @@ -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] \ No newline at end of file diff --git a/agents/HA_Planner.md b/agents/HA_Planner.md new file mode 100644 index 0000000..01cac57 --- /dev/null +++ b/agents/HA_Planner.md @@ -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] \ No newline at end of file diff --git a/agents/HA_Reviewer.md b/agents/HA_Reviewer.md new file mode 100644 index 0000000..6e34168 --- /dev/null +++ b/agents/HA_Reviewer.md @@ -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] \ No newline at end of file diff --git a/agents/HA_Tester.md b/agents/HA_Tester.md new file mode 100644 index 0000000..45d7bea --- /dev/null +++ b/agents/HA_Tester.md @@ -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] \ No newline at end of file diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md new file mode 100644 index 0000000..691a4b2 --- /dev/null +++ b/docs/WORKFLOW.md @@ -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.* \ No newline at end of file diff --git a/src/app/api/v1/llm/chat/route.ts b/src/app/api/v1/llm/chat/route.ts index 7763f9a..2c3d9a9 100644 --- a/src/app/api/v1/llm/chat/route.ts +++ b/src/app/api/v1/llm/chat/route.ts @@ -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 } + ); } -} +} \ No newline at end of file diff --git a/src/app/api/v1/llm/opening-explanation/route.ts b/src/app/api/v1/llm/opening-explanation/route.ts index a874b2e..4c16eb3 100644 --- a/src/app/api/v1/llm/opening-explanation/route.ts +++ b/src/app/api/v1/llm/opening-explanation/route.ts @@ -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', }); } -} +} \ No newline at end of file diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx index 3977e58..6240254 100644 --- a/src/app/onboarding/page.tsx +++ b/src/app/onboarding/page.tsx @@ -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() { 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( + () => (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 ( - +
+ + +
); } return (
-

Enter Gemini API Key

+

Enter OpenRouter API Key

- To receive AI feedback, please enter your Google Gemini API key. + To receive AI coaching, enter your OpenRouter API key. It will be stored locally in your browser. + Supports Gemini, Claude, GPT, Grok, DeepSeek, Llama, and more.

+ + {/* Model Selector */} +
+ + +
+
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) {
); -} +} \ No newline at end of file diff --git a/src/components/ModelSelector.tsx b/src/components/ModelSelector.tsx new file mode 100644 index 0000000..b5ae631 --- /dev/null +++ b/src/components/ModelSelector.tsx @@ -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(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 ( +
+ + + {isOpen && ( +
+ {Object.entries(groupedModels).map(([provider, models]) => ( +
+
+ {provider} +
+ {models.map((model) => ( + + ))} +
+ ))} +
+ )} +
+ ); +} + +// Utility: group array by key +function groupBy(arr: readonly T[], keyFn: (item: T) => string): Record { + const result: Record = {}; + for (const item of arr) { + const key = keyFn(item); + if (!result[key]) result[key] = []; + result[key].push(item); + } + return result; +} \ No newline at end of file diff --git a/src/components/useTutorChat.ts b/src/components/useTutorChat.ts index 527b2eb..e1815e3 100644 --- a/src/components/useTutorChat.ts +++ b/src/components/useTutorChat.ts @@ -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([]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); - const [chatSession, setChatSession] = useState(null); + const [chatHistory, setChatHistory] = useState< + { role: "user" | "model"; text: string }[] + >([]); const messagesContainerRef = useRef(null); const lastAnalyzedMoveRef = useRef(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, }; -} +} \ No newline at end of file diff --git a/src/lib/openrouter.ts b/src/lib/openrouter.ts new file mode 100644 index 0000000..26f293f --- /dev/null +++ b/src/lib/openrouter.ts @@ -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 }; \ No newline at end of file diff --git a/tasks.md b/tasks.md new file mode 100644 index 0000000..4c6a092 --- /dev/null +++ b/tasks.md @@ -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). \ No newline at end of file