diff --git a/src/components/Openings/OpeningDrillAnalysis.tsx b/src/components/Openings/OpeningDrillAnalysis.tsx index 7a2f78b6..7fba8db9 100644 --- a/src/components/Openings/OpeningDrillAnalysis.tsx +++ b/src/components/Openings/OpeningDrillAnalysis.tsx @@ -226,7 +226,6 @@ export const OpeningDrillAnalysis: React.FC = ({ )} - ) diff --git a/src/components/Openings/OpeningSelectionModal.tsx b/src/components/Openings/OpeningSelectionModal.tsx index 975728d3..c5229cf3 100644 --- a/src/components/Openings/OpeningSelectionModal.tsx +++ b/src/components/Openings/OpeningSelectionModal.tsx @@ -263,7 +263,7 @@ const MobileOpeningPopup: React.FC = ({ className="flex-1 rounded border border-glass-border bg-white/5 py-2 text-sm font-medium text-white backdrop-blur-sm transition-colors hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-50" title={addTitle} > - {isDuplicate ? 'Drill Already Added' : 'Add Drill'} + {isDuplicate ? 'Already Added' : 'Add Drill'} )} @@ -384,7 +384,7 @@ const BrowsePanel: React.FC<{ } const renderTabs = () => ( -
+
{[ { label: 'Openings', value: 'openings' as const }, { label: 'Endgames', value: 'endgames' as const }, @@ -400,17 +400,17 @@ const BrowsePanel: React.FC<{ setActiveTab('browse') }} aria-pressed={isSelected} - className={`relative flex-1 px-3 py-2 text-xs font-medium transition-all duration-200 md:text-sm ${ + className={`relative flex-1 border-r border-white/5 px-3 py-3 text-xs font-medium transition-all duration-200 last:border-r-0 md:text-sm ${ isSelected - ? 'bg-white/10 text-white' - : 'hover:bg-white/8 bg-white/5 text-white/60 hover:text-white/90' + ? 'bg-white/[0.06] text-white' + : 'bg-transparent text-white/55 hover:bg-white/[0.03] hover:text-white/90' }`} > {label} {isSelected && ( )} @@ -423,38 +423,40 @@ const BrowsePanel: React.FC<{ return (
{renderTabs()}
{ e.preventDefault() onAddCustomPosition() }} > -
+
setCustomInput(e.target.value)} - placeholder="Drill a custom FEN/PGN" - className="h-full flex-1 rounded border border-glass-border bg-white/5 px-3 text-sm text-white placeholder-primary/50 focus:outline-none focus:ring-1 focus:ring-white/20" + placeholder="Paste FEN or PGN…" + className="flex-1 rounded-md border border-white/[0.08] bg-white/[0.04] px-3 py-[9px] text-[13px] text-white placeholder-white/35 focus:outline-none focus:ring-1 focus:ring-white/15" />
- {customError &&

{customError}

} + {customError && ( +

{customError}

+ )} -
+
- + search setSearchTerm(e.target.value)} - className="w-full rounded border border-glass-border bg-white/5 py-2 pl-10 pr-4 text-sm text-white placeholder-white/60 backdrop-blur-sm focus:outline-none focus:ring-1 focus:ring-white/20" + className="w-full rounded-md border border-white/[0.08] bg-white/[0.04] py-[9px] pl-9 pr-3 text-[13px] text-white placeholder-white/35 focus:outline-none focus:ring-1 focus:ring-white/15" />
-
-

Saved custom positions:

-
- -
+
{filteredOpenings.length === 0 ? ( -
+
No saved positions yet. Add a FEN or PGN above to get started.
) : ( @@ -489,19 +487,19 @@ const BrowsePanel: React.FC<{ return (
{ setPreviewOpening(opening) setPreviewVariation(null) @@ -532,18 +530,20 @@ const BrowsePanel: React.FC<{
-

{opening.name}

- +

+ {opening.name} +

+ Custom
-

+

{opening.description}

-
+
{openingIsSelected ? ( @@ -566,7 +566,7 @@ const BrowsePanel: React.FC<{ className="rounded p-1 text-secondary/60 transition-colors hover:text-secondary disabled:cursor-not-allowed disabled:opacity-30 group-hover:text-secondary/80" title="Add position with current settings" > - + add @@ -579,7 +579,7 @@ const BrowsePanel: React.FC<{ className="rounded p-1 text-secondary/60 transition-colors hover:text-secondary" title="Remove custom position" > - + delete @@ -594,29 +594,73 @@ const BrowsePanel: React.FC<{ ) } - return ( + const renderRow = ( + label: string, + pgn: string, + isItemSelected: boolean, + isPreviewed: boolean, + onSelect: () => void, + onToggle: () => void, + toggleTitle: string, + ) => (
- {renderTabs()} -
-

Select {categoryLabelPlural}

-

- Browse and select {categoryLabelPlural.toLowerCase()} to drill. +

{ + if (e.key === 'Enter' || e.key === ' ') { + onSelect() + } + }} + > +

+ {label}

+ {pgn &&

{pgn}

}
+ +
+ ) -
-

Select {categoryLabelPlural}

-

- Choose {categoryLabelPlural.toLowerCase()} to practice -

-
+ return ( +
+ {renderTabs()} -
+
- + search setSearchTerm(e.target.value)} - className="w-full rounded border border-glass-border bg-white/5 py-2 pl-10 pr-4 text-sm text-white placeholder-white/60 backdrop-blur-sm focus:outline-none focus:ring-1 focus:ring-white/20" + className="w-full rounded-lg border border-white/[0.08] bg-white/[0.06] py-[9px] pl-9 pr-4 text-[13px] text-white placeholder-white/35 focus:border-white/20 focus:outline-none" />
@@ -641,27 +685,33 @@ const BrowsePanel: React.FC<{ ) const openingIsBeingPreviewed = previewOpening.id === opening.id && !previewVariation + const showStandaloneOpening = opening.variations.length === 0 + return ( -
-
-
-
{ +
+
+

+ {opening.name} +

+ {opening.pgn && opening.variations.length > 0 && ( +

+ {opening.pgn} +

+ )} + {opening.description && ( +

+ {opening.description} +

+ )} +
+ + {showStandaloneOpening + ? renderRow( + opening.name, + opening.pgn, + openingIsSelected, + openingIsBeingPreviewed, + () => { setPreviewOpening(opening) setPreviewVariation(null) trackOpeningPreviewSelected( @@ -672,62 +722,20 @@ const BrowsePanel: React.FC<{ if (isMobile) { onOpeningClick(opening, null) } - }} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - setPreviewOpening(opening) - setPreviewVariation(null) - trackOpeningPreviewSelected( - opening.name, - opening.id, - false, - ) - if (isMobile) { - onOpeningClick(opening, null) - } + }, + () => { + if (openingIsSelected) { + removeOpeningSelection(opening, null) + } else { + addQuickSelection(opening, null) } - }} - > -
-
-

{opening.name}

-

- {opening.description} -

-
-
-
-
- {openingIsSelected ? ( - - ) : ( - - )} -
-
-
+ }, + openingIsSelected + ? `Remove ${categoryLabel.toLowerCase()} from selection` + : `Add ${categoryLabel.toLowerCase()} with current settings`, + ) + : null} + {opening.variations.map((variation) => { const variationIsSelected = selections.some( (selection) => @@ -739,89 +747,58 @@ const BrowsePanel: React.FC<{ previewVariation?.id === variation.id return ( -
-
-
{ - setPreviewOpening(opening) - setPreviewVariation(variation) - trackOpeningPreviewSelected( - opening.name, - opening.id, - true, - variation.name, - ) - if (isMobile) { - onOpeningClick(opening, variation) - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - setPreviewOpening(opening) - setPreviewVariation(variation) - trackOpeningPreviewSelected( - opening.name, - opening.id, - true, - variation.name, - ) - if (isMobile) { - onOpeningClick(opening, variation) - } - } - }} - > -
-

- {variation.name} -

-
-
- {variationIsSelected ? ( - - ) : ( - - )} -
-
+ + {renderRow( + variation.name, + (() => { + if (!variation.pgn.startsWith(opening.pgn)) + return variation.pgn + const suffix = variation.pgn + .slice(opening.pgn.length) + .trim() + if (!suffix) return '' + // Find the last move number in the parent PGN to determine context + const moveNumMatch = opening.pgn.match( + /(\d+)\.\s*(\S+)\s*(\S+)?\s*$/, + ) + if (!moveNumMatch) return suffix + const moveNum = parseInt(moveNumMatch[1]) + const hasWhiteReply = !!moveNumMatch[3] + // If parent ended after black's move (both white+black present), + // suffix starts with a new white move + if (hasWhiteReply) { + return `${moveNum + 1}. ${suffix}` + } + // Parent ended after white's move, suffix is black's reply + return `${moveNum}. ...${suffix}` + })(), + variationIsSelected, + variationIsBeingPreviewed, + () => { + setPreviewOpening(opening) + setPreviewVariation(variation) + trackOpeningPreviewSelected( + opening.name, + opening.id, + true, + variation.name, + ) + if (isMobile) { + onOpeningClick(opening, variation) + } + }, + () => { + if (variationIsSelected) { + removeOpeningSelection(opening, variation) + } else { + addQuickSelection(opening, variation) + } + }, + variationIsSelected + ? 'Remove variation from selection' + : 'Add variation with current settings', + )} + ) })}
@@ -831,7 +808,7 @@ const BrowsePanel: React.FC<{
) } -const PreviewPanel: React.FC<{ +const DrillStudioPanel: React.FC<{ previewOpening: Opening previewVariation: OpeningVariation | null previewFen: string @@ -846,6 +823,15 @@ const PreviewPanel: React.FC<{ selectedTraits: EndgameTrait[] availableTraits: EndgameTrait[] onToggleTrait: (trait: EndgameTrait) => void + selections: OpeningSelection[] + removeSelection: (id: string) => void + onSelectQueueItem: (selection: OpeningSelection) => void + handleStartDrilling: () => void + selectedMaiaVersion: (typeof MAIA_MODELS_WITH_NAMES)[0] + setSelectedMaiaVersion: (version: (typeof MAIA_MODELS_WITH_NAMES)[0]) => void + targetMoveNumber: number | null + setTargetMoveNumber: (number: number | null) => void + showTargetSlider: boolean }> = ({ previewOpening, previewVariation, @@ -861,16 +847,46 @@ const PreviewPanel: React.FC<{ selectedTraits, availableTraits, onToggleTrait, + selections, + removeSelection, + onSelectQueueItem, + handleStartDrilling, + selectedMaiaVersion, + setSelectedMaiaVersion, + targetMoveNumber, + setTargetMoveNumber, + showTargetSlider, }) => { const addDisabled = isDuplicate || isAddDisabled - const addButtonLabel = isDuplicate ? 'Drill Already Added' : 'Add to Drill' + const addButtonLabel = isDuplicate ? 'Already Added' : 'Add Drill' const addButtonTitle = isDuplicate ? '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 = () => (
-

Include traits:

+

+ Included Traits +

{availableTraits.length === 0 ? (

No positions available for this selection. @@ -911,94 +927,241 @@ const PreviewPanel: React.FC<{ return (

-
-

Preview {panelLabel}

-

Configure your drill settings

-
+
+ {/* Scrollable Content */} +
+
+ {/* Preview: Board + Info side by side */} +
+
+ +
-
-
-

- {previewOpening.name} - - {previewVariation && ` → ${previewVariation.name}`} - -

-

{previewOpening.description}

-
+
+
+

+ Preview +

+

+ {previewVariation?.name || previewOpening.name} +

+

+ {previewVariation ? previewOpening.name : panelLabel} ·{' '} + {previewOpening.description} +

+
- {isEndgame ? ( - renderEndgameTraitControls() - ) : ( -
-

Play as:

-
- + ))} +
+ )} + + {disabledReason && !isDuplicate ? ( +

{disabledReason}

+ ) : null} +
+
+ + {/* Settings row: Opponent + Target Moves */} +
+
+ + +
+ + {showTargetSlider ? ( +
+ +
+ { + const val = parseInt(e.target.value) + setTargetMoveNumber(val >= 21 ? null : val) + }} + className="w-full accent-human-4" + /> +
+ 5 + +
+
- White - - + )}
-
- )} -
-

Preview:

-
- + {/* Add Drill button */} + + + {/* Queue */} +
+
+

+ Queue +

+ + {selections.length} + +
+ + {selections.length === 0 ? ( +
+ 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 isActive = + previewOpening.id === selection.opening.id && + (selection.variation + ? previewVariation?.id === selection.variation.id + : !previewVariation) + + return ( +
onSelectQueueItem(selection)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') + onSelectQueueItem(selection) + }} + className={`flex cursor-pointer items-center gap-2 rounded-md px-2.5 py-[7px] transition-colors ${ + isActive + ? 'bg-white/[0.08]' + : 'bg-white/[0.04] hover:bg-white/[0.06]' + }`} + > +

+ + {label} + + · {meta} +

+ +
+ ) + })} +
+ )} +
-
-
- + {/* Fixed footer: Start button */} +
+ +
) @@ -1011,8 +1174,8 @@ const SelectedPanel: React.FC<{ handleStartDrilling: () => void selectedMaiaVersion: (typeof MAIA_MODELS_WITH_NAMES)[0] setSelectedMaiaVersion: (version: (typeof MAIA_MODELS_WITH_NAMES)[0]) => void - targetMoveNumber: number - setTargetMoveNumber: (number: number) => void + targetMoveNumber: number | null + setTargetMoveNumber: (number: number | null) => void categoryLabel: string categoryLabelPlural: string showTargetSlider: boolean @@ -1031,19 +1194,9 @@ const SelectedPanel: React.FC<{ }) => (
-
-

- Selected {categoryLabelPlural} ({selections.length}) -

-

- Click × to remove from the selection -

-
- - {/* Mobile header */} -
+

Selected ({selections.length})

Tap to remove

@@ -1176,21 +1329,23 @@ const SelectedPanel: React.FC<{ {showTargetSlider && (

- Target Move Count: {targetMoveNumber} + Target Move Count:{' '} + {targetMoveNumber === null ? '∞' : targetMoveNumber}

- setTargetMoveNumber(parseInt(e.target.value) || 10) - } + max="21" + value={targetMoveNumber === null ? 21 : targetMoveNumber} + onChange={(e) => { + const val = parseInt(e.target.value) + setTargetMoveNumber(val >= 21 ? null : val) + }} className="w-full accent-human-4" />
5 - 20 +
)} @@ -1378,7 +1533,9 @@ export const OpeningSelectionModal: React.FC = ({ const [selectedColor, setSelectedColor] = useState<'white' | 'black'>( initialBrowseCategory === 'endgames' ? 'white' : initialSelectedColor, ) - const [targetMoveNumber, setTargetMoveNumber] = useState(initialTargetMoves) + const [targetMoveNumber, setTargetMoveNumber] = useState( + initialTargetMoves, + ) const [searchTerm, setSearchTerm] = useState('') const [activeTab, setActiveTab] = useState('browse') const [initialTourCheck, setInitialTourCheck] = useState(false) @@ -2382,13 +2539,13 @@ export const OpeningSelectionModal: React.FC = ({ initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - className="relative flex h-[90vh] max-h-[900px] w-[98vw] max-w-[1400px] flex-col items-start justify-start overflow-hidden rounded-lg border border-glass-border bg-glass backdrop-blur-md md:h-[90vh]" + className="relative flex h-[90vh] max-h-[900px] w-[98vw] max-w-[1320px] flex-col items-start justify-start overflow-hidden rounded-xl border border-glass-border bg-[#231d1a] backdrop-blur-md md:h-[90vh]" >
-
- -

- Practice openings, endgames, or custom positions against Maia. - Select drills, choose your color or trait settings, and pick - your opponent strength. -

-
+
+

+ Maia Drill Studio +

+

+ Select drills, configure settings, practice against Maia. +

@@ -2436,7 +2578,7 @@ export const OpeningSelectionModal: React.FC = ({ /> {/* Main Content - Responsive Layout */} -
+
= ({ categoryLabel={categoryLabel} categoryLabelPlural={categoryLabelPlural} /> - = ({ selectedTraits={previewSelectedTraits} availableTraits={previewAvailableTraits} onToggleTrait={(trait) => { - const key = getTraitSelectionKey( - previewOpening.id, - previewVariation?.id ?? null, - ) const current = new Set(previewSelectedTraits) if (current.has(trait)) { current.delete(trait) @@ -2493,7 +2631,24 @@ export const OpeningSelectionModal: React.FC = ({ Array.from(current), ) }} + selections={selections} + removeSelection={removeSelection} + onSelectQueueItem={(selection) => { + setPreviewOpening(selection.opening) + setPreviewVariation(selection.variation ?? null) + setSelectedColor(selection.playerColor) + }} + handleStartDrilling={handleStartDrilling} + selectedMaiaVersion={selectedMaiaVersion} + setSelectedMaiaVersion={setSelectedMaiaVersion} + targetMoveNumber={targetMoveNumber} + setTargetMoveNumber={setTargetMoveNumber} + showTargetSlider={browseCategory === 'openings'} /> +
+ + {/* Mobile-only Selected Panel */} +
{ safeTrack('opening_quick_add_used', { opening_name: openingName, @@ -129,7 +129,7 @@ export const trackOpeningConfiguredAndAdded = ( openingName: string, playerColor: 'white' | 'black', maiaVersion: string, - targetMoves: number, + targetMoves: number | null, variationName?: string, ) => { safeTrack('opening_configured_and_added', { diff --git a/src/lib/posthog-browser-config.ts b/src/lib/posthog-browser-config.ts index 926110f9..7a5f2c2d 100644 --- a/src/lib/posthog-browser-config.ts +++ b/src/lib/posthog-browser-config.ts @@ -1,4 +1,8 @@ -import { COPY_AUTOCAPTURE_EVENT, type CaptureResult, type PostHogConfig } from 'posthog-js' +import { + COPY_AUTOCAPTURE_EVENT, + type CaptureResult, + type PostHogConfig, +} from 'posthog-js' const dropAutocaptureEvents = (event: CaptureResult | null) => { if (!event) { diff --git a/src/pages/puzzles.tsx b/src/pages/puzzles.tsx index 45a016d8..aeb85172 100644 --- a/src/pages/puzzles.tsx +++ b/src/pages/puzzles.tsx @@ -1180,7 +1180,6 @@ const Train: React.FC = ({
)}
-
{gamesController}