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/ChessGame.test.tsx b/src/components/ChessGame.test.tsx index 560b77a..3248f87 100644 --- a/src/components/ChessGame.test.tsx +++ b/src/components/ChessGame.test.tsx @@ -67,9 +67,11 @@ jest.mock("./StartScreen", () => ({ describe("ChessGame Component", () => { const mockPersonality = { + id: "test", name: "Test Personality", systemPrompt: "You are a helpful assistant.", image: "🤖", + description: "Test description", }; beforeEach(() => { diff --git a/src/components/EvaluationBar.test.tsx b/src/components/EvaluationBar.test.tsx index f7e92ab..4d0013f 100644 --- a/src/components/EvaluationBar.test.tsx +++ b/src/components/EvaluationBar.test.tsx @@ -4,7 +4,7 @@ import "@testing-library/jest-dom"; describe("EvaluationBar", () => { it("renders 0.0 for initial state", () => { - render(); + render(); expect(screen.getByText("0.0")).toBeInTheDocument(); }); @@ -19,12 +19,12 @@ describe("EvaluationBar", () => { }); it("renders mate score", () => { - render(); + render(); expect(screen.getByText("M3")).toBeInTheDocument(); }); it("renders negative mate score", () => { - render(); + render(); expect(screen.getByText("M5")).toBeInTheDocument(); }); }); 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) => (