002ed92bea
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>
151 lines
6.4 KiB
TypeScript
151 lines
6.4 KiB
TypeScript
'use client';
|
|
|
|
import { AlertCircle, ExternalLink, X, Settings, Key } from 'lucide-react';
|
|
import { GeminiErrorInfo } from '@/lib/geminiErrorHandler';
|
|
import { ApiKeyInfo, getApiKeySourceDescription } from '@/lib/apiKeyHelper';
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
interface GeminiErrorModalProps {
|
|
error: GeminiErrorInfo;
|
|
apiKeyInfo: ApiKeyInfo;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function GeminiErrorModal({ error, apiKeyInfo, onClose }: GeminiErrorModalProps) {
|
|
const router = useRouter();
|
|
|
|
const handleGoToSettings = () => {
|
|
onClose();
|
|
router.push('/settings');
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center">
|
|
<AlertCircle className="text-red-600 dark:text-red-400" size={24} />
|
|
</div>
|
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
|
{error.isQuotaError ? 'API Quota Exceeded' : 'API Error'}
|
|
</h2>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
|
aria-label="Close"
|
|
>
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6 space-y-4">
|
|
{/* API Key Info */}
|
|
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
|
<div className="flex items-start gap-3">
|
|
<Key className="text-gray-600 dark:text-gray-400 flex-shrink-0 mt-0.5" size={18} />
|
|
<div className="flex-1 space-y-1">
|
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
Current API Key:
|
|
</p>
|
|
<p className="text-sm font-mono text-gray-700 dark:text-gray-300">
|
|
{apiKeyInfo.anonymized}
|
|
</p>
|
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
|
Source: {getApiKeySourceDescription(apiKeyInfo.source)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-gray-700 dark:text-gray-300">
|
|
{error.userMessage}
|
|
</p>
|
|
|
|
{error.retryAfterSeconds && (
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
|
<p className="text-sm text-blue-800 dark:text-blue-300">
|
|
You can try again in <strong>{error.retryAfterSeconds} seconds</strong>.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{error.isQuotaError && (
|
|
<div className="space-y-3">
|
|
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
|
<p className="text-sm text-yellow-800 dark:text-yellow-300 mb-2">
|
|
<strong>Solutions:</strong>
|
|
</p>
|
|
<ul className="text-sm text-yellow-800 dark:text-yellow-300 list-disc list-inside space-y-1">
|
|
{apiKeyInfo.source === 'localStorage' && (
|
|
<li>Change your API key to a different one with available quota</li>
|
|
)}
|
|
<li>Upgrade to a paid Gemini API plan for higher quotas</li>
|
|
<li>Wait until tomorrow for your free tier quota to reset</li>
|
|
<li>Use the chess tutor less frequently throughout the day</li>
|
|
{apiKeyInfo.source === 'env' && (
|
|
<li>Update the NEXT_PUBLIC_GEMINI_API_KEY environment variable</li>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
|
|
{/* Change API Key button (if from localStorage) */}
|
|
{apiKeyInfo.source === 'localStorage' && (
|
|
<button
|
|
onClick={handleGoToSettings}
|
|
className="flex items-center justify-center gap-2 w-full px-4 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium"
|
|
>
|
|
<Settings size={16} />
|
|
Change API Key in Settings
|
|
</button>
|
|
)}
|
|
|
|
<a
|
|
href="https://ai.google.dev/pricing"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center justify-center gap-2 w-full px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
|
>
|
|
View Gemini API Pricing
|
|
<ExternalLink size={16} />
|
|
</a>
|
|
|
|
<a
|
|
href="https://ai.dev/usage"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center justify-center gap-2 w-full px-4 py-3 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors font-medium"
|
|
>
|
|
Check Your API Usage
|
|
<ExternalLink size={16} />
|
|
</a>
|
|
</div>
|
|
)}
|
|
|
|
{/* Technical details (collapsible) */}
|
|
<details className="mt-4">
|
|
<summary className="text-sm text-gray-500 dark:text-gray-400 cursor-pointer hover:text-gray-700 dark:hover:text-gray-300">
|
|
Technical details
|
|
</summary>
|
|
<pre className="mt-2 p-3 bg-gray-100 dark:bg-gray-900 rounded text-xs text-gray-600 dark:text-gray-400 overflow-x-auto">
|
|
{error.technicalMessage}
|
|
</pre>
|
|
</details>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="p-6 border-t border-gray-200 dark:border-gray-700 flex justify-end">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-6 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors font-medium"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|