Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 87 additions & 28 deletions src/components/Analysis/BoardChrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface AnalysisMaiaWinrateBarProps {
displayText: string
labelPositionTop: MotionValue<string>
disabled?: boolean
variant?: StockfishEvalBarVariant
className?: string
bubbleMinWidthPx?: number
desktopSize?: 'compact' | 'expanded'
Expand Down Expand Up @@ -159,7 +160,7 @@ export const AnalysisStockfishEvalBar: React.FC<
? isExpandedDesktop
? 'h-6 min-w-[42px] rounded-full px-2 text-[11px]'
: 'h-5 min-w-[36px] rounded-full px-1.5 text-[10px]'
: 'h-4 min-w-[30px] rounded-full px-1 text-[8px]'
: 'h-[18px] w-[36px] rounded-full px-1 text-[9px]'

return (
<div
Expand Down Expand Up @@ -234,23 +235,35 @@ export const AnalysisMaiaWinrateBar: React.FC<AnalysisMaiaWinrateBarProps> = ({
displayText,
labelPositionTop,
disabled = false,
variant = 'desktop',
className,
bubbleMinWidthPx,
desktopSize = 'compact',
}) => {
const isDesktop = variant === 'desktop'
const isExpandedDesktop = desktopSize === 'expanded'
const widthClass = isDesktop
? isExpandedDesktop
? 'w-[18px]'
: 'w-[16px]'
: 'w-4'
const tickTextClass = isDesktop
? isExpandedDesktop
? 'text-[9px]'
: 'text-[8px]'
: 'text-[8px]'
const bubbleClass = isDesktop
? isExpandedDesktop
? 'h-6 min-w-[42px] px-2 text-[11px]'
: 'h-5 min-w-[36px] px-1.5 text-[10px]'
: 'h-[18px] w-[36px] rounded-full px-1 text-[9px]'
return (
<div
className={[
`relative h-full ${isExpandedDesktop ? 'w-[18px]' : 'w-[16px]'}`,
className,
]
className={[`relative h-full ${widthClass}`, className]
.filter(Boolean)
.join(' ')}
>
<div
className={`relative h-full ${isExpandedDesktop ? 'w-[18px]' : 'w-[16px]'}`}
>
<div className={`relative h-full ${widthClass}`}>
<div
className="absolute inset-0 overflow-hidden rounded-[5px] border border-glass-border bg-glass-strong shadow-[0_0_0_1px_rgb(var(--color-backdrop)/0.35)]"
style={{
Expand Down Expand Up @@ -282,12 +295,12 @@ export const AnalysisMaiaWinrateBar: React.FC<AnalysisMaiaWinrateBarProps> = ({
),
)}
<div
className={`absolute left-1/2 top-0 -translate-x-1/2 font-bold leading-none text-black/90 [text-shadow:0_1px_1px_rgb(255_255_255_/_0.5)] ${isExpandedDesktop ? 'text-[9px]' : 'text-[8px]'}`}
className={`absolute left-1/2 top-0 -translate-x-1/2 font-bold leading-none text-black/90 [text-shadow:0_1px_1px_rgb(255_255_255_/_0.5)] ${tickTextClass}`}
>
100
</div>
<div
className={`absolute bottom-0 left-1/2 -translate-x-1/2 font-bold leading-none text-white/95 [text-shadow:0_1px_1px_rgb(0_0_0_/_0.55)] ${isExpandedDesktop ? 'text-[9px]' : 'text-[8px]'}`}
className={`absolute bottom-0 left-1/2 -translate-x-1/2 font-bold leading-none text-white/95 [text-shadow:0_1px_1px_rgb(0_0_0_/_0.55)] ${tickTextClass}`}
>
0
</div>
Expand All @@ -296,11 +309,7 @@ export const AnalysisMaiaWinrateBar: React.FC<AnalysisMaiaWinrateBarProps> = ({
) : null}
</div>
<motion.div
className={`absolute left-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-black/45 bg-white font-bold leading-none text-black/85 ${
isExpandedDesktop
? 'h-6 min-w-[42px] px-2 text-[11px]'
: 'h-5 min-w-[36px] px-1.5 text-[10px]'
}`}
className={`absolute left-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-black/45 bg-white font-bold leading-none text-black/85 ${bubbleClass}`}
style={{
top: labelPositionTop,
boxShadow: '0 0 0 2px rgb(255 255 255 / 0.32)',
Expand All @@ -318,6 +327,7 @@ export const AnalysisMaiaWinrateBar: React.FC<AnalysisMaiaWinrateBarProps> = ({
type SegmentConfig = {
key: 'blunder' | 'ok' | 'good'
label: string
mobileLabel?: string
probability: number
topMoves: { move: string; probability: number; label: string }[]
badge: string
Expand Down Expand Up @@ -376,6 +386,7 @@ export const AnalysisCompactBlunderMeter: React.FC<
{
key: 'blunder',
label: 'Blunders',
mobileLabel: 'Blunders',
probability: data.blunderMoves.probability,
topMoves: getTopCategoryMoves(data.blunderMoves.moves),
badge: '??',
Expand All @@ -387,6 +398,7 @@ export const AnalysisCompactBlunderMeter: React.FC<
{
key: 'ok',
label: 'Mistakes',
mobileLabel: 'Mistakes',
probability: data.okMoves.probability,
topMoves: getTopCategoryMoves(data.okMoves.moves),
badge: '?',
Expand All @@ -398,6 +410,7 @@ export const AnalysisCompactBlunderMeter: React.FC<
{
key: 'good',
label: 'Best Moves',
mobileLabel: 'Best',
probability: data.goodMoves.probability,
topMoves: getTopCategoryMoves(data.goodMoves.moves),
badge: '✓',
Expand All @@ -415,13 +428,13 @@ export const AnalysisCompactBlunderMeter: React.FC<
? 'h-5 min-w-5 text-[10px]'
: 'h-3.5 min-w-3.5 text-[8px]'
const percentTextClass = isDesktop ? 'text-[11px]' : 'text-[9px]'
const metaTextClass = isDesktop ? 'text-xs py-1.5' : 'text-[9px] py-0.5'
const metaTextClass = isDesktop ? 'text-xs py-1.5' : 'text-[10px] py-1.5'
const playedMoveOutlineOuterInsetClass = isDesktop
? '-inset-x-[11px] -inset-y-[5px]'
: '-inset-x-[7px] -inset-y-[4px]'
: '-inset-x-[6px] -inset-y-[2px]'
const playedMoveOutlineInnerInsetClass = isDesktop
? '-inset-x-[8px] -inset-y-[3px]'
: '-inset-x-[5px] -inset-y-[3px]'
: '-inset-x-[4px] -inset-y-[1px]'
const renderTopMoveButton = (
segmentKey: string,
topMove: { move: string; probability: number; label: string },
Expand Down Expand Up @@ -471,6 +484,26 @@ export const AnalysisCompactBlunderMeter: React.FC<
</button>
)
}
const renderMobileMoveSequence = (
segmentKey: string,
moves: { move: string; probability: number; label: string }[],
startIndex = 0,
) =>
moves.map((topMove, index) => {
const absoluteIndex = startIndex + index
return (
<span
key={`${segmentKey}-${topMove.move}-${absoluteIndex}`}
className="inline-flex min-w-0 items-baseline"
>
{absoluteIndex > 0 ? <span className="mr-1"> </span> : null}
{renderTopMoveButton(segmentKey, topMove)}
{absoluteIndex < startIndex + moves.length - 1 ? (
<span className="mr-1">,</span>
) : null}
</span>
)
})

return (
<div
Expand Down Expand Up @@ -546,22 +579,48 @@ export const AnalysisCompactBlunderMeter: React.FC<
</div>
) : (
<div
className={`flex items-center gap-2.5 whitespace-nowrap font-semibold leading-tight tracking-[0.01em] ${metaTextClass}`}
className={`flex min-h-[38px] items-start gap-2.5 pt-1 font-semibold leading-snug tracking-[0.01em] ${metaTextClass}`}
>
{segments.map((segment) => (
<div
key={`maia-top-moves-${segment.key}`}
className={`min-w-0 flex-1 truncate ${segment.moveClass}`}
className={`flex min-w-0 flex-1 items-start ${segment.moveClass} ${
segment.key === 'good' ? '-ml-5' : ''
}`}
>
{segment.label}:{' '}
{segment.topMoves.length
? segment.topMoves.map((topMove, index) => (
<span key={`${segment.key}-${topMove.move}`}>
{index > 0 ? <span>, </span> : null}
{renderTopMoveButton(segment.key, topMove)}
<span className="mr-2 shrink-0 whitespace-nowrap">
{segment.mobileLabel ?? segment.label}:
</span>
{segment.topMoves.length ? (
segment.topMoves.length >= 3 ? (
<span className="flex min-w-0 flex-col gap-0.5 leading-tight">
<span className="-mx-[6px] inline-flex min-w-0 items-baseline whitespace-nowrap px-[6px]">
{renderMobileMoveSequence(
segment.key,
segment.topMoves.slice(0, 2),
0,
)}
</span>
<span className="-mx-[6px] inline-flex min-w-0 items-baseline whitespace-nowrap px-[6px]">
{renderMobileMoveSequence(
segment.key,
segment.topMoves.slice(2),
2,
)}
</span>
))
: '-'}
</span>
) : (
<span className="-mx-[6px] inline-flex min-w-0 items-baseline whitespace-nowrap px-[6px]">
{renderMobileMoveSequence(
segment.key,
segment.topMoves,
0,
)}
</span>
)
) : (
'-'
)}
</div>
))}
</div>
Expand Down
130 changes: 101 additions & 29 deletions src/components/Analysis/Highlight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ export const Highlight: React.FC<Props> = ({
// Track whether description exists (not its content)
const hasDescriptionRef = useRef(boardDescription?.segments?.length > 0)
const [animationKey, setAnimationKey] = useState(0)
const maiaHeaderSelectRef = useRef<HTMLSelectElement | null>(null)

// Calculate if we're in the first 10 ply
const isInFirst10Ply = currentNode
Expand Down Expand Up @@ -353,6 +354,27 @@ export const Highlight: React.FC<Props> = ({
}
}, [boardDescription?.segments?.length])

const useCompactMobileColumnTitles = isMobile && !simplified
const mobileMaiaColumnTitle = `Maia ${currentMaiaModel.slice(-4)}: Human Moves`
const mobileStockfishColumnTitle = 'SF 17: Engine Moves'
const openMaiaHeaderPicker = () => {
const select = maiaHeaderSelectRef.current as
| (HTMLSelectElement & { showPicker?: () => void })
| null
if (!select) return

select.focus()
if (select.showPicker) {
try {
select.showPicker()
return
} catch {
// Fall back to click if showPicker is unavailable or rejected.
}
}
select.click()
}

return (
<div
id="analysis-highlight"
Expand All @@ -367,28 +389,72 @@ export const Highlight: React.FC<Props> = ({
<div className="relative flex w-full flex-col border-b border-white/5">
{isHomePage ? (
<div className="py-2 text-center text-sm font-semibold text-human-1 md:text-xxs lg:text-xs">
Maia {currentMaiaModel.slice(-4)}
{useCompactMobileColumnTitles
? mobileMaiaColumnTitle
: `Maia ${currentMaiaModel.slice(-4)}`}
</div>
) : (
<>
<select
value={currentMaiaModel}
onChange={(e) => setCurrentMaiaModel(e.target.value)}
className="cursor-pointer appearance-none bg-transparent py-2 text-center text-sm font-semibold text-human-1 outline-none transition-colors duration-200 hover:text-human-1/80 md:text-xxs lg:text-xs"
>
{MAIA_MODELS.map((model) => (
<option
value={model}
key={model}
className="bg-transparent text-human-1"
{useCompactMobileColumnTitles ? (
<div className="flex items-center justify-center py-2 pr-4 text-sm font-semibold text-human-1">
<select
ref={maiaHeaderSelectRef}
value={currentMaiaModel}
onChange={(e) => setCurrentMaiaModel(e.target.value)}
className="cursor-pointer appearance-none bg-transparent text-center outline-none transition-colors duration-200 hover:text-human-1/80"
>
Maia {model.slice(-4)}
</option>
))}
</select>
<span className="material-symbols-outlined pointer-events-none absolute right-1 top-1/2 -translate-y-1/2 text-sm text-human-1/60">
keyboard_arrow_down
</span>
{MAIA_MODELS.map((model) => (
<option
value={model}
key={model}
className="bg-transparent text-human-1"
>
{`Maia ${model.slice(-4)}`}
</option>
))}
</select>
<span className="ml-0.5 whitespace-nowrap">
: Human Moves
</span>
</div>
) : (
<select
ref={maiaHeaderSelectRef}
value={currentMaiaModel}
onChange={(e) => setCurrentMaiaModel(e.target.value)}
className="cursor-pointer appearance-none bg-transparent py-2 text-center text-sm font-semibold text-human-1 outline-none transition-colors duration-200 hover:text-human-1/80 md:text-xxs lg:text-xs"
>
{MAIA_MODELS.map((model) => (
<option
value={model}
key={model}
className="bg-transparent text-human-1"
>
{`Maia ${model.slice(-4)}`}
</option>
))}
</select>
)}
<button
type="button"
className="material-symbols-outlined absolute right-0.5 top-1/2 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center text-base leading-none text-human-1/65"
onMouseDown={(e) => {
e.preventDefault()
openMaiaHeaderPicker()
}}
onClick={(e) => {
e.preventDefault()
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
openMaiaHeaderPicker()
}
}}
aria-label="Change Maia model"
>
expand_more
</button>
</>
)}
</div>
Expand All @@ -407,11 +473,13 @@ export const Highlight: React.FC<Props> = ({
<div
className={`flex w-full flex-col items-start justify-center md:items-center ${simplified ? 'p-3' : 'px-2 py-1.5 xl:py-2'}`}
>
<p
className={`mb-1 whitespace-nowrap text-sm font-semibold text-human-2 ${simplified ? 'text-sm' : 'md:text-xxs lg:text-xs'}`}
>
Human Moves
</p>
{!useCompactMobileColumnTitles && (
<p
className={`mb-1 whitespace-nowrap text-sm font-semibold text-human-2 ${simplified ? 'text-sm' : 'md:text-xxs lg:text-xs'}`}
>
Human Moves
</p>
)}
<div className="flex w-full cursor-pointer items-center justify-between">
<p
className={`text-left font-mono ${simplified ? 'text-xs' : 'text-sm md:text-xxs'} text-secondary/50`}
Expand Down Expand Up @@ -454,7 +522,9 @@ export const Highlight: React.FC<Props> = ({
<div className="flex flex-col items-center justify-start gap-0.5 xl:gap-1">
<div className="flex w-full flex-col border-b border-white/5 py-2">
<p className="whitespace-nowrap text-center text-sm font-semibold text-engine-1 md:text-xxs lg:text-xs">
Stockfish 17
{useCompactMobileColumnTitles
? mobileStockfishColumnTitle
: 'Stockfish 17'}
</p>
</div>

Expand All @@ -477,11 +547,13 @@ export const Highlight: React.FC<Props> = ({
<div
className={`flex w-full flex-col items-start justify-center ${simplified ? 'p-3' : 'px-2 py-1.5 xl:py-2'} md:items-center`}
>
<p
className={`mb-1 whitespace-nowrap text-sm font-semibold text-engine-2 ${simplified ? 'text-sm' : 'md:text-xxs lg:text-xs'}`}
>
Engine Moves
</p>
{!useCompactMobileColumnTitles && (
<p
className={`mb-1 whitespace-nowrap text-sm font-semibold text-engine-2 ${simplified ? 'text-sm' : 'md:text-xxs lg:text-xs'}`}
>
Engine Moves
</p>
)}
<div className="flex w-full cursor-pointer items-center justify-between">
<p
className={`text-left font-mono text-secondary/50 ${simplified ? 'text-xs' : 'text-sm md:text-xxs'}`}
Expand Down
Loading
Loading