diff --git a/next.config.js b/next.config.js index 6d705248..da7d3461 100644 --- a/next.config.js +++ b/next.config.js @@ -6,6 +6,15 @@ const withTM = require('next-transpile-modules')(['@react-chess/chessground']) module.exports = withTM({ reactStrictMode: false, output: 'standalone', + async redirects() { + return [ + { + source: '/openings', + destination: '/drills', + permanent: true, + }, + ] + }, async rewrites() { return [ { diff --git a/src/components/Common/Header.tsx b/src/components/Common/Header.tsx index fa9d095b..648b073d 100644 --- a/src/components/Common/Header.tsx +++ b/src/components/Common/Header.tsx @@ -209,10 +209,10 @@ export const Header: React.FC = () => { PUZZLES - PRACTICE + DRILLS { Puzzles - - Practice + + Drills Bot-or-not diff --git a/src/components/Home/HomeHero.tsx b/src/components/Home/HomeHero.tsx index be9f8305..c0b1a826 100644 --- a/src/components/Home/HomeHero.tsx +++ b/src/components/Home/HomeHero.tsx @@ -229,9 +229,9 @@ export const HomeHero: React.FC = ({ scrollHandler }: Props) => { /> diff --git a/src/components/Home/Sections/AdditionalFeaturesSection.tsx b/src/components/Home/Sections/AdditionalFeaturesSection.tsx index 06aac3fa..4c0b6526 100644 --- a/src/components/Home/Sections/AdditionalFeaturesSection.tsx +++ b/src/components/Home/Sections/AdditionalFeaturesSection.tsx @@ -135,10 +135,10 @@ export const AdditionalFeaturesSection = ({ const features: Feature[] = [ { icon: , - title: 'Practice', + title: 'Drill', description: "Drill chess openings against Maia models calibrated to specific rating levels, allowing you to practice against opponents similar to those you'll face.", - action: { type: 'link', href: '/openings', label: 'Practice' }, + action: { type: 'link', href: '/drills', label: 'Drill' }, iconBgColor: 'bg-human-3/10', iconTextColor: 'text-human-3', }, diff --git a/src/components/Openings/DrillPerformanceModal.tsx b/src/components/Openings/DrillPerformanceModal.tsx index 624eba98..2c7a56f5 100644 --- a/src/components/Openings/DrillPerformanceModal.tsx +++ b/src/components/Openings/DrillPerformanceModal.tsx @@ -39,7 +39,7 @@ interface Props { performanceData: DrillPerformanceData onContinueAnalyzing: () => void onNextDrill: () => void - isLastDrill: boolean + onReconfigureDrills: () => void } // Helper function to extract move number from FEN string @@ -769,7 +769,7 @@ const DesktopLayout: React.FC<{ performanceData: DrillPerformanceData onContinueAnalyzing: () => void onNextDrill: () => void - isLastDrill: boolean + onReconfigureDrills: () => void gameTree: GameTree openingEndNode: GameNode playerMoveCount: number @@ -788,7 +788,7 @@ const DesktopLayout: React.FC<{ performanceData, onContinueAnalyzing, onNextDrill, - isLastDrill, + onReconfigureDrills, gameTree, openingEndNode, playerMoveCount, @@ -812,9 +812,7 @@ const DesktopLayout: React.FC<{ {/* Header */}
-

- Opening Analysis Complete -

+

Drill Review

{drill.selection.opening.name} @@ -1064,23 +1062,29 @@ const DesktopLayout: React.FC<{

{/* Action Buttons */} -
- - {!isLastDrill && ( +
+
+ - )} +
+
+ +
) @@ -1093,7 +1097,7 @@ const MobileLayout: React.FC<{ performanceData: DrillPerformanceData onContinueAnalyzing: () => void onNextDrill: () => void - isLastDrill: boolean + onReconfigureDrills: () => void activeTab: 'replay' | 'analysis' | 'insights' setActiveTab: (tab: 'replay' | 'analysis' | 'insights') => void gameTree: GameTree @@ -1114,7 +1118,7 @@ const MobileLayout: React.FC<{ performanceData, onContinueAnalyzing, onNextDrill, - isLastDrill, + onReconfigureDrills, activeTab, setActiveTab, gameTree, @@ -1140,7 +1144,7 @@ const MobileLayout: React.FC<{ {/* Header */}
-

Analysis Complete

+

Drill Review

{drill.selection.opening.name} @@ -1280,19 +1284,29 @@ const MobileLayout: React.FC<{

{/* Action Buttons */} -
- - +
+
+ + +
+
+ +
) @@ -1301,7 +1315,7 @@ export const DrillPerformanceModal: React.FC = ({ performanceData, onContinueAnalyzing, onNextDrill, - isLastDrill, + onReconfigureDrills, }) => { const { isMobile } = useContext(WindowSizeContext) const [activeTab, setActiveTab] = useState< @@ -1490,7 +1504,7 @@ export const DrillPerformanceModal: React.FC = ({ performanceData={performanceData} onContinueAnalyzing={onContinueAnalyzing} onNextDrill={onNextDrill} - isLastDrill={isLastDrill} + onReconfigureDrills={onReconfigureDrills} activeTab={activeTab} setActiveTab={setActiveTab} gameTree={gameTree} @@ -1510,7 +1524,7 @@ export const DrillPerformanceModal: React.FC = ({ performanceData={performanceData} onContinueAnalyzing={onContinueAnalyzing} onNextDrill={onNextDrill} - isLastDrill={isLastDrill} + onReconfigureDrills={onReconfigureDrills} gameTree={gameTree} openingEndNode={openingEndNode} playerMoveCount={playerMoveCount} diff --git a/src/components/Openings/OpeningDrillSidebar.tsx b/src/components/Openings/OpeningDrillSidebar.tsx index caed62b6..cf3513e7 100644 --- a/src/components/Openings/OpeningDrillSidebar.tsx +++ b/src/components/Openings/OpeningDrillSidebar.tsx @@ -48,6 +48,10 @@ export const OpeningDrillSidebar: React.FC = ({ const currentIsEndgame = currentDrill?.opening.categoryType === 'endgame' const currentTraitLabel = currentDrill?.endgameMeta?.traitLabel const currentGroupLabel = currentDrill?.endgameMeta?.groupLabel + const currentPoolSelectionId = currentDrill + ? currentDrill.endgameMeta?.groupId || + currentDrill.id.replace(/__attempt_\d+$/, '') + : null const poolCategory = selectionPool[0]?.opening.categoryType ?? @@ -245,7 +249,7 @@ export const OpeningDrillSidebar: React.FC = ({

- Active {poolLabel} Pool ({selectionPool.length}) + Active Drill Pool ({selectionPool.length})

{selectionPool.length === 0 ? (

@@ -253,53 +257,83 @@ export const OpeningDrillSidebar: React.FC = ({

) : (
- {selectionPool.map((selection, index) => ( -
- {selection.opening.categoryType === 'endgame' ? ( - - trophy - - ) : ( -
- {`${selection.playerColor} -
- )} -
-
-

- {selection.opening.name} -

- {selection.opening.isCustom && ( - - Custom - - )} -
- {selection.opening.categoryType === 'endgame' - ? selection.endgameMeta?.traitLabel && ( -

- {selection.endgameMeta.traitLabel} -

- ) - : selection.variation && ( -

- {selection.variation.name} -

+ {selectionPool.map((selection, index) => { + const isCurrentPoolSelection = + currentPoolSelectionId === selection.id + + return ( +
+ {isCurrentPoolSelection && ( + + )} + {selection.opening.categoryType === 'endgame' ? ( + + trophy + + ) : ( +
+ {`${selection.playerColor} +
+ )} +
+
+

+ {selection.opening.name} +

+ {selection.opening.isCustom && ( + + Custom + )} +
+ {selection.opening.categoryType === 'endgame' + ? selection.endgameMeta?.traitLabel && ( +

+ {selection.endgameMeta.traitLabel} +

+ ) + : selection.variation && ( +

+ {selection.variation.name} +

+ )} +
-
- ))} + ) + })}
)}
diff --git a/src/components/Openings/OpeningSelectionModal.tsx b/src/components/Openings/OpeningSelectionModal.tsx index 449cf377..963ce06a 100644 --- a/src/components/Openings/OpeningSelectionModal.tsx +++ b/src/components/Openings/OpeningSelectionModal.tsx @@ -26,7 +26,6 @@ import { trackOpeningSelectionModalOpened, trackOpeningSearchUsed, trackOpeningPreviewSelected, - trackOpeningQuickAddUsed, trackOpeningConfiguredAndAdded, trackOpeningRemovedFromSelection, trackDrillConfigurationCompleted, @@ -71,6 +70,70 @@ const formatCategoryLabel = (category: DrillCategoryType) => { } } +const getMaiaOpponentName = (maiaVersion: string) => + MAIA3_OPPONENT_RATINGS.find((version) => version.id === maiaVersion)?.name ?? + maiaVersion + +const getSelectionDetailLine = (selection: OpeningSelection) => { + if ( + selection.opening.categoryType === 'endgame' && + selection.endgameTraits?.length + ) { + return selection.endgameTraits + .map((trait) => ENDGAME_TRAIT_LABELS[trait]) + .join(', ') + } + + return selection.opening.categoryType === 'custom' ? 'Custom position' : null +} + +const SelectionTitle: React.FC<{ + selection: OpeningSelection + className?: string +}> = ({ selection, className = 'text-[13px]' }) => ( +

+ {selection.opening.name} + {selection.variation && ( + : {selection.variation.name} + )} +

+) + +const SelectionConfigurationLine: React.FC<{ + selection: OpeningSelection + className?: string +}> = ({ selection, className = 'mt-1 text-[10px] text-white/50' }) => { + const finalItem = + selection.opening.categoryType === 'endgame' + ? `${selection.endgamePositions?.length ?? 0} positions` + : selection.targetMoveNumber === null + ? '∞ moves' + : `${selection.targetMoveNumber} moves` + + return ( +
+ {getMaiaOpponentName(selection.maiaVersion)} + · + + + {`${selection.playerColor} + + {selection.playerColor === 'white' ? 'White' : 'Black'} + + · + {finalItem} +
+ ) +} + interface Props { openings: Opening[] endgames?: Opening[] @@ -316,20 +379,9 @@ const BrowsePanel: React.FC<{ setPreviewOpening: (opening: Opening) => void setPreviewVariation: (variation: OpeningVariation | null) => void setActiveTab: (tab: MobileTab) => void - addQuickSelection: ( - opening: Opening, - variation: OpeningVariation | null, - ) => void - isDuplicateSelection: ( - opening: Opening, - variation: OpeningVariation | null, - traits?: EndgameTrait[], - ) => boolean searchTerm: string setSearchTerm: (term: string) => void - selections: OpeningSelection[] onOpeningClick: (opening: Opening, variation: OpeningVariation | null) => void - removeSelection: (id: string) => void onRemoveCustomOpening: (openingId: string) => void browseCategory: 'openings' | 'endgames' | 'custom' onBrowseCategoryChange: (category: 'openings' | 'endgames' | 'custom') => void @@ -347,13 +399,9 @@ const BrowsePanel: React.FC<{ setPreviewOpening, setPreviewVariation, setActiveTab, - addQuickSelection, - isDuplicateSelection, searchTerm, setSearchTerm, - selections, onOpeningClick, - removeSelection, onRemoveCustomOpening, browseCategory, onBrowseCategoryChange, @@ -399,20 +447,6 @@ const BrowsePanel: React.FC<{ const searchPlaceholder = `Search ${categoryLabelPlural.toLowerCase()}...` - const removeOpeningSelection = ( - opening: Opening, - variation: OpeningVariation | null, - ) => { - const selectionToRemove = selections.find( - (selection) => - selection.opening.id === opening.id && - selection.variation?.id === variation?.id, - ) - if (selectionToRemove) { - removeSelection(selectionToRemove.id) - } - } - const renderTabs = () => (
{[ @@ -591,11 +625,6 @@ const BrowsePanel: React.FC<{
) : ( filteredOpenings.map((opening) => { - const openingIsSelected = selections.some( - (selection) => - selection.opening.id === opening.id && - selection.variation === null, - ) const openingIsBeingPreviewed = previewOpening.id === opening.id && !previewVariation @@ -603,11 +632,9 @@ const BrowsePanel: React.FC<{
@@ -659,33 +686,6 @@ const BrowsePanel: React.FC<{
- {openingIsSelected ? ( - - ) : ( - - )}
) @@ -825,11 +802,6 @@ const BrowsePanel: React.FC<{
{!isCategoryCollapsed && category.openings.map((opening) => { - const openingIsSelected = selections.some( - (selection) => - selection.opening.id === opening.id && - selection.variation === null, - ) const openingIsBeingPreviewed = previewOpening.id === opening.id && !previewVariation const showStandaloneOpening = opening.variations.length === 0 @@ -886,7 +858,6 @@ const BrowsePanel: React.FC<{ ? renderRow( opening.name, opening.pgn, - openingIsSelected, openingIsBeingPreviewed, () => { setPreviewOpening(opening) @@ -900,25 +871,10 @@ const BrowsePanel: React.FC<{ onOpeningClick(opening, null) } }, - () => { - if (openingIsSelected) { - removeOpeningSelection(opening, null) - } else { - addQuickSelection(opening, null) - } - }, - openingIsSelected - ? `Remove ${categoryLabel.toLowerCase()} from selection` - : `Add ${categoryLabel.toLowerCase()} with current settings`, ) : null} {opening.variations.map((variation) => { - const variationIsSelected = selections.some( - (selection) => - selection.opening.id === opening.id && - selection.variation?.id === variation.id, - ) const variationIsBeingPreviewed = previewOpening.id === opening.id && previewVariation?.id === variation.id @@ -947,7 +903,6 @@ const BrowsePanel: React.FC<{ } return `${moveNum}. ...${suffix}` })(), - variationIsSelected, variationIsBeingPreviewed, () => { setPreviewOpening(opening) @@ -962,16 +917,6 @@ const BrowsePanel: React.FC<{ onOpeningClick(opening, variation) } }, - () => { - if (variationIsSelected) { - removeOpeningSelection(opening, variation) - } else { - addQuickSelection(opening, variation) - } - }, - variationIsSelected - ? 'Remove variation from selection' - : 'Add variation with current settings', )} ) @@ -1043,25 +988,6 @@ const DrillStudioPanel: React.FC<{ ? 'Already added with same settings' : disabledReason || undefined - const getSelectionSubtitle = (selection: OpeningSelection) => { - if ( - selection.opening.categoryType === 'endgame' && - selection.endgameTraits?.length - ) { - return selection.endgameTraits - .map((trait) => ENDGAME_TRAIT_LABELS[trait]) - .join(', ') - } - if (selection.variation?.name) { - return selection.variation.name - } - return selection.opening.categoryType === 'custom' - ? 'Custom position' - : selection.playerColor === 'white' - ? 'White' - : 'Black' - } - const renderEndgameTraitControls = () => (

@@ -1225,7 +1151,7 @@ const DrillStudioPanel: React.FC<{ }} className="w-full accent-human-4" /> -

+
5
@@ -1249,7 +1175,7 @@ const DrillStudioPanel: React.FC<{ onClick={addSelection} disabled={addDisabled} title={addButtonTitle} - className="w-full rounded-md bg-human-4/80 py-2.5 text-[14px] font-semibold text-black transition-colors hover:bg-human-4 disabled:cursor-not-allowed disabled:bg-white/[0.06] disabled:text-white/30 2xl:w-auto 2xl:px-10" + className="w-full rounded-md bg-human-4/80 py-2.5 text-[14px] font-semibold text-white transition-colors hover:bg-human-4 disabled:cursor-not-allowed disabled:bg-white/[0.06] disabled:text-white/30 2xl:w-auto 2xl:px-10" > {addButtonLabel} @@ -1270,21 +1196,9 @@ const DrillStudioPanel: React.FC<{ Select drills from the library to begin.
) : ( -
+
{selections.map((selection) => { - const isEndgame = - selection.opening.categoryType === 'endgame' - const label = selection.variation - ? `${selection.opening.name}: ${selection.variation.name}` - : selection.opening.name - const meta = - isEndgame && selection.endgameTraits?.length - ? selection.endgameTraits - .map((t) => ENDGAME_TRAIT_LABELS[t]) - .join(', ') - : selection.playerColor === 'white' - ? 'White' - : 'Black' + const detailLine = getSelectionDetailLine(selection) const isActive = previewOpening.id === selection.opening.id && @@ -1308,14 +1222,20 @@ const DrillStudioPanel: React.FC<{ : 'bg-white/[0.04] hover:bg-white/[0.06]' }`} > -

- - {label} - - · {meta} -

+
+ + {detailLine && ( +

+ {detailLine} +

+ )} + +
@@ -1523,7 +1430,7 @@ const SelectedPanel: React.FC<{ }} className="w-full accent-human-4" /> -
+
5
@@ -2171,20 +2078,6 @@ export const OpeningSelectionModal: React.FC = ({ } }, [hasTrackedModalOpen, initialSelections.length]) - // Update the ID to reflect the new settings - useEffect(() => { - setSelections((prevSelections) => - prevSelections.map((selection) => ({ - ...selection, - maiaVersion: selectedMaiaVersion.id, - targetMoveNumber: - getOpeningCategory(selection.opening) === 'endgame' - ? null - : targetMoveNumber, - })), - ) - }, [selectedMaiaVersion.id, targetMoveNumber]) - const handleStartTour = () => { startTour(tourConfigs.openingDrill.id, tourConfigs.openingDrill.steps, true) } @@ -2252,41 +2145,54 @@ export const OpeningSelectionModal: React.FC = ({ previewOpening.id, ]) - const isDuplicateSelection = useCallback( + const findMatchingSelection = useCallback( ( opening: Opening, variation: OpeningVariation | null, - traits: EndgameTrait[] = [], + { + playerColor = selectedColor, + maiaVersion = selectedMaiaVersion.id, + traits = [], + }: { + playerColor?: 'white' | 'black' + maiaVersion?: string + traits?: EndgameTrait[] + } = {}, ) => { const category = getOpeningCategory(opening) - if (category === 'endgame') { - const normalizedTraits = [...traits].sort().join('|') - return selections.some((selection) => { - if (getOpeningCategory(selection.opening) !== 'endgame') { - return false - } + const normalizedTraits = [...traits].sort().join('|') + + return ( + selections.find((selection) => { if (selection.opening.id !== opening.id) return false if ((selection.variation?.id ?? null) !== (variation?.id ?? null)) { return false } - const existingTraits = [...(selection.endgameTraits ?? [])] - .sort() - .join('|') - return existingTraits === normalizedTraits - }) - } + if (selection.maiaVersion !== maiaVersion) return false + + if (category === 'endgame') { + const existingTraits = [...(selection.endgameTraits ?? [])] + .sort() + .join('|') + return existingTraits === normalizedTraits + } - return selections.some( - (selection) => - selection.opening.id === opening.id && - selection.variation?.id === variation?.id && - selection.playerColor === selectedColor && - selection.maiaVersion === selectedMaiaVersion.id, + return selection.playerColor === playerColor + }) ?? null ) }, [selectedColor, selectedMaiaVersion.id, selections], ) + const isDuplicateSelection = useCallback( + ( + opening: Opening, + variation: OpeningVariation | null, + traits: EndgameTrait[] = [], + ) => !!findMatchingSelection(opening, variation, { traits }), + [findMatchingSelection], + ) + const addSelection = () => { const category = getOpeningCategory(previewOpening) if ( @@ -2320,8 +2226,8 @@ export const OpeningSelectionModal: React.FC = ({ const newSelection: OpeningSelection = { id: `endgame-${previewOpening.id}-${previewVariation?.id || 'all'}-${selectedTraits.slice().sort().join('-')}-${ - positions.length - }-${Date.now()}`, + selectedMaiaVersion.id + }`, opening: previewOpening, variation: previewVariation, playerColor: 'white', @@ -2342,7 +2248,7 @@ export const OpeningSelectionModal: React.FC = ({ if (isDuplicateSelection(previewOpening, previewVariation)) return const newSelection: OpeningSelection = { - id: `${previewOpening.id}-${previewVariation?.id || 'main'}-${selectedColor}-${selectedMaiaVersion.id}-${targetMoveNumber}`, + id: `${previewOpening.id}-${previewVariation?.id || 'main'}-${selectedColor}-${selectedMaiaVersion.id}`, opening: previewOpening, variation: previewVariation, playerColor: selectedColor, @@ -2397,12 +2303,16 @@ export const OpeningSelectionModal: React.FC = ({ return } - if (isDuplicateSelection(mobilePopupOpening, mobilePopupVariation)) { + if ( + findMatchingSelection(mobilePopupOpening, mobilePopupVariation, { + playerColor: color, + }) + ) { return } const newSelection: OpeningSelection = { - id: `${mobilePopupOpening.id}-${mobilePopupVariation?.id || 'main'}-${color}-${selectedMaiaVersion.id}-${targetMoveNumber}`, + id: `${mobilePopupOpening.id}-${mobilePopupVariation?.id || 'main'}-${color}-${selectedMaiaVersion.id}`, opening: mobilePopupOpening, variation: mobilePopupVariation, playerColor: color, @@ -2453,8 +2363,8 @@ export const OpeningSelectionModal: React.FC = ({ const newSelection: OpeningSelection = { id: `endgame-${mobilePopupOpening.id}-${mobilePopupVariation?.id || 'all'}-${traits.slice().sort().join('-')}-${ - positions.length - }-${Date.now()}`, + selectedMaiaVersion.id + }`, opening: mobilePopupOpening, variation: mobilePopupVariation, playerColor: 'white', @@ -2477,10 +2387,17 @@ export const OpeningSelectionModal: React.FC = ({ const handleMobilePopupRemove = () => { if (!mobilePopupOpening) return - const selectionToRemove = selections.find( - (s) => - s.opening.id === mobilePopupOpening.id && - s.variation?.id === mobilePopupVariation?.id, + const traits = + getOpeningCategory(mobilePopupOpening) === 'endgame' + ? getSelectedEndgameTraits(mobilePopupOpening, mobilePopupVariation) + : [] + + const selectionToRemove = findMatchingSelection( + mobilePopupOpening, + mobilePopupVariation, + { + traits, + }, ) if (selectionToRemove) { @@ -2495,87 +2412,9 @@ export const OpeningSelectionModal: React.FC = ({ const isOpeningSelected = ( opening: Opening, variation: OpeningVariation | null, + traits: EndgameTrait[] = [], ) => { - return selections.some( - (s) => s.opening.id === opening.id && s.variation?.id === variation?.id, - ) - } - - const addQuickSelection = ( - opening: Opening, - variation: OpeningVariation | null, - ) => { - const category = getOpeningCategory(opening) - if ( - activeSelectionCategory && - activeSelectionCategory !== category && - selections.length > 0 - ) { - return - } - - if (category === 'endgame') { - const selectedTraits = getSelectedEndgameTraits(opening, variation) - if (!selectedTraits.length) return - if (isDuplicateSelection(opening, variation, selectedTraits)) return - - const positions = buildEndgamePositions( - opening, - variation, - selectedTraits, - ) - if (!positions.length) return - - const scope = variation ? 'motif' : 'category' - const newSelection: OpeningSelection = { - id: `endgame-${opening.id}-${variation?.id || 'all'}-${selectedTraits.slice().sort().join('-')}-${ - positions.length - }-${Date.now()}`, - opening, - variation, - playerColor: 'white', - maiaVersion: selectedMaiaVersion.id, - targetMoveNumber: null, - endgameTraits: selectedTraits, - endgamePositions: positions, - endgameScope: scope, - } - - setSelections([...selections, newSelection]) - setPreviewOpening(opening) - setPreviewVariation(variation) - if (isMobile) { - setActiveTab('selected') - } - return - } - - if (isDuplicateSelection(opening, variation)) return - - if (!opening.isCustom) { - trackOpeningQuickAddUsed( - opening.name, - selectedColor, - selectedMaiaVersion.id, - targetMoveNumber, - ) - } - - const newSelection: OpeningSelection = { - id: `${opening.id}-${variation?.id || 'main'}-${selectedColor}-${selectedMaiaVersion.id}-${targetMoveNumber}`, - opening, - variation, - playerColor: selectedColor, - maiaVersion: selectedMaiaVersion.id, - targetMoveNumber, - } - - setSelections([...selections, newSelection]) - setPreviewOpening(opening) - setPreviewVariation(variation) - if (isMobile) { - setActiveTab('selected') - } + return !!findMatchingSelection(opening, variation, { traits }) } const handleStartDrilling = () => { @@ -2742,7 +2581,7 @@ export const OpeningSelectionModal: React.FC = ({ >

- Practice with Maia + Drill with Maia

Select drills, configure settings, practice against Maia 3. @@ -2767,13 +2606,9 @@ export const OpeningSelectionModal: React.FC = ({ setPreviewOpening={setPreviewOpening} setPreviewVariation={setPreviewVariation} setActiveTab={setActiveTab} - addQuickSelection={addQuickSelection} - isDuplicateSelection={isDuplicateSelection} searchTerm={searchTerm} setSearchTerm={setSearchTerm} - selections={selections} onOpeningClick={handleMobileOpeningClick} - removeSelection={removeSelection} onRemoveCustomOpening={handleRemoveCustomOpening} browseCategory={browseCategory} onBrowseCategoryChange={handleBrowseCategoryChange} @@ -2817,6 +2652,13 @@ export const OpeningSelectionModal: React.FC = ({ setPreviewOpening(selection.opening) setPreviewVariation(selection.variation ?? null) setSelectedColor(selection.playerColor) + setTargetMoveNumber(selection.targetMoveNumber) + const maiaVersion = MAIA3_OPPONENT_RATINGS.find( + (version) => version.id === selection.maiaVersion, + ) + if (maiaVersion) { + setSelectedMaiaVersion(maiaVersion) + } }} handleStartDrilling={handleStartDrilling} selectedMaiaVersion={selectedMaiaVersion} @@ -2862,6 +2704,7 @@ export const OpeningSelectionModal: React.FC = ({ isSelected={isOpeningSelected( mobilePopupOpening, mobilePopupVariation, + mobileSelectedTraits, )} isEndgame={mobilePopupOpening.categoryType === 'endgame'} selectedTraits={mobileSelectedTraits} diff --git a/src/hooks/useOpeningDrillController/useOpeningDrillController.ts b/src/hooks/useOpeningDrillController/useOpeningDrillController.ts index 9fbd4883..dee4b5c2 100644 --- a/src/hooks/useOpeningDrillController/useOpeningDrillController.ts +++ b/src/hooks/useOpeningDrillController/useOpeningDrillController.ts @@ -348,6 +348,9 @@ export const useOpeningDrillController = ( >(null) const [waitingForMaiaResponse, setWaitingForMaiaResponse] = useState(false) const [continueAnalyzingMode, setContinueAnalyzingMode] = useState(false) + const [isAwaitingExtensionDecision, setIsAwaitingExtensionDecision] = + useState(false) + const [isCurrentDrillExtended, setIsCurrentDrillExtended] = useState(false) const loadedCompletedDrillGameRef = useRef(null) const loadedCompletedDrillFinalNodeRef = useRef(null) const loadedCompletedDrillSelectionIdRef = useRef(null) @@ -420,6 +423,11 @@ export const useOpeningDrillController = ( useEffect(() => { baseSelectionsRef.current = expandedSelections attemptCountersRef.current = {} + bgCancelledRef.current = true + bgChainRef.current = Promise.resolve() + bgAnalyzedFensRef.current = new Set() + bgDrillIdRef.current = null + stockfish.stopEvaluation() setCompletedDrills([]) setInitialCycleComplete(false) setInitialDrillPointer(-1) @@ -428,6 +436,8 @@ export const useOpeningDrillController = ( setCurrentPerformanceData(null) setCurrentDrillGame(null) setDrillEndReasonMessage(null) + setIsAwaitingExtensionDecision(false) + setIsCurrentDrillExtended(false) analysisCancellationRef.current = false setDrillAnalysisProgress(getInitialAnalysisProgress()) @@ -442,7 +452,7 @@ export const useOpeningDrillController = ( setCurrentDrillNumber(1) setWaitingForMaiaResponse(false) setContinueAnalyzingMode(false) - }, [expandedSelections, createDrillInstance]) + }, [expandedSelections, createDrillInstance, stockfish]) useEffect(() => { if (!currentDrill) { @@ -458,6 +468,8 @@ export const useOpeningDrillController = ( setCurrentDrillGame(loadedCompletedDrillGame) setWaitingForMaiaResponse(false) setContinueAnalyzingMode(true) + setIsAwaitingExtensionDecision(false) + setIsCurrentDrillExtended(false) loadedCompletedDrillGameRef.current = null return } @@ -489,6 +501,8 @@ export const useOpeningDrillController = ( setCurrentDrillGame(drillGame) setWaitingForMaiaResponse(false) setContinueAnalyzingMode(false) + setIsAwaitingExtensionDecision(false) + setIsCurrentDrillExtended(false) setDrillEndReasonMessage(null) }, [currentDrill]) @@ -544,6 +558,10 @@ export const useOpeningDrillController = ( if (!currentDrillGame || !currentDrill || continueAnalyzingMode) return false + if (isAwaitingExtensionDecision || isCurrentDrillExtended) { + return false + } + const boardTerminationReason = resolveBoardTerminationReason( gameTree.toChess(), ) @@ -559,6 +577,8 @@ export const useOpeningDrillController = ( currentDrill, currentDrillGame, gameTree, + isAwaitingExtensionDecision, + isCurrentDrillExtended, treeController.currentNode, ]) @@ -648,10 +668,13 @@ export const useOpeningDrillController = ( if (node.move && node.san) { const moveIndex = currentPath.length - 2 - const isPlayerMove = - selection.playerColor === 'white' - ? moveIndex % 2 === 0 - : moveIndex % 2 === 1 + const prevNode = currentPath[currentPath.length - 2] + const moverColor = prevNode + ? new Chess(prevNode.fen).turn() === 'w' + ? 'white' + : 'black' + : null + const isPlayerMove = moverColor === selection.playerColor const stockfishEval = node.analysis?.stockfish const maiaEval = node.analysis?.maia?.[currentMaiaModel] @@ -672,7 +695,6 @@ export const useOpeningDrillController = ( const evaluation = stockfishEval?.model_optimal_cp as number - const prevNode = currentPath[currentPath.length - 2] const prevEvaluation = prevNode?.analysis?.stockfish ?.model_optimal_cp as number const evaluationLoss = Math.abs(evaluation - prevEvaluation) @@ -710,7 +732,9 @@ export const useOpeningDrillController = ( san: node.san, fen: node.fen, fenBeforeMove: prevNode?.fen, - moveNumber: Math.ceil((moveIndex + 1) / 2), + moveNumber: prevNode + ? parseInt(prevNode.fen.split(' ')[5], 10) || 1 + : 1, isPlayerMove, evaluation, classification, @@ -1056,12 +1080,11 @@ export const useOpeningDrillController = ( // If drill changed, reset for the new drill if (currentDrillGame.id !== bgDrillIdRef.current) { bgCancelledRef.current = true - // Let any in-flight work finish with the cancelled flag, then reset - bgChainRef.current = bgChainRef.current.then(() => { - bgCancelledRef.current = false - }) + stockfish.stopEvaluation() + bgChainRef.current = Promise.resolve() bgAnalyzedFensRef.current = new Set() bgDrillIdRef.current = currentDrillGame.id + bgCancelledRef.current = false } const mainLine = gameTree.getMainLine() @@ -1070,6 +1093,7 @@ export const useOpeningDrillController = ( ? Math.max(mainLine.indexOf(openingEndNode), 0) : 0 const drillNodes = mainLine.slice(startIndex) + const scheduledDrillId = currentDrillGame.id for (const node of drillNodes) { if (bgAnalyzedFensRef.current.has(node.fen)) continue @@ -1079,7 +1103,12 @@ export const useOpeningDrillController = ( // Wrapped in try/catch so one failure doesn't break the whole chain. bgChainRef.current = bgChainRef.current.then(async () => { try { - if (bgCancelledRef.current) return + if ( + bgCancelledRef.current || + bgDrillIdRef.current !== scheduledDrillId + ) { + return + } console.log('[bg] maia start:', node.san || node.move || '?') await ensureMaiaRef.current(node) const hasMaia = !!( @@ -1087,7 +1116,12 @@ export const useOpeningDrillController = ( MAIA_MODELS.every((m) => node.analysis.maia?.[m]) ) console.log('[bg] maia done:', hasMaia, '| sf start') - if (bgCancelledRef.current) return + if ( + bgCancelledRef.current || + bgDrillIdRef.current !== scheduledDrillId + ) { + return + } await ensureStockfishRef.current(node) console.log( '[bg] sf done, depth:', @@ -1098,7 +1132,13 @@ export const useOpeningDrillController = ( } }) } - }, [currentDrillGame, gameTree, isAnalyzingDrill, treeController.currentNode]) + }, [ + currentDrillGame, + gameTree, + isAnalyzingDrill, + stockfish, + treeController.currentNode, + ]) // Stop background analysis. Signals cancellation and stops stockfish so // ensureDrillAnalysis can use stockfish immediately. The chain's remaining @@ -1210,14 +1250,23 @@ export const useOpeningDrillController = ( const persistCompletedDrill = useCallback((drill: CompletedDrill) => { setCompletedDrills((prev) => { - const alreadyPresent = prev.some((existing) => { + const existingIndex = prev.findIndex((existing) => { return ( existing.selection.id === drill.selection.id && - existing.completedAt.getTime() === drill.completedAt.getTime() + existing.finalNode.fen === drill.finalNode.fen ) }) - return alreadyPresent ? prev : [...prev, drill] + if (existingIndex === -1) { + return [...prev, drill] + } + + const next = [...prev] + next[existingIndex] = { + ...next[existingIndex], + ...drill, + } + return next }) }, []) @@ -1233,6 +1282,7 @@ export const useOpeningDrillController = ( if (boardTerminationReason) return boardTerminationReason if ( + !isCurrentDrillExtended && drillGame.selection.targetMoveNumber !== null && drillGame.playerMoveCount >= drillGame.selection.targetMoveNumber ) { @@ -1241,6 +1291,81 @@ export const useOpeningDrillController = ( return null }, + [isCurrentDrillExtended], + ) + + const buildPerformanceData = useCallback( + async ( + drillGame: OpeningDrillGame, + completionReason?: DrillCompletionReason, + ) => { + const resolvedReason = resolveDrillEndReason(drillGame, completionReason) + const completionNote = resolvedReason + ? getDrillEndReasonMessage(resolvedReason, drillGame) + : null + + try { + await logOpeningDrill({ + opening_fen: drillGame.selection.variation + ? drillGame.selection.variation.fen + : drillGame.selection.opening.fen, + side_played: drillGame.selection.playerColor, + opponent: drillGame.selection.maiaVersion, + num_moves: drillGame.moves.length, + moves_played_uci: drillGame.moves, + }) + } catch (error) { + console.error('Failed to log opening drill:', error) + } + + const analysisSuccessful = await ensureDrillAnalysis(drillGame) + if (!analysisSuccessful) { + return null + } + + const performanceData = await evaluateDrillPerformance(drillGame) + const feedback = completionNote + ? [ + completionNote, + ...performanceData.feedback.filter( + (entry) => entry !== completionNote, + ), + ] + : performanceData.feedback + const completedDrill = { + ...performanceData.drill, + feedback, + } + const enrichedPerformanceData = { + ...performanceData, + drill: completedDrill, + feedback, + } + + persistCompletedDrill(completedDrill) + return enrichedPerformanceData + }, + [ + ensureDrillAnalysis, + evaluateDrillPerformance, + persistCompletedDrill, + resolveDrillEndReason, + ], + ) + + const promptExtendCurrentDrill = useCallback( + (drillGame: OpeningDrillGame) => { + const target = drillGame.selection.targetMoveNumber + setIsAwaitingExtensionDecision(true) + setIsCurrentDrillExtended(false) + setIsAnalyzingDrill(false) + setWaitingForMaiaResponse(false) + setDrillEndReasonMessage( + target !== null + ? `Target reached (${drillGame.playerMoveCount}/${target} moves). Extend drill or end with feedback.` + : 'Target reached. Extend drill or end with feedback.', + ) + }, [], ) @@ -1260,49 +1385,16 @@ export const useOpeningDrillController = ( } try { + setIsAwaitingExtensionDecision(false) setIsAnalyzingDrill(true) - - // Submit drill data to backend once the drill is complete - try { - await logOpeningDrill({ - opening_fen: drillGame.selection.variation - ? drillGame.selection.variation.fen - : drillGame.selection.opening.fen, - side_played: drillGame.selection.playerColor, - opponent: drillGame.selection.maiaVersion, - num_moves: drillGame.moves.length, - moves_played_uci: drillGame.moves, - }) - } catch (error) { - console.error('Failed to log opening drill:', error) - // Continue even if backend submission fails - } - - const analysisSuccessful = await ensureDrillAnalysis(drillGame) - if (!analysisSuccessful) { + const enrichedPerformanceData = await buildPerformanceData( + drillGame, + completionReason, + ) + if (!enrichedPerformanceData) { return } - - // Simple performance evaluation without complex analysis tracking - - const performanceData = await evaluateDrillPerformance(drillGame) - const enrichedPerformanceData = completionNote - ? { - ...performanceData, - feedback: [ - completionNote, - ...performanceData.feedback.filter( - (entry) => entry !== completionNote, - ), - ], - } - : performanceData - setCurrentPerformanceData(enrichedPerformanceData) - persistCompletedDrill(enrichedPerformanceData.drill) - - // Simplified: just show the performance modal - setShowPerformanceModal(true) } catch (error) { console.error('Error completing drill analysis:', error) @@ -1311,13 +1403,7 @@ export const useOpeningDrillController = ( setIsAnalyzingDrill(false) } }, - [ - currentDrillGame, - ensureDrillAnalysis, - evaluateDrillPerformance, - persistCompletedDrill, - resolveDrillEndReason, - ], + [buildPerformanceData, currentDrillGame, resolveDrillEndReason], ) const completeDrillWithDelay = useCallback( @@ -1337,21 +1423,47 @@ export const useOpeningDrillController = ( [completeDrill, resolveDrillEndReason], ) - const moveToNextDrill = useCallback(() => { + const moveToNextDrill = useCallback(async () => { if (currentPerformanceData?.drill) { persistCompletedDrill(currentPerformanceData.drill) + } else if (currentDrillGame) { + const completionReason = isAwaitingExtensionDecision + ? 'target_moves_reached' + : (resolveDrillEndReason(currentDrillGame) ?? undefined) + + if (completionReason) { + try { + setIsAnalyzingDrill(true) + await buildPerformanceData(currentDrillGame, completionReason) + } catch (error) { + console.error('Error persisting completed drill:', error) + } finally { + setIsAnalyzingDrill(false) + } + } } + setShowPerformanceModal(false) setCurrentPerformanceData(null) setContinueAnalyzingMode(false) setAnalysisEnabled(false) + setIsAwaitingExtensionDecision(false) + setIsCurrentDrillExtended(false) setDrillEndReasonMessage(null) setWaitingForMaiaResponse(false) analysisCancellationRef.current = false setDrillAnalysisProgress(getInitialAnalysisProgress()) setCurrentDrillGame(null) assignNextDrill() - }, [assignNextDrill, currentPerformanceData, persistCompletedDrill]) + }, [ + assignNextDrill, + buildPerformanceData, + currentDrillGame, + currentPerformanceData, + isAwaitingExtensionDecision, + persistCompletedDrill, + resolveDrillEndReason, + ]) // Continue analyzing current drill const continueAnalyzing = useCallback(() => { @@ -1364,32 +1476,71 @@ export const useOpeningDrillController = ( setShowPerformanceModal(false) setAnalysisEnabled(true) setContinueAnalyzingMode(true) + setIsAwaitingExtensionDecision(false) setWaitingForMaiaResponse(false) }, [currentDrillGame, treeController]) + const extendCurrentDrill = useCallback(() => { + if (!currentDrillGame || !isAwaitingExtensionDecision) { + return + } + + setIsCurrentDrillExtended(true) + setIsAwaitingExtensionDecision(false) + setDrillEndReasonMessage(null) + setWaitingForMaiaResponse(true) + }, [currentDrillGame, isAwaitingExtensionDecision]) + const showPerformance = useCallback(async () => { if (!currentDrillGame) return try { setIsAnalyzingDrill(true) - const analysisSuccessful = await ensureDrillAnalysis(currentDrillGame) - if (!analysisSuccessful) { - return + const completionReason = isAwaitingExtensionDecision + ? 'target_moves_reached' + : (resolveDrillEndReason(currentDrillGame) ?? undefined) + + if (completionReason) { + const performanceData = await buildPerformanceData( + currentDrillGame, + completionReason, + ) + if (!performanceData) { + return + } + setCurrentPerformanceData(performanceData) + } else { + const analysisSuccessful = await ensureDrillAnalysis(currentDrillGame) + if (!analysisSuccessful) { + return + } + const performanceData = await evaluateDrillPerformance(currentDrillGame) + setCurrentPerformanceData(performanceData) } - const performanceData = await evaluateDrillPerformance(currentDrillGame) - setCurrentPerformanceData(performanceData) setShowPerformanceModal(true) } catch (error) { console.error('Error analyzing current drill performance:', error) } finally { setIsAnalyzingDrill(false) } - }, [currentDrillGame, ensureDrillAnalysis, evaluateDrillPerformance]) + }, [ + buildPerformanceData, + currentDrillGame, + ensureDrillAnalysis, + evaluateDrillPerformance, + isAwaitingExtensionDecision, + resolveDrillEndReason, + ]) // Shows performance modal for current drill const showCurrentPerformance = useCallback(() => { + if (currentPerformanceData) { + setShowPerformanceModal(true) + return + } + showPerformance() - }, [showPerformance]) + }, [currentPerformanceData, showPerformance]) const loadCompletedDrill = useCallback((completedDrill: CompletedDrill) => { const rootNode = getRootNode(completedDrill.finalNode) @@ -1423,6 +1574,8 @@ export const useOpeningDrillController = ( setCurrentDrillGame(restoredGame) setAnalysisEnabled(true) setContinueAnalyzingMode(true) + setIsAwaitingExtensionDecision(false) + setIsCurrentDrillExtended(false) setShowPerformanceModal(false) setCurrentPerformanceData(null) setWaitingForMaiaResponse(false) @@ -1432,6 +1585,11 @@ export const useOpeningDrillController = ( // Reset drill to start over const resetDrillSession = useCallback(() => { attemptCountersRef.current = {} + bgCancelledRef.current = true + bgChainRef.current = Promise.resolve() + bgAnalyzedFensRef.current = new Set() + bgDrillIdRef.current = null + stockfish.stopEvaluation() setCompletedDrills([]) setInitialCycleComplete(false) setInitialDrillPointer(-1) @@ -1440,6 +1598,8 @@ export const useOpeningDrillController = ( setCurrentDrillGame(null) setAnalysisEnabled(false) setContinueAnalyzingMode(false) + setIsAwaitingExtensionDecision(false) + setIsCurrentDrillExtended(false) setShowPerformanceModal(false) setCurrentPerformanceData(null) setDrillEndReasonMessage(null) @@ -1455,7 +1615,7 @@ export const useOpeningDrillController = ( setCurrentDrill(firstInstance) setInitialDrillPointer(0) setCurrentDrillNumber(1) - }, [createDrillInstance]) + }, [createDrillInstance, stockfish]) // Make a move for the player const makePlayerMove = useCallback( @@ -1548,11 +1708,12 @@ export const useOpeningDrillController = ( if (boardTerminationReason) { completeDrillWithDelay(updatedGame, boardTerminationReason) } else if ( + !isCurrentDrillExtended && currentDrill && currentDrill.targetMoveNumber !== null && updatedGame.playerMoveCount >= currentDrill.targetMoveNumber ) { - completeDrillWithDelay(updatedGame, 'target_moves_reached') + promptExtendCurrentDrill(updatedGame) } else { console.log( 'Setting waitingForMaiaResponse to true after player move', @@ -1573,8 +1734,10 @@ export const useOpeningDrillController = ( currentDrill, completeDrillWithDelay, continueAnalyzingMode, + isCurrentDrillExtended, isDrillComplete, isAnalyzingDrill, + promptExtendCurrentDrill, treeController, ], ) @@ -1914,6 +2077,8 @@ export const useOpeningDrillController = ( setDrillEndReasonMessage(null) setWaitingForMaiaResponse(false) setContinueAnalyzingMode(false) + setIsAwaitingExtensionDecision(false) + setIsCurrentDrillExtended(false) }, [currentDrill]) return { @@ -1927,6 +2092,8 @@ export const useOpeningDrillController = ( isPlayerTurn, isDrillComplete, isAtOpeningEnd, + isAwaitingExtensionDecision, + isCurrentDrillExtended, drillEndReasonMessage, // Tree controller @@ -1950,6 +2117,7 @@ export const useOpeningDrillController = ( completeDrill, moveToNextDrill, continueAnalyzing, + extendCurrentDrill, endCurrentDrillWithFeedback, // Analysis diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 7a172967..6f16bcf2 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -41,14 +41,14 @@ function MaiaPlatform({ Component, pageProps }: AppProps) { const isAnalysisPage = router.pathname.startsWith('/analysis') const isPageWithMaia = [ '/analysis', - '/openings', + '/drills', '/puzzles', '/settings', '/broadcast', ].some((path) => router.pathname.includes(path)) const isPageWithStockfish = [ '/analysis', - '/openings', + '/drills', '/puzzles', '/broadcast', ].some((path) => router.pathname.includes(path)) diff --git a/src/pages/drills/index.tsx b/src/pages/drills/index.tsx new file mode 100644 index 00000000..7121d9b1 --- /dev/null +++ b/src/pages/drills/index.tsx @@ -0,0 +1 @@ +export { default } from '../openings' diff --git a/src/pages/openings/index.tsx b/src/pages/openings/index.tsx index 57bd728d..a56a7845 100644 --- a/src/pages/openings/index.tsx +++ b/src/pages/openings/index.tsx @@ -88,9 +88,18 @@ const OpeningsPage: NextPage = () => { ) const handleCloseModal = () => { + if (drillConfiguration) { + setShowSelectionModal(false) + return + } + router.push('/') } + const handleReconfigureDrills = useCallback(() => { + setShowSelectionModal(true) + }, []) + const [drillConfiguration, setDrillConfiguration] = useState(null) const [promotionFromTo, setPromotionFromTo] = useState< @@ -728,17 +737,23 @@ const OpeningsPage: NextPage = () => { } }, [controller.gameTree, controller.currentDrill?.id]) - const targetMoves = controller.currentDrill?.targetMoveNumber ?? null + const configuredTargetMoves = + controller.currentDrill?.targetMoveNumber ?? null + const targetMoves = controller.isCurrentDrillExtended + ? null + : configuredTargetMoves const targetMovesLabel = typeof targetMoves === 'number' ? targetMoves : '∞' const moveProgressPercent = - controller.currentDrillGame && - typeof targetMoves === 'number' && - targetMoves > 0 - ? Math.min( - (controller.currentDrillGame.playerMoveCount / targetMoves) * 100, - 100, - ) - : 0 + controller.currentDrillGame && controller.isCurrentDrillExtended + ? 100 + : controller.currentDrillGame && + typeof targetMoves === 'number' && + targetMoves > 0 + ? Math.min( + (controller.currentDrillGame.playerMoveCount / targetMoves) * 100, + 100, + ) + : 0 const moveListTerminationNote = useMemo(() => { if (!controller.drillEndReasonMessage) return undefined @@ -992,13 +1007,23 @@ const OpeningsPage: NextPage = () => { const renderDrillActionButtons = (fullWidth = false) => (

+ {controller.isAwaitingExtensionDecision && + !controller.showPerformanceModal && + !controller.continueAnalyzingMode && ( + + )} {controller.currentPerformanceData && !controller.showPerformanceModal && ( )} {!controller.currentPerformanceData && @@ -1008,18 +1033,41 @@ const OpeningsPage: NextPage = () => { onClick={controller.endCurrentDrillWithFeedback} className={`${fullWidth ? 'w-full' : ''} rounded-md border border-human-4/50 bg-human-4/10 px-4 py-2 text-sm font-medium text-human-3 transition-colors hover:bg-human-4/20`} > - End Drill + Feedback + {controller.isAwaitingExtensionDecision ? 'Review' : 'End & Review'} + + )} + {!controller.showPerformanceModal && + !controller.continueAnalyzingMode && ( + )} -
) + const renderPostDrillAnalysisButtons = (fullWidth = false) => + controller.continueAnalyzingMode ? ( +
+ {controller.currentPerformanceData && ( + + )} + +
+ ) : null + const renderLiveDrillSummary = () => controller.currentDrill ? (
@@ -1476,7 +1524,7 @@ const OpeningsPage: NextPage = () => {
- {renderDrillActionButtons()} + {renderPostDrillAnalysisButtons()}
{/* Right Panel - Analysis Sidebar (same as analysis page) */} @@ -1738,7 +1786,7 @@ const OpeningsPage: NextPage = () => { )} {/* Action Buttons */} - {renderDrillActionButtons(true)} + {renderPostDrillAnalysisButtons(true)} {/* Analysis Components Stacked */}
@@ -1907,7 +1955,7 @@ const OpeningsPage: NextPage = () => { performanceData={controller.currentPerformanceData} onContinueAnalyzing={controller.continueAnalyzing} onNextDrill={controller.moveToNextDrill} - isLastDrill={false} + onReconfigureDrills={handleReconfigureDrills} /> )} diff --git a/src/types/openings.ts b/src/types/openings.ts index f22f2498..1400fbac 100644 --- a/src/types/openings.ts +++ b/src/types/openings.ts @@ -165,6 +165,7 @@ export interface CompletedDrill { goodMoves: string[] finalEvaluation: number completedAt: Date + feedback?: string[] // Enhanced analysis data moveAnalyses?: MoveAnalysis[] accuracyPercentage?: number