'use client'; import { useEffect, useState, useMemo } from 'react'; import { Chess } from 'chess.js'; import { Chessboard } from 'react-chessboard'; import { OpeningMetadata } from '@/lib/openings'; import { useOpeningTraining, useChessInstance } from '@/contexts/OpeningTrainingContext'; import { loadSession } from '@/lib/openingTrainer/sessionManager'; import { parseMoveSequence, getUserColor, VariationTree, getAllPossibleNextMoves, identifyCurrentVariation, isMoveInVariationTree, describeCurrentPosition, } from '@/lib/openingTrainer/gameLogic'; import { getWikipediaSummary } from '@/lib/openingTrainer/wikipediaService'; import { WikipediaSummary as WikipediaSummaryType } from '@/types/openingTraining'; import { extractFamilyName } from '@/lib/openingTrainer/openingFamilies'; import WikipediaSummary from './WikipediaSummary'; import DeviationDialog from './DeviationDialog'; import { Tutor } from '@/components/Tutor'; import { Personality } from '@/lib/personalities'; import { SupportedLanguage } from '@/lib/i18n/translations'; import { useRouter } from 'next/navigation'; interface OpeningTrainerProps { opening: OpeningMetadata; personality: Personality; apiKey: string; language: SupportedLanguage; // Family training mode - allows multiple variations variationTree?: VariationTree; allVariations?: OpeningMetadata[]; } export default function OpeningTrainer({ opening, personality, apiKey, language, variationTree, allVariations, }: OpeningTrainerProps) { const router = useRouter(); const isFamilyMode = !!variationTree && !!allVariations; const { session, stockfish, currentFeedback, initializeSession, makeMove, navigateToMove, } = useOpeningTraining(); // Get Chess instance on-demand from current FEN const chess = useChessInstance(session); const [boardOrientation, setBoardOrientation] = useState<'white' | 'black'>( 'white' ); const [isInitializing, setIsInitializing] = useState(true); const [error, setError] = useState(null); const [showRecoveryDialog, setShowRecoveryDialog] = useState(false); const [existingSession, setExistingSession] = useState(null); const [wikipediaSummary, setWikipediaSummary] = useState(null); // Tutor message control - track when tutor last spoke const [lastTutorMessageMoveIndex, setLastTutorMessageMoveIndex] = useState(-1); // Deviation handling const [showDeviationDialog, setShowDeviationDialog] = useState(false); // ============================================================================ // Tutor Message Guardrail (computed values - must be before early returns) // ============================================================================ const moveCount = session?.moveHistory.length ?? 0; const userColor = getUserColor(opening); // Determine when tutor should be allowed to speak const shouldTutorSpeak = useMemo(() => { if (!session || moveCount === 0) { // At start, tutor can give initial greeting return lastTutorMessageMoveIndex === -1; } // Check if we've had new moves since tutor last spoke const newMovesSinceLastMessage = moveCount - lastTutorMessageMoveIndex; if (session.deviationMoveIndex !== null) { // Off-book: Tutor speaks immediately after player's deviation return newMovesSinceLastMessage >= 1; } // In theory: Wait for both player AND opponent to move const isAtEndOfRepertoire = session.phase === 'end_of_repertoire'; const movesNeeded = isAtEndOfRepertoire ? 1 : 2; return newMovesSinceLastMessage >= movesNeeded; }, [session, moveCount, lastTutorMessageMoveIndex]); // Detect deviation and show dialog useEffect(() => { if (session?.deviationMoveIndex !== null && !showDeviationDialog) { const timer = setTimeout(() => { setShowDeviationDialog(true); }, 500); return () => clearTimeout(timer); } }, [session?.deviationMoveIndex, showDeviationDialog]); useEffect(() => { checkForExistingSession(); }, [opening.eco]); // Fetch Wikipedia summary for the opening (using family name for better results) useEffect(() => { const fetchWikipediaSummary = async () => { try { // Extract family name (e.g., "French Defense" from "French Defense: Exchange Variation") // This ensures we find the Wikipedia article for the main opening, not specific variations const familyName = extractFamilyName(opening.name); console.log(`[Wikipedia] Looking up: "${familyName}" (from "${opening.name}")`); const summary = await getWikipediaSummary(familyName); setWikipediaSummary(summary); } catch (err) { console.error('Failed to fetch Wikipedia summary:', err); // Silently fail - Wikipedia is nice-to-have, not critical } }; fetchWikipediaSummary(); }, [opening.name]); const checkForExistingSession = () => { const saved = loadSession(opening.eco); if (saved && saved.moveHistory.length > 0) { // Found existing session with moves setExistingSession(saved); setShowRecoveryDialog(true); setIsInitializing(false); } else { // No existing session or empty session - start fresh initSession(false); } }; const initSession = async (forceNew: boolean = false) => { setIsInitializing(true); setError(null); setShowRecoveryDialog(false); try { await initializeSession(opening, forceNew); // Determine board orientation from opening // ECO D and E are typically Black defenses const orientation = ['D', 'E'].includes(opening.eco[0]) ? 'black' : 'white'; setBoardOrientation(orientation); } catch (err) { console.error('Session initialization error:', err); setError('Failed to initialize training session'); } finally { setIsInitializing(false); } }; const handleResumeSession = () => { initSession(false); }; const handleStartFresh = () => { initSession(true); }; const handlePieceDrop = ( sourceSquare: string, targetSquare: string ): boolean => { if (!chess) return false; try { // Create a temporary clone to test if the move is legal // WITHOUT modifying the actual chess instance const testChess = new Chess(); testChess.loadPgn(chess.pgn()); // Try to make the move on the clone (this can throw for invalid moves) let move; try { move = testChess.move({ from: sourceSquare, to: targetSquare, promotion: 'q', // Always promote to queen for simplicity }); } catch (moveError) { // Invalid move format or illegal move - silently reject console.log('Invalid move attempt:', { from: sourceSquare, to: targetSquare }); return false; } if (move === null) { // Illegal move return false; } // Move was legal - process it on the actual chess instance via makeMove makeMove(move.san); return true; } catch (error) { console.error('Unexpected error in handlePieceDrop:', error); return false; } }; // ============================================================================ // Deviation Dialog Handlers // ============================================================================ const handleUndoDeviation = () => { if (!session || session.deviationMoveIndex === null) return; // Navigate back to the move before deviation navigateToMove(session.deviationMoveIndex - 1); setShowDeviationDialog(false); // After a brief delay, make another legal move to continue in theory // This allows the player to try again }; const handleStartGameFromPosition = () => { if (!session || !chess) return; // Store the game start data in localStorage with the expected key // The home page (/) will pick this up and start the game const gameStartData = { fen: session.currentFEN, personalityId: personality.id, color: getUserColor(opening), stockfishDepth: 15, }; localStorage.setItem('chess_tutor_pending_game', JSON.stringify(gameStartData)); // Optional: Store additional context for the tutor const openingContext = { openingName: opening.name, openingEco: opening.eco, movesCompleted: session.deviationMoveIndex || session.moveHistory.length, wikipediaSummary: wikipediaSummary?.extract, contextMessage: `You've studied the ${opening.name} (${opening.eco}) up to move ${ session.deviationMoveIndex || session.moveHistory.length }. Let's continue playing from here!`, }; localStorage.setItem('chess_tutor_opening_context', JSON.stringify(openingContext)); // Navigate to home page which will start the game router.push('/'); }; const handleContinueExploring = () => { // User wants to continue exploring off-book moves // Just close the dialog and let them continue setShowDeviationDialog(false); }; const handleTutorMessageSent = () => { // Called when tutor successfully sends a message // Update the tracking to prevent rapid-fire messages setLastTutorMessageMoveIndex(moveCount); }; // Session recovery dialog if (showRecoveryDialog && existingSession) { return (

Resume Training?

You have an existing training session for this opening with{' '} {existingSession.moveHistory.length} move {existingSession.moveHistory.length !== 1 ? 's' : ''} . Would you like to resume where you left off or start fresh?

Last updated:{' '} {new Date(existingSession.lastUpdated).toLocaleString()}

); } if (isInitializing) { return (

Initializing training session...

{!stockfish && (

Loading chess engine...

)}
); } if (error) { return (

Error

{error}

); } if (!session || !chess) { return (

No active session

); } const currentPosition = chess.fen(); // Build opening practice mode prop for Tutor const repertoireMoves = parseMoveSequence(opening.moves); const lastMove = session.moveHistory.length > 0 ? session.moveHistory[session.moveHistory.length - 1] : null; // Determine which color the tutor is playing const tutorColor = userColor === 'white' ? 'black' : 'white'; // Find last user move and last tutor move const userMoves = session.moveHistory.filter( m => m.color === userColor ); const tutorMoves = session.moveHistory.filter( m => m.color === tutorColor ); const lastUserMove = userMoves.length > 0 ? userMoves[userMoves.length - 1] : null; const lastTutorMove = tutorMoves.length > 0 ? tutorMoves[tutorMoves.length - 1] : null; // Family mode: compute current position info from variation tree const variationPositionInfo = useMemo(() => { if (!isFamilyMode || !variationTree) { return null; } const positionDesc = describeCurrentPosition(variationTree, session.moveHistory); const currentVariations = identifyCurrentVariation(variationTree, session.moveHistory); const possibleMoves = getAllPossibleNextMoves(variationTree, session.moveHistory); return { ...positionDesc, currentVariations, possibleMoves, isInAnyVariation: positionDesc.matchingCount > 0, }; }, [isFamilyMode, variationTree, session.moveHistory]); // Get all possible next moves (for display and tutor context) const theoreticalMoves = useMemo(() => { if (isFamilyMode && variationPositionInfo) { return variationPositionInfo.nextMoves; } // Single variation mode const moves = parseMoveSequence(opening.moves); const nextMove = moves[session.moveHistory.length]; return nextMove ? [nextMove] : []; }, [isFamilyMode, variationPositionInfo, opening.moves, session.moveHistory.length]); const openingPracticeMode = { openingName: opening.name, openingEco: opening.eco, repertoireMoves, currentMoveIndex: session.moveHistory.length, isInTheory: isFamilyMode ? (variationPositionInfo?.isInAnyVariation ?? false) : session.deviationMoveIndex === null, deviationMoveIndex: session.deviationMoveIndex, lastUserMove: lastUserMove ? { from: lastUserMove.uci.substring(0, 2), to: lastUserMove.uci.substring(2, 4), san: lastUserMove.san, color: lastUserMove.color === 'white' ? 'w' : 'b', piece: lastUserMove.san[0].toLowerCase(), flags: '', captured: undefined, promotion: lastUserMove.uci.length > 4 ? lastUserMove.uci[4] : undefined } as any : null, lastTutorMove: lastTutorMove ? { from: lastTutorMove.uci.substring(0, 2), to: lastTutorMove.uci.substring(2, 4), san: lastTutorMove.san, color: lastTutorMove.color === 'white' ? 'w' : 'b', piece: lastTutorMove.san[0].toLowerCase(), flags: '', captured: undefined, promotion: lastTutorMove.uci.length > 4 ? lastTutorMove.uci[4] : undefined } as any : null, currentFeedback: currentFeedback ? { category: currentFeedback.classification.category, evaluationChange: currentFeedback.classification.evaluationChange, theoreticalAlternatives: isFamilyMode ? theoreticalMoves : currentFeedback.classification.theoreticalAlternatives } : null, wikipediaSummary: wikipediaSummary?.extract || undefined, shouldTutorSpeak, onTutorMessageSent: handleTutorMessageSent, // Family mode specific info isFamilyMode, variationInfo: isFamilyMode && variationPositionInfo ? { matchingVariations: variationPositionInfo.matchingCount, currentVariationNames: variationPositionInfo.currentVariationNames, possibleMoves: variationPositionInfo.nextMoves, isEndOfLine: variationPositionInfo.isEndOfLine, } : undefined, }; return (
{/* Main board area */}
{/* Board */}
{ if (!targetSquare) return false; return handlePieceDrop(sourceSquare, targetSquare); }, boardOrientation: boardOrientation, darkSquareStyle: { backgroundColor: '#779954' }, lightSquareStyle: { backgroundColor: '#e9edcc' }, animationDurationInMs: 200, }} />
{/* Move controls */}

Move History

{/* Move list */}
{moveCount === 0 ? (

No moves yet. Make your first move!

) : ( session.moveHistory.map((move, index) => (
navigateToMove(index)} role="listitem" className={`p-2 rounded cursor-pointer transition-colors ${ index === session.currentMoveIndex ? 'bg-blue-100 dark:bg-blue-900/30 border border-blue-300 dark:border-blue-700' : 'bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700' }`} aria-label={`Move ${move.moveNumber}${ move.color === 'white' ? '.' : '...' } ${move.san}, classified as ${move.classification.category}`} aria-current={index === session.currentMoveIndex ? 'true' : undefined} >
{move.moveNumber} {move.color === 'white' ? '.' : '...'} {move.san} {move.classification.category}
)) )}
{/* Sidebar - tutor and info */}
{/* Tutor Chat */} {apiKey ? ( {}} apiKey={apiKey} personality={personality} language={language} playerColor={userColor} onCheckComputerMove={() => {}} resignationContext={null} openingPracticeMode={openingPracticeMode} /> ) : (
🔑

API Key Required

To chat with your coach, please set up your Gemini API key in the settings.

)} {/* Wikipedia summary */} {/* Session info */}
Opening: {opening.eco}
Moves played: {moveCount}
{/* Family mode: show variation info */} {isFamilyMode && variationPositionInfo && ( <>
Matching variations: {variationPositionInfo.matchingCount}
{/* Show possible moves */} {variationPositionInfo.nextMoves.length > 0 && (
Possible moves:
{variationPositionInfo.nextMoves.map((move) => ( {move} ))}
)} {/* Show current variation names (if narrowed down) */} {variationPositionInfo.matchingCount > 0 && variationPositionInfo.matchingCount <= 3 && (
Current line:
{variationPositionInfo.currentVariationNames.slice(0, 3).map((name) => ( {name} ))}
)} {variationPositionInfo.isEndOfLine && (
End of repertoire line
)}
)} {/* Show off-book indicator (non-family mode or when truly off-book) */} {!isFamilyMode && session.deviationMoveIndex !== null && (
Off-book since move {session.deviationMoveIndex + 1}
)} {/* Family mode: show off-book when not in any variation */} {isFamilyMode && variationPositionInfo && !variationPositionInfo.isInAnyVariation && moveCount > 0 && (
Off-book - move not in any variation
)}
{/* Deviation Dialog */} {showDeviationDialog && session?.deviationMoveIndex !== null && ( )}
); }