From 5aead575194f1761408925e70c9fa8f44318b71f Mon Sep 17 00:00:00 2001 From: Ashton Anderson Date: Sat, 28 Mar 2026 13:39:28 -0400 Subject: [PATCH] feat: align broadcast analysis with analysis layout --- src/components/Analysis/BroadcastAnalysis.tsx | 1128 +++++++++++++---- .../broadcast/[broadcastId]/[roundId].tsx | 9 + 2 files changed, 913 insertions(+), 224 deletions(-) diff --git a/src/components/Analysis/BroadcastAnalysis.tsx b/src/components/Analysis/BroadcastAnalysis.tsx index 8b1c63a1..53f5de92 100644 --- a/src/components/Analysis/BroadcastAnalysis.tsx +++ b/src/components/Analysis/BroadcastAnalysis.tsx @@ -2,28 +2,51 @@ import React, { useMemo, useState, useEffect, + useLayoutEffect, useCallback, useContext, + useRef, } from 'react' -import { motion } from 'framer-motion' +import { motion, useSpring, useTransform } from 'framer-motion' import type { Key } from 'chessground/types' import { Chess, PieceSymbol } from 'chess.ts' -import type { DrawShape } from 'chessground/draw' +import type { DrawBrushes, DrawShape } from 'chessground/draw' -import { TABLET_BREAKPOINT_PX, WindowSizeContext } from 'src/contexts' +import { WindowSizeContext } from 'src/contexts' import { MAIA_MODELS } from 'src/constants/common' +import { cpToWinrate } from 'src/lib' +import { + AnalysisArrowLegend, + AnalysisCompactBlunderMeter, + AnalysisMaiaWinrateBar, + AnalysisStockfishEvalBar, +} from 'src/components/Analysis/BoardChrome' import { GameInfo } from 'src/components/Common/GameInfo' import { MaterialBalance } from 'src/components/Common/MaterialBalance' import { GameBoard } from 'src/components/Board/GameBoard' -import { PlayerInfo } from 'src/components/Common/PlayerInfo' import { MovesContainer } from 'src/components/Board/MovesContainer' import { LiveGame, GameNode, BroadcastStreamController } from 'src/types' import { BoardController } from 'src/components/Board/BoardController' import { PromotionOverlay } from 'src/components/Board/PromotionOverlay' -import { AnalysisSidebar } from 'src/components/Analysis' +import { + AnalysisSidebar, + MovesByRating, + SimplifiedAnalysisOverview, +} from 'src/components/Analysis' import { ConfigurableScreens } from 'src/components/Analysis/ConfigurableScreens' import { BroadcastGameList } from 'src/components/Analysis/BroadcastGameList' import { useAnalysisController } from 'src/hooks/useAnalysisController' +import type { MaiaEvaluation, StockfishEvaluation } from 'src/types' + +const EVAL_BAR_RANGE = 4 +const DEFAULT_STOCKFISH_EVAL_BAR = { + hasEval: false, + pawns: 0, + displayPawns: 0, + label: '--', +} +const useIsomorphicLayoutEffect = + typeof window !== 'undefined' ? useLayoutEffect : useEffect interface Props { game: LiveGame @@ -38,17 +61,80 @@ export const BroadcastAnalysis: React.FC = ({ broadcastController, analysisController, }) => { - const { width } = useContext(WindowSizeContext) - const isMobile = useMemo( - () => width > 0 && width <= TABLET_BREAKPOINT_PX, - [width], - ) + const { width, height } = useContext(WindowSizeContext) + const isMobile = useMemo(() => width > 0 && width <= 670, [width]) + const useMobileStyleAnalysisLayout = useMemo(() => { + if (isMobile) return true + return width > 670 && width <= 1120 + }, [isMobile, width]) + const isTabletUsingMobileStyleLayout = useMemo( + () => useMobileStyleAnalysisLayout && !isMobile, + [isMobile, useMobileStyleAnalysisLayout], + ) + const desktopBoardHeaderStripRef = useRef(null) + const desktopBlunderMeterSectionRef = useRef(null) + const [desktopMiddleMeasuredHeights, setDesktopMiddleMeasuredHeights] = + useState({ + boardHeaderStripPx: 28, + blunderMeterSectionPx: 126, + }) + const [showGameListMobile, setShowGameListMobile] = useState(false) const [hoverArrow, setHoverArrow] = useState(null) const [currentSquare, setCurrentSquare] = useState(null) const [promotionFromTo, setPromotionFromTo] = useState< [string, string] | null >(null) + + useIsomorphicLayoutEffect(() => { + if (useMobileStyleAnalysisLayout) return + + const headerEl = desktopBoardHeaderStripRef.current + const blunderEl = desktopBlunderMeterSectionRef.current + if (!headerEl && !blunderEl) return + + const updateHeights = () => { + const next = { + boardHeaderStripPx: + headerEl?.getBoundingClientRect().height ?? + desktopMiddleMeasuredHeights.boardHeaderStripPx, + blunderMeterSectionPx: + blunderEl?.getBoundingClientRect().height ?? + desktopMiddleMeasuredHeights.blunderMeterSectionPx, + } + + setDesktopMiddleMeasuredHeights((prev) => { + if ( + Math.abs(prev.boardHeaderStripPx - next.boardHeaderStripPx) < 0.5 && + Math.abs(prev.blunderMeterSectionPx - next.blunderMeterSectionPx) < + 0.5 + ) { + return prev + } + return next + }) + } + + updateHeights() + }, [useMobileStyleAnalysisLayout, width]) + + const emptyBlunderMeterData = useMemo( + () => ({ + goodMoves: { moves: [], probability: 0 }, + okMoves: { moves: [], probability: 0 }, + blunderMoves: { moves: [], probability: 0 }, + }), + [], + ) + const emptyRecommendations = useMemo( + () => ({ + maia: undefined, + stockfish: undefined, + }), + [], + ) + const mockHover = useCallback(() => void 0, []) + const mockMakeMove = useCallback(() => void 0, []) const destinationBadges = useMemo(() => { if ( !analysisController.showTopMoveBadges || @@ -126,12 +212,9 @@ export const BroadcastAnalysis: React.FC = ({ (playedMove: [string, string] | null) => { if (!playedMove) return - const availableMoves: { from: string; to: string }[] = [] - for (const [from, tos] of analysisController.availableMoves.entries()) { - for (const to of tos as string[]) { - availableMoves.push({ from, to }) - } - } + const availableMoves = Array.from( + analysisController.availableMoves.entries(), + ).flatMap(([from, tos]) => tos.map((to) => ({ from, to }))) const matching = availableMoves.filter((m) => { return m.from === playedMove[0] && m.to === playedMove[1] @@ -172,6 +255,397 @@ export const BroadcastAnalysis: React.FC = ({ return chess.turn() === 'w' ? 'white' : 'black' }, [analysisController.currentNode]) + const playedMoveShapes = useMemo(() => { + const playedMove = analysisController.currentNode?.mainChild?.move + if (!playedMove || playedMove.length < 4) { + return [] + } + + return [ + { + brush: 'playedMoveOutline', + orig: playedMove.slice(0, 2) as Key, + dest: playedMove.slice(2, 4) as Key, + }, + { + brush: 'playedMoveCore', + orig: playedMove.slice(0, 2) as Key, + dest: playedMove.slice(2, 4) as Key, + }, + ] as DrawShape[] + }, [analysisController.currentNode?.mainChild?.move]) + + const analysisArrowBrushes = useMemo( + () => ({ + playedMoveOutline: { + key: 'playedMoveOutline', + color: '#4A8FB3', + opacity: 0.95, + lineWidth: 11, + }, + playedMoveCore: { + key: 'playedMoveCore', + color: '#FFFFFF', + opacity: 0.98, + lineWidth: 8, + }, + }), + [], + ) + + const staggerOverlappingArrows = useCallback((shapes: DrawShape[]) => { + const overlapGroups = new Map() + + shapes.forEach((shape, index) => { + const arrow = shape as DrawShape & { orig?: string; dest?: string } + if (!arrow.orig || !arrow.dest) return + + const key = `${arrow.orig}-${arrow.dest}` + const indices = overlapGroups.get(key) ?? [] + indices.push(index) + overlapGroups.set(key, indices) + }) + + const layeredShapes = shapes.map((shape) => { + const arrow = shape as DrawShape & { + modifiers?: { [key: string]: unknown; lineWidth?: number } + } + + return { + ...arrow, + modifiers: arrow.modifiers ? { ...arrow.modifiers } : undefined, + } as DrawShape + }) + + overlapGroups.forEach((indices) => { + if (indices.length < 2) return + + indices.forEach((shapeIndex, order) => { + const shape = layeredShapes[shapeIndex] as DrawShape & { + modifiers?: { [key: string]: unknown; lineWidth?: number } + } + + const baseWidth = + typeof shape.modifiers?.lineWidth === 'number' + ? shape.modifiers.lineWidth + : 8 + + const expandedWidth = baseWidth + (indices.length - order - 1) * 1.25 + const adjustedWidth = Math.min(13.5, Math.max(2.5, expandedWidth)) + + shape.modifiers = { + ...(shape.modifiers || {}), + lineWidth: adjustedWidth, + } + }) + }) + + return layeredShapes + }, []) + + const rawCompactBlunderMeterData = analysisController.blunderMeter + const [ + displayedCompactBlunderMeterData, + setDisplayedCompactBlunderMeterData, + ] = useState(rawCompactBlunderMeterData) + + const hasUsableBlunderMeterData = useMemo(() => { + const totalProbability = + rawCompactBlunderMeterData.goodMoves.probability + + rawCompactBlunderMeterData.okMoves.probability + + rawCompactBlunderMeterData.blunderMoves.probability + const hasMoves = + rawCompactBlunderMeterData.goodMoves.moves.length > 0 || + rawCompactBlunderMeterData.okMoves.moves.length > 0 || + rawCompactBlunderMeterData.blunderMoves.moves.length > 0 + + return totalProbability > 0 || hasMoves + }, [rawCompactBlunderMeterData]) + + useIsomorphicLayoutEffect(() => { + if (hasUsableBlunderMeterData) { + setDisplayedCompactBlunderMeterData(rawCompactBlunderMeterData) + } + }, [hasUsableBlunderMeterData, rawCompactBlunderMeterData]) + + const compactBlunderMeterData = displayedCompactBlunderMeterData + + const rawStockfishEvalBar = useMemo(() => { + const stockfish = analysisController.moveEvaluation?.stockfish + const sideToMove = analysisController.currentNode?.turn || 'w' + + if (!stockfish) { + return { + ...DEFAULT_STOCKFISH_EVAL_BAR, + depth: 0, + } + } + + const mateIn = stockfish.mate_vec?.[stockfish.model_move] + if (mateIn !== undefined) { + const matingColor = + mateIn > 0 ? sideToMove : sideToMove === 'w' ? 'b' : 'w' + const whitePerspectiveSign = matingColor === 'w' ? 1 : -1 + return { + hasEval: true, + pawns: whitePerspectiveSign * EVAL_BAR_RANGE, + displayPawns: whitePerspectiveSign * EVAL_BAR_RANGE, + label: `M${Math.abs(mateIn)}`, + depth: stockfish.depth ?? 0, + } + } + + const cp = + stockfish.model_optimal_cp ?? Object.values(stockfish.cp_vec)[0] ?? 0 + const rawPawns = cp / 100 + const clampedPawns = Math.max( + -EVAL_BAR_RANGE, + Math.min(EVAL_BAR_RANGE, rawPawns), + ) + + return { + hasEval: true, + pawns: clampedPawns, + displayPawns: rawPawns, + label: `${rawPawns > 0 ? '+' : ''}${rawPawns.toFixed(2)}`, + depth: stockfish.depth ?? 0, + } + }, [analysisController.currentNode?.turn, analysisController.moveEvaluation]) + + const [displayedStockfishEvalBar, setDisplayedStockfishEvalBar] = useState( + DEFAULT_STOCKFISH_EVAL_BAR, + ) + + useIsomorphicLayoutEffect(() => { + if (!rawStockfishEvalBar.hasEval || rawStockfishEvalBar.depth <= 10) { + return + } + + setDisplayedStockfishEvalBar({ + hasEval: true, + pawns: rawStockfishEvalBar.pawns, + displayPawns: rawStockfishEvalBar.displayPawns, + label: rawStockfishEvalBar.label, + }) + }, [rawStockfishEvalBar]) + + const evalPositionPercent = useMemo(() => { + const normalized = + (displayedStockfishEvalBar.pawns + EVAL_BAR_RANGE) / (EVAL_BAR_RANGE * 2) + return Math.max(0, Math.min(1, normalized)) * 100 + }, [displayedStockfishEvalBar.pawns]) + + const displayedStockfishEvalText = useMemo(() => { + if (!displayedStockfishEvalBar.hasEval) { + return '--' + } + + if (displayedStockfishEvalBar.label.startsWith('M')) { + return displayedStockfishEvalBar.label + } + + const roundedPawns = + Math.round(displayedStockfishEvalBar.displayPawns * 10) / 10 + const safePawns = Math.abs(roundedPawns) < 0.05 ? 0 : roundedPawns + return `${safePawns > 0 ? '+' : ''}${safePawns.toFixed(1)}` + }, [ + displayedStockfishEvalBar.displayPawns, + displayedStockfishEvalBar.hasEval, + displayedStockfishEvalBar.label, + ]) + + const currentTurnForBars: 'w' | 'b' = + analysisController.currentNode?.turn || 'w' + + const isCurrentPositionCheckmateForBars = useMemo(() => { + if (!analysisController.currentNode) return false + try { + const chess = new Chess(analysisController.currentNode.fen) + return chess.inCheckmate() + } catch { + return false + } + }, [analysisController.currentNode]) + + const isInFirst10PlyForBars = useMemo(() => { + if (!analysisController.currentNode) return false + const moveNumber = analysisController.currentNode.moveNumber + const turn = analysisController.currentNode.turn + const plyFromStart = (moveNumber - 1) * 2 + (turn === 'b' ? 1 : 0) + return plyFromStart < 10 + }, [analysisController.currentNode]) + + const rawMaiaWhiteWinBar = useMemo(() => { + const stockfishEval = analysisController.moveEvaluation?.stockfish + + if (isCurrentPositionCheckmateForBars) { + const percent = currentTurnForBars === 'w' ? 0 : 100 + return { hasValue: true, percent, label: `${percent.toFixed(1)}%` } + } + + if (stockfishEval?.is_checkmate) { + const percent = currentTurnForBars === 'w' ? 0 : 100 + return { hasValue: true, percent, label: `${percent.toFixed(1)}%` } + } + + if ( + stockfishEval?.model_move && + stockfishEval.mate_vec && + stockfishEval.mate_vec[stockfishEval.model_move] !== undefined + ) { + const mateValue = stockfishEval.mate_vec[stockfishEval.model_move] + const deliveringColor = + mateValue > 0 + ? currentTurnForBars + : currentTurnForBars === 'w' + ? 'b' + : 'w' + const percent = deliveringColor === 'w' ? 100 : 0 + return { hasValue: true, percent, label: `${percent.toFixed(1)}%` } + } + + if ( + isInFirst10PlyForBars && + stockfishEval?.model_optimal_cp !== undefined + ) { + const percent = Math.max( + 0, + Math.min(100, cpToWinrate(stockfishEval.model_optimal_cp) * 100), + ) + return { + hasValue: true, + percent, + label: `${(Math.round(percent * 10) / 10).toFixed(1)}%`, + } + } + + if (analysisController.moveEvaluation?.maia) { + const percent = Math.max( + 0, + Math.min(100, analysisController.moveEvaluation.maia.value * 100), + ) + return { + hasValue: true, + percent, + label: `${(Math.round(percent * 10) / 10).toFixed(1)}%`, + } + } + + return { hasValue: false, percent: 50, label: '--' } + }, [ + analysisController.moveEvaluation, + currentTurnForBars, + isCurrentPositionCheckmateForBars, + isInFirst10PlyForBars, + ]) + + const [displayedMaiaWhiteWinBar, setDisplayedMaiaWhiteWinBar] = + useState(rawMaiaWhiteWinBar) + + useIsomorphicLayoutEffect(() => { + if (!rawMaiaWhiteWinBar.hasValue) { + return + } + + setDisplayedMaiaWhiteWinBar(rawMaiaWhiteWinBar) + }, [rawMaiaWhiteWinBar]) + + const maiaWhiteWinPositionPercent = useMemo( + () => Math.max(0, Math.min(100, displayedMaiaWhiteWinBar.percent)), + [displayedMaiaWhiteWinBar.percent], + ) + + const smoothedMaiaWhiteWinPosition = useSpring(50, { + stiffness: 520, + damping: 42, + mass: 0.25, + }) + const smoothedMaiaWhiteWinVerticalPositionLabel = useTransform( + smoothedMaiaWhiteWinPosition, + (value) => `${100 - value}%`, + ) + + useIsomorphicLayoutEffect(() => { + smoothedMaiaWhiteWinPosition.set(maiaWhiteWinPositionPercent) + }, [maiaWhiteWinPositionPercent, smoothedMaiaWhiteWinPosition]) + + const smoothedEvalPosition = useSpring(50, { + stiffness: 520, + damping: 42, + mass: 0.25, + }) + const smoothedEvalVerticalPositionLabel = useTransform( + smoothedEvalPosition, + (value) => `${100 - value}%`, + ) + + useIsomorphicLayoutEffect(() => { + smoothedEvalPosition.set(evalPositionPercent) + }, [evalPositionPercent, smoothedEvalPosition]) + + const desktopMaiaBubbleReservePx = useMemo( + () => (width >= 1360 ? 62 : 52), + [width], + ) + const desktopEvalBubbleReservePx = useMemo( + () => (width >= 1360 ? 56 : 48), + [width], + ) + const desktopEvalGutterWidthPx = useMemo( + () => desktopEvalBubbleReservePx + 6, + [desktopEvalBubbleReservePx], + ) + const desktopMaiaGutterWidthPx = useMemo( + () => desktopMaiaBubbleReservePx + 6, + [desktopMaiaBubbleReservePx], + ) + const desktopColumnTargetHeightCss = '85vh' + const desktopBoardBaselineSizeCss = 'min(42vw, 72vh)' + const desktopBoardWidthCapVw = useMemo(() => { + if (width >= 1536) return 48 + if (width >= 1280) return 46 + return 42 + }, [width]) + const desktopBoardHeightCapPx = useMemo(() => { + if (height <= 0) return null + + const targetColumnHeightPx = height * 0.85 + const extraGapPx = 4 + const measuredNonBoardHeightPx = + desktopMiddleMeasuredHeights.boardHeaderStripPx + + desktopMiddleMeasuredHeights.blunderMeterSectionPx + + extraGapPx + + return Math.max( + 320, + Math.floor(targetColumnHeightPx - measuredNonBoardHeightPx), + ) + }, [desktopMiddleMeasuredHeights, height]) + const desktopBoardSizeCss = useMemo(() => { + const heightCapCss = + desktopBoardHeightCapPx !== null ? `${desktopBoardHeightCapPx}px` : '72vh' + const expandedTargetCss = `min(${desktopBoardWidthCapVw}vw, ${heightCapCss})` + + return `max(${desktopBoardBaselineSizeCss}, ${expandedTargetCss})` + }, [ + desktopBoardBaselineSizeCss, + desktopBoardHeightCapPx, + desktopBoardWidthCapVw, + ]) + const desktopBoardMinSizeCss = useMemo( + () => `calc(max(24rem, ${desktopBoardSizeCss}))`, + [desktopBoardSizeCss], + ) + const desktopConfigPanelHeightCss = '9.5rem' + const desktopSidebarContentHeightCss = `calc(${desktopColumnTargetHeightCss} - ${desktopConfigPanelHeightCss} - 0.75rem)` + const desktopBarChromeSize: 'compact' | 'expanded' = + width >= 1360 ? 'expanded' : 'compact' + const desktopCompactBlunderMaiaHeaderLabel = useMemo(() => { + const ratingLevel = + analysisController.currentMaiaModel?.replace('maia_kdd_', '') || '----' + return `Maia %\n@ ${ratingLevel}` + }, [analysisController.currentMaiaModel]) + const NestedGameInfo = () => (
@@ -188,14 +662,12 @@ export const BroadcastAnalysis: React.FC = ({ {player.rating ? <>({player.rating}) : null}
- {game.termination?.winner === (index == 0 ? 'white' : 'black') ? ( + {game.termination?.winner === (index === 0 ? 'white' : 'black') ? (

1

) : game.termination?.winner !== 'none' ? (

0

- ) : game.termination === undefined ? ( - <> - ) : ( -

½

+ ) : game.termination === undefined ? null : ( +

1/2

)}
))} @@ -210,23 +682,19 @@ export const BroadcastAnalysis: React.FC = ({
-
+ +
+ {game.whitePlayer.name} {game.whitePlayer.rating && ( ({game.whitePlayer.rating}) )} -
{broadcastController.broadcastState.isLive && !game.termination ? ( LIVE ) : game.termination?.winner === 'none' ? ( - ½-½ + 1/2-1/2 ) : ( @@ -240,17 +708,13 @@ export const BroadcastAnalysis: React.FC = ({ )}
-
+ +
+ {game.blackPlayer.name} {game.blackPlayer.rating && ( ({game.blackPlayer.rating}) )} -
@@ -294,21 +758,21 @@ export const BroadcastAnalysis: React.FC = ({ const desktopLayout = ( -
+
-
+
= ({ > -
-
-
- -
+
+
+
+
-
-
+
= ({ -
- { - const clock = - analysisController.orientation === 'white' - ? broadcastController.currentGame?.blackClock - : broadcastController.currentGame?.whiteClock - console.log('Top PlayerInfo clock data:', { - orientation: analysisController.orientation, - currentGame: - broadcastController.currentGame?.white + - ' vs ' + - broadcastController.currentGame?.black, - whiteClock: broadcastController.currentGame?.whiteClock, - blackClock: broadcastController.currentGame?.blackClock, - selectedClock: clock, - }) - return clock - })()} - /> -
- { - const baseShapes = [...analysisController.arrows] - if (hoverArrow) { - baseShapes.push(hoverArrow) - } - return baseShapes - })()} - currentNode={analysisController.currentNode as GameNode} - orientation={analysisController.orientation} - onPlayerMakeMove={onPlayerMakeMove} - goToNode={analysisController.goToNode} - gameTree={game.tree} - destinationBadges={destinationBadges} +
+
+
+
+
+ + White Win % + +
+
+
+ +
+ +
+ +
+
+
+ + SF Eval + +
+
+
+
+ +
+
+ { + const baseShapes = [...playedMoveShapes] + baseShapes.push(...analysisController.arrows) + + if (hoverArrow) { + baseShapes.push(hoverArrow) + } + + return staggerOverlappingArrows(baseShapes) + })()} + currentNode={analysisController.currentNode as GameNode} + orientation={analysisController.orientation} + onPlayerMakeMove={onPlayerMakeMove} + goToNode={analysisController.goToNode} + gameTree={game.tree} + destinationBadges={destinationBadges} + brushes={analysisArrowBrushes as unknown as DrawBrushes} + /> + {promotionFromTo ? ( + + ) : null} +
+
+ +
+
+
+
+
+ - {promotionFromTo ? ( - - ) : null}
-
- = ({ setHoverArrow={setHoverArrow} analysisEnabled={true} handleToggleAnalysis={() => { - // Analysis toggle not needed for broadcast - always enabled + // Broadcast analysis is always enabled. + }} + hideDetailedBlunderMeter={true} + desktopContentHeightCss={desktopSidebarContentHeightCss} + containerStyle={{ + width: 'clamp(23rem, 27vw, 26rem)', + minWidth: '23rem', + flexBasis: 'clamp(23rem, 27vw, 26rem)', }} + footerContent={ +
+ +
+ } itemVariants={itemVariants} />
@@ -483,90 +1002,251 @@ export const BroadcastAnalysis: React.FC = ({ style={{ willChange: 'transform, opacity' }} >
- - - - -
- { - const baseShapes = [...analysisController.arrows] - if (hoverArrow) { - baseShapes.push(hoverArrow) - } - return baseShapes - })()} - currentNode={analysisController.currentNode as GameNode} - orientation={analysisController.orientation} - onPlayerMakeMove={onPlayerMakeMove} - goToNode={analysisController.goToNode} - gameTree={game.tree} - destinationBadges={destinationBadges} - /> - {promotionFromTo ? ( - +
+
+
+ + live_tv + +

Switch Game

+
+ +
+

+ Select a broadcast game to analyze +

+
+
+ setShowGameListMobile(false)} /> - ) : null} +
-
-
- + setShowGameListMobile(true)} + showGameListButton={true} + > + + +
+
+
+ + Maia % + +
+
+
+ +
+ +
+ +
+
+
+ + SF Eval + +
+
+
+
+ +
+
+ { + const baseShapes = [...playedMoveShapes] + baseShapes.push(...analysisController.arrows) + + if (hoverArrow) { + baseShapes.push(hoverArrow) + } + + return staggerOverlappingArrows(baseShapes) + })()} + currentNode={analysisController.currentNode as GameNode} + orientation={analysisController.orientation} + onPlayerMakeMove={onPlayerMakeMove} + goToNode={analysisController.goToNode} + gameTree={game.tree} + destinationBadges={destinationBadges} + brushes={analysisArrowBrushes as unknown as DrawBrushes} + /> + {promotionFromTo ? ( + + ) : null} +
+
+ +
+
+
-
- +
+
+ +
+
+ +
-
- - +
+
+ +
+ +
+ +
+
+ + + )}
) - return
{isMobile ? mobileLayout : desktopLayout}
+ return ( +
{useMobileStyleAnalysisLayout ? mobileLayout : desktopLayout}
+ ) } diff --git a/src/pages/broadcast/[broadcastId]/[roundId].tsx b/src/pages/broadcast/[broadcastId]/[roundId].tsx index 3e7f23e9..c884b234 100644 --- a/src/pages/broadcast/[broadcastId]/[roundId].tsx +++ b/src/pages/broadcast/[broadcastId]/[roundId].tsx @@ -116,6 +116,15 @@ const BroadcastAnalysisPage: NextPage = () => { false, ) + useEffect(() => { + if (analysisController.currentMaiaModel !== 'maia_kdd_2600') { + analysisController.setCurrentMaiaModel('maia_kdd_2600') + } + }, [ + analysisController.currentMaiaModel, + analysisController.setCurrentMaiaModel, + ]) + // Auto-follow live moves for the selected game const lastGameMoveCount = useRef(0)