Simplify opening training: train on entire opening family instead of single variation
- Add family training mode where users select an opening family (e.g. "Italian Game") and can play any variation within that family - Create variation tree data structure to efficiently track which variations match the current move sequence - FamilySelector now navigates directly to /learning/openings/family/[familyName] instead of showing a second selector for individual variations - OpeningTrainer displays matching variations, possible moves, and current line - LLM tutor explains which variation is being played and mentions alternatives - Remove the OpeningSelector step from the flow for a simpler UX The new flow: 1. User selects an opening family (e.g. "Sicilian Defense") 2. Training starts immediately with all variations loaded 3. User can play any move that exists in any variation 4. The UI shows which variations are still possible 5. The AI tutor guides the user through the repertoire
This commit is contained in:
@@ -1,14 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { OpeningFamily } from '@/lib/openingTrainer/openingFamilies';
|
||||
|
||||
interface FamilySelectorProps {
|
||||
families: OpeningFamily[];
|
||||
onSelectFamily: (familyName: string) => void;
|
||||
onSelectFamily?: (familyName: string) => void; // Made optional - now navigates directly
|
||||
}
|
||||
|
||||
export default function FamilySelector({ families, onSelectFamily }: FamilySelectorProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const handleSelectFamily = (familyName: string) => {
|
||||
// Navigate directly to family training page
|
||||
const encodedName = encodeURIComponent(familyName);
|
||||
router.push(`/learning/openings/family/${encodedName}`);
|
||||
};
|
||||
|
||||
// Group families by ECO range for display
|
||||
const groupedFamilies = useMemo(() => {
|
||||
const groups: Record<string, OpeningFamily[]> = {
|
||||
@@ -52,7 +61,7 @@ export default function FamilySelector({ families, onSelectFamily }: FamilySelec
|
||||
{categoryFamilies.map((family) => (
|
||||
<button
|
||||
key={family.name}
|
||||
onClick={() => onSelectFamily(family.name)}
|
||||
onClick={() => handleSelectFamily(family.name)}
|
||||
className="block p-6 border-2 border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg hover:border-blue-500 dark:hover:border-blue-400 hover:shadow-lg transition-all text-left"
|
||||
aria-label={`Select ${family.name} opening family`}
|
||||
>
|
||||
|
||||
@@ -6,7 +6,15 @@ 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 } from '@/lib/openingTrainer/gameLogic';
|
||||
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';
|
||||
@@ -22,10 +30,21 @@ interface OpeningTrainerProps {
|
||||
personality: Personality;
|
||||
apiKey: string;
|
||||
language: SupportedLanguage;
|
||||
// Family training mode - allows multiple variations
|
||||
variationTree?: VariationTree;
|
||||
allVariations?: OpeningMetadata[];
|
||||
}
|
||||
|
||||
export default function OpeningTrainer({ opening, personality, apiKey, language }: OpeningTrainerProps) {
|
||||
export default function OpeningTrainer({
|
||||
opening,
|
||||
personality,
|
||||
apiKey,
|
||||
language,
|
||||
variationTree,
|
||||
allVariations,
|
||||
}: OpeningTrainerProps) {
|
||||
const router = useRouter();
|
||||
const isFamilyMode = !!variationTree && !!allVariations;
|
||||
|
||||
const {
|
||||
session,
|
||||
@@ -365,12 +384,42 @@ export default function OpeningTrainer({ opening, personality, apiKey, language
|
||||
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: session.deviationMoveIndex === null,
|
||||
isInTheory: isFamilyMode
|
||||
? (variationPositionInfo?.isInAnyVariation ?? false)
|
||||
: session.deviationMoveIndex === null,
|
||||
deviationMoveIndex: session.deviationMoveIndex,
|
||||
lastUserMove: lastUserMove ? {
|
||||
from: lastUserMove.uci.substring(0, 2),
|
||||
@@ -395,11 +444,19 @@ export default function OpeningTrainer({ opening, personality, apiKey, language
|
||||
currentFeedback: currentFeedback ? {
|
||||
category: currentFeedback.classification.category,
|
||||
evaluationChange: currentFeedback.classification.evaluationChange,
|
||||
theoreticalAlternatives: currentFeedback.classification.theoreticalAlternatives
|
||||
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 (
|
||||
@@ -572,13 +629,81 @@ export default function OpeningTrainer({ opening, personality, apiKey, language
|
||||
<span className="text-gray-600 dark:text-gray-400">Moves played:</span>
|
||||
<span className="font-medium text-gray-900 dark:text-white">{moveCount}</span>
|
||||
</div>
|
||||
{session.deviationMoveIndex !== null && (
|
||||
|
||||
{/* Family mode: show variation info */}
|
||||
{isFamilyMode && variationPositionInfo && (
|
||||
<>
|
||||
<div className="pt-2 border-t border-gray-300 dark:border-gray-600">
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="text-gray-600 dark:text-gray-400">Matching variations:</span>
|
||||
<span className="font-medium text-blue-600 dark:text-blue-400">
|
||||
{variationPositionInfo.matchingCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Show possible moves */}
|
||||
{variationPositionInfo.nextMoves.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<span className="text-gray-600 dark:text-gray-400 block mb-1">Possible moves:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{variationPositionInfo.nextMoves.map((move) => (
|
||||
<span
|
||||
key={move}
|
||||
className="inline-block px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400 rounded text-xs font-mono"
|
||||
>
|
||||
{move}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show current variation names (if narrowed down) */}
|
||||
{variationPositionInfo.matchingCount > 0 && variationPositionInfo.matchingCount <= 3 && (
|
||||
<div className="mt-2">
|
||||
<span className="text-gray-600 dark:text-gray-400 block mb-1">Current line:</span>
|
||||
<div className="space-y-1">
|
||||
{variationPositionInfo.currentVariationNames.slice(0, 3).map((name) => (
|
||||
<span
|
||||
key={name}
|
||||
className="block text-xs text-gray-700 dark:text-gray-300 truncate"
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{variationPositionInfo.isEndOfLine && (
|
||||
<div className="mt-2">
|
||||
<span className="inline-block px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-400 rounded text-xs">
|
||||
End of repertoire line
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Show off-book indicator (non-family mode or when truly off-book) */}
|
||||
{!isFamilyMode && session.deviationMoveIndex !== null && (
|
||||
<div className="pt-2 border-t border-gray-300 dark:border-gray-600">
|
||||
<span className="inline-block px-2 py-1 bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-400 rounded text-xs">
|
||||
Off-book since move {session.deviationMoveIndex + 1}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Family mode: show off-book when not in any variation */}
|
||||
{isFamilyMode && variationPositionInfo && !variationPositionInfo.isInAnyVariation && moveCount > 0 && (
|
||||
<div className="pt-2 border-t border-gray-300 dark:border-gray-600">
|
||||
<span className="inline-block px-2 py-1 bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-400 rounded text-xs">
|
||||
Off-book - move not in any variation
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -82,6 +82,14 @@ interface TutorProps {
|
||||
wikipediaSummary?: string; // Optional Wikipedia context
|
||||
shouldTutorSpeak?: boolean; // Guardrail: controls when tutor can send messages
|
||||
onTutorMessageSent?: () => void; // Callback when tutor sends a message
|
||||
// Family mode: training with multiple variations
|
||||
isFamilyMode?: boolean;
|
||||
variationInfo?: {
|
||||
matchingVariations: number;
|
||||
currentVariationNames: string[];
|
||||
possibleMoves: string[];
|
||||
isEndOfLine: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -359,6 +367,8 @@ Acknowledge this new puzzle briefly (1 sentence) and encourage the student to fi
|
||||
const isInTheory = openingPracticeMode?.isInTheory ?? true;
|
||||
const currentFeedback = openingPracticeMode?.currentFeedback;
|
||||
const repertoireMovesLength = openingPracticeMode?.repertoireMoves?.length ?? 0;
|
||||
const isFamilyMode = openingPracticeMode?.isFamilyMode ?? false;
|
||||
const variationInfo = openingPracticeMode?.variationInfo;
|
||||
|
||||
// Automatic commentary for opening practice mode
|
||||
useEffect(() => {
|
||||
@@ -382,6 +392,14 @@ Acknowledge this new puzzle briefly (1 sentence) and encourage the student to fi
|
||||
if (userMoveKey && userMoveKey !== lastUserMoveRef.current) {
|
||||
lastUserMoveRef.current = userMoveKey;
|
||||
|
||||
// Build variation context for family mode
|
||||
const variationContext = isFamilyMode && variationInfo ? `
|
||||
Variation info:
|
||||
- Matching variations: ${variationInfo.matchingVariations}
|
||||
- Current line(s): ${variationInfo.currentVariationNames.slice(0, 3).join(', ')}${variationInfo.currentVariationNames.length > 3 ? '...' : ''}
|
||||
- Possible next moves: ${variationInfo.possibleMoves.join(', ') || 'none (end of line)'}
|
||||
${variationInfo.isEndOfLine ? '- This is the end of this variation line' : ''}` : '';
|
||||
|
||||
// Generate commentary about user's move
|
||||
const moveCommentary = `
|
||||
[SYSTEM TRIGGER: user_move_in_opening]
|
||||
@@ -391,13 +409,25 @@ Move category: ${currentFeedback?.category || 'unknown'}
|
||||
Position status: ${isInTheory ? 'In theory' : 'Deviated from repertoire'}
|
||||
${currentFeedback?.evaluationChange !== undefined ? `Evaluation change: ${currentFeedback.evaluationChange.toFixed(2)}` : ''}
|
||||
${currentFeedback?.theoreticalAlternatives && currentFeedback.theoreticalAlternatives.length > 0 ? `Theory suggested: ${currentFeedback.theoreticalAlternatives.join(', ')}` : ''}
|
||||
${variationContext}
|
||||
|
||||
INSTRUCTIONS:
|
||||
${isInTheory
|
||||
? `- The student is following the repertoire correctly - praise them briefly
|
||||
? isFamilyMode
|
||||
? `- The student is playing a valid move in the ${openingName} family
|
||||
- Tell them which specific variation(s) they're now in (if narrowed down)
|
||||
- Explain the key idea behind this move (1-2 sentences)
|
||||
- If there are multiple possible moves at this position, you can briefly mention alternatives
|
||||
- If you're about to make the next move, explain what it accomplishes`
|
||||
: `- The student is following the repertoire correctly - praise them briefly
|
||||
- Explain the key idea behind this move (1-2 sentences)
|
||||
- If you're about to make the next move, you can mention it naturally`
|
||||
: `- The student deviated from theory
|
||||
: isFamilyMode
|
||||
? `- The student played a move not in any known variation of ${openingName}
|
||||
- Gently mention which moves would have been in theory (${currentFeedback?.theoreticalAlternatives?.join(' or ') || 'the main lines'})
|
||||
- Explain why those moves are preferred in the ${openingName}
|
||||
- Encourage them to explore or try a different move`
|
||||
: `- The student deviated from theory
|
||||
- Gently point out what the repertoire move was
|
||||
- Explain why the repertoire move is preferred
|
||||
- Ask if they want to try again or continue exploring`}
|
||||
@@ -469,7 +499,9 @@ Remember: You are both the opponent AND the tutor. Explain your move as if you'r
|
||||
language,
|
||||
openingName,
|
||||
currentFeedback,
|
||||
repertoireMovesLength
|
||||
repertoireMovesLength,
|
||||
isFamilyMode,
|
||||
variationInfo
|
||||
]);
|
||||
|
||||
// Scroll chat container to bottom (not the whole page)
|
||||
|
||||
Reference in New Issue
Block a user