Add mobile app support and opening training feature

This commit implements iOS/Android mobile app support using Capacitor
and adds a comprehensive opening training feature with LLM-powered
explanations.

## Mobile App Infrastructure

- Add Capacitor configuration for iOS/Android builds
- Create mobile build script that excludes API routes
- Update Next.js config for conditional static export
- Add layout components with generateStaticParams for static builds
- Generate 500+ static pages for offline mobile use

## Chess Engine Abstraction

- Create ChessEngine interface for pluggable implementations
- Add LocalEngine (GPL - uses stockfish.js in browser)
- Add RemoteEngine (proprietary - calls API server)
- Factory pattern selects engine based on environment
- Enables GPL compliance for web, proprietary for mobile

## Opening Training Feature

- Interactive opening repertoire training
- Move validation with engine-backed feedback
- LLM explanations using Gemini API
- Wikipedia integration for opening context
- Opening family grouping (e4, d4, c4, etc.)
- Session state management
- Real-time move feedback with evaluation

Components:
- OpeningSelector: Browse and select openings by family
- OpeningTrainer: Main training interface with chessboard
- MoveFeedback: Display move quality and LLM explanations
- WikipediaSummary: Show opening history and context
- ErrorBoundary: Graceful error handling

Services:
- openingLoader: Load and filter opening database
- engineService: Engine evaluation wrapper
- moveValidator: Validate moves against repertoire
- feedbackGenerator: Generate contextual feedback
- wikipediaService: Fetch and cache Wikipedia data
- sessionManager: Track training session state

## Wikipedia Integration

- Automatic Wikipedia article fetching for openings
- Client-side and server-side caching
- Sanitized summaries with proper formatting
- Link opening database to Wikipedia slugs
- API endpoints for on-demand fetching

## Docker Improvements

- Add entrypoint script for automatic data setup
- Fetch Wikipedia data on first container startup
- Generate opening move index automatically
- Remove generated data from git (public/openings/*.json, public/wikipedia/*.json)
- Add READMEs explaining data requirements
- Update .gitignore for generated files

## Dual Licensing Strategy

- Add LICENSING.md explaining dual licensing approach
- GPL-3.0 for web builds (includes Stockfish)
- Proprietary option for mobile builds (no GPL code)
- Single codebase, multiple licensing models
- Legal compliance documented

## API Endpoints

- POST /api/v1/llm/opening-explanation - Get LLM move explanations
- GET /api/v1/wikipedia/summary - Fetch Wikipedia summaries

## Type Updates

- Add openingTraining types
- Update Tutor component to use ChessEngine interface
- Add Gemini error handling types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Stefan
2025-12-07 18:40:58 +01:00
parent 52dd32eb15
commit 002ed92bea
57 changed files with 7015 additions and 234280 deletions
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env tsx
/**
* Add Wikipedia slugs to opening database
*
* This script updates the opening JSON files to include wikipediaSlug field
* based on the cached Wikipedia articles we've already downloaded.
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
interface OpeningMetadata {
name: string;
eco: string;
moves: string;
isEcoRoot?: boolean;
wikipediaSlug?: string;
}
/**
* Extract family name from opening name
*/
function extractFamilyName(openingName: string): string {
const separators = [':', ',', '', '—', ' - '];
for (const sep of separators) {
if (openingName.includes(sep)) {
return openingName.split(sep)[0].trim();
}
}
return openingName;
}
/**
* Convert family name to slug
*/
function familyNameToSlug(familyName: string): string {
return familyName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
}
/**
* Load Wikipedia cache index to see what we have
*/
function getAvailableWikipediaSlugs(): Set<string> {
const wikiDir = path.join(__dirname, '..', 'public', 'wikipedia');
const slugs = new Set<string>();
if (!fs.existsSync(wikiDir)) {
return slugs;
}
const files = fs.readdirSync(wikiDir);
for (const file of files) {
if (file.endsWith('.json') && file !== 'index.json') {
const slug = file.replace('.json', '');
slugs.add(slug);
}
}
return slugs;
}
/**
* Update opening database files with Wikipedia slugs
*/
function updateOpeningDatabases() {
const availableSlugs = getAvailableWikipediaSlugs();
console.log(`\n📚 Found ${availableSlugs.size} Wikipedia cache files\n`);
const ecoFiles = ['ecoA', 'ecoB', 'ecoC', 'ecoD', 'ecoE'];
let totalUpdated = 0;
let totalSkipped = 0;
for (const ecoFile of ecoFiles) {
const filePath = path.join(__dirname, '..', 'public', 'openings', `${ecoFile}.json`);
console.log(`\n📖 Processing ${ecoFile}.json...`);
if (!fs.existsSync(filePath)) {
console.log(` ⚠️ File not found, skipping`);
continue;
}
const data: Record<string, OpeningMetadata> = JSON.parse(
fs.readFileSync(filePath, 'utf-8')
);
let updatedCount = 0;
let skippedCount = 0;
// Update each opening
for (const [fen, opening] of Object.entries(data)) {
const familyName = extractFamilyName(opening.name);
const slug = familyNameToSlug(familyName);
if (availableSlugs.has(slug)) {
opening.wikipediaSlug = slug;
updatedCount++;
} else {
skippedCount++;
}
}
// Write updated file
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
console.log(` ✓ Updated ${updatedCount} openings`);
console.log(` ⚠️ Skipped ${skippedCount} (no Wikipedia cache)`);
totalUpdated += updatedCount;
totalSkipped += skippedCount;
}
console.log('\n' + '='.repeat(50));
console.log('✨ Wikipedia Slug Addition Complete!\n');
console.log(`✓ Total updated: ${totalUpdated}`);
console.log(`⚠️ Total skipped: ${totalSkipped}`);
console.log('='.repeat(50) + '\n');
}
// Run the script
console.log('🔗 Adding Wikipedia Slugs to Opening Database\n');
updateOpeningDatabases();