From f8cf03c888236717b0b1a9e3a80b39140b7d802c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Jan 2026 21:52:36 +0000 Subject: [PATCH] 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 --- .../openings/family/[familyName]/page.tsx | 194 ++++++++++++++++ src/app/learning/openings/page.tsx | 39 +--- .../OpeningTrainer/FamilySelector.tsx | 13 +- .../OpeningTrainer/OpeningTrainer.tsx | 135 ++++++++++- src/components/Tutor.tsx | 38 ++- src/lib/openingTrainer/gameLogic.ts | 218 ++++++++++++++++++ src/lib/openingTrainer/openingLoader.ts | 44 ++++ 7 files changed, 640 insertions(+), 41 deletions(-) create mode 100644 src/app/learning/openings/family/[familyName]/page.tsx diff --git a/src/app/learning/openings/family/[familyName]/page.tsx b/src/app/learning/openings/family/[familyName]/page.tsx new file mode 100644 index 0000000..741d9c1 --- /dev/null +++ b/src/app/learning/openings/family/[familyName]/page.tsx @@ -0,0 +1,194 @@ +'use client'; + +import { useEffect, useState, useMemo } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { ArrowLeft } from 'lucide-react'; +import Header from '@/components/Header'; +import { useTranslation } from '@/lib/i18n/useTranslation'; +import { SupportedLanguage } from '@/lib/i18n/translations'; +import { OpeningMetadata } from '@/lib/openings'; +import { OpeningTrainingProvider } from '@/contexts/OpeningTrainingContext'; +import OpeningTrainer from '@/components/OpeningTrainer/OpeningTrainer'; +import { OpeningTrainerErrorBoundary } from '@/components/OpeningTrainer/ErrorBoundary'; +import { getOpeningsByFamily } from '@/lib/openingTrainer/openingLoader'; +import { buildVariationTree, VariationTree } from '@/lib/openingTrainer/gameLogic'; +import { Personality, PERSONALITIES } from '@/lib/personalities'; + +/** + * Family Training Page + * + * This page enables training on an entire opening family (e.g., "Italian Game") + * instead of a single specific variation. Users can play any moves that exist + * in any variation, and the tutor will guide them through the repertoire. + */ +export default function FamilyTrainingPage() { + const params = useParams(); + const router = useRouter(); + const familyName = decodeURIComponent(params.familyName as string); + + const [language, setLanguage] = useState('en'); + const [mounted, setMounted] = useState(false); + const [variations, setVariations] = useState([]); + const [variationTree, setVariationTree] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [selectedPersonality, setSelectedPersonality] = useState(PERSONALITIES[0]); + const [apiKey, setApiKey] = useState(''); + + useEffect(() => { + const storedLang = localStorage.getItem('chess_tutor_language'); + if (storedLang) setLanguage(storedLang as SupportedLanguage); + + // Load API key + const storedApiKey = localStorage.getItem('gemini_api_key'); + if (storedApiKey) setApiKey(storedApiKey); + + // Load personality + const storedPersonalityId = localStorage.getItem('chess_tutor_personality'); + if (storedPersonalityId) { + const personality = PERSONALITIES.find(p => p.id === storedPersonalityId); + if (personality) setSelectedPersonality(personality); + } + + setMounted(true); + }, []); + + const t = useTranslation(language); + + useEffect(() => { + if (mounted) { + loadFamilyVariations(); + } + }, [familyName, mounted]); + + const loadFamilyVariations = () => { + setIsLoading(true); + + // Get all variations for this family + const familyVariations = getOpeningsByFamily(familyName); + + if (familyVariations.length === 0) { + // No variations found - redirect back to selection + router.push('/learning/openings'); + return; + } + + setVariations(familyVariations); + + // Build the variation tree for efficient lookup + const tree = buildVariationTree(familyVariations, familyName); + setVariationTree(tree); + + setIsLoading(false); + }; + + // Create a "representative" opening for the family + // Uses the first ECO code and combines info from all variations + const familyOpening: OpeningMetadata | null = useMemo(() => { + if (variations.length === 0) return null; + + // Get all unique ECO codes + const ecoCodes = [...new Set(variations.map((v: OpeningMetadata) => v.eco))].sort(); + const ecoRange = ecoCodes.length === 1 + ? ecoCodes[0] + : `${ecoCodes[0]}-${ecoCodes[ecoCodes.length - 1]}`; + + // Find the variation with the most moves (for the initial repertoire display) + const longestVariation = variations[0]; // Already sorted by move count + + // Create a combined opening metadata + return { + eco: longestVariation.eco, + name: familyName, + moves: longestVariation.moves, // Use longest for display, but tree handles all + src: 'family', + isEcoRoot: true, + wikipediaSlug: longestVariation.wikipediaSlug, + }; + }, [variations, familyName]); + + if (!mounted) return null; + + if (isLoading) { + return ( + <> +
+
+
+
+
+
+

{t.learning.openingTrainer.loadingSession}

+
+
+
+
+ + ); + } + + if (!familyOpening || !variationTree) { + return ( + <> +
+
+
+
+

+ {t.learning.openingTrainer.openingNotFound} +

+

+ No variations found for "{familyName}" +

+ +
+
+
+ + ); + } + + return ( + <> +
+
+
+ {/* Header */} +
+ +
+ +
+

{familyName}

+

+ {variations.length} variation{variations.length !== 1 ? 's' : ''} available +

+
+ + + + + + +
+
+ + ); +} diff --git a/src/app/learning/openings/page.tsx b/src/app/learning/openings/page.tsx index ca50009..f87316c 100644 --- a/src/app/learning/openings/page.tsx +++ b/src/app/learning/openings/page.tsx @@ -4,7 +4,6 @@ import { useState, useEffect, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { ArrowLeft } from 'lucide-react'; import Header from '@/components/Header'; -import OpeningSelector from '@/components/OpeningTrainer/OpeningSelector'; import FamilySelector from '@/components/OpeningTrainer/FamilySelector'; import { useTranslation } from '@/lib/i18n/useTranslation'; import { SupportedLanguage } from '@/lib/i18n/translations'; @@ -15,7 +14,6 @@ export default function OpeningsPage() { const router = useRouter(); const [language, setLanguage] = useState('en'); const [mounted, setMounted] = useState(false); - const [selectedFamily, setSelectedFamily] = useState(null); useEffect(() => { const storedLang = localStorage.getItem('chess_tutor_language'); @@ -35,14 +33,6 @@ export default function OpeningsPage() { return groupOpeningsByFamily(allOpenings); }, [allOpenings]); - const handleSelectFamily = (familyName: string) => { - setSelectedFamily(familyName); - }; - - const handleBackToFamilies = () => { - setSelectedFamily(null); - }; - if (!mounted) return null; return ( @@ -61,28 +51,15 @@ export default function OpeningsPage() { - {!selectedFamily ? ( - <> -

- Opening Training -

-

- Select an opening family to explore. Each family contains multiple variations - with engine-backed feedback and AI-powered explanations. -

+

+ Opening Training +

+

+ Select an opening family to train. You can play any variation within the family, + and your AI coach will guide you through the different lines. +

- - - ) : ( - - )} + diff --git a/src/components/OpeningTrainer/FamilySelector.tsx b/src/components/OpeningTrainer/FamilySelector.tsx index 878f0e7..bf3866e 100644 --- a/src/components/OpeningTrainer/FamilySelector.tsx +++ b/src/components/OpeningTrainer/FamilySelector.tsx @@ -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 = { @@ -52,7 +61,7 @@ export default function FamilySelector({ families, onSelectFamily }: FamilySelec {categoryFamilies.map((family) => (