diff --git a/src/components/Puzzles/Feedback.tsx b/src/components/Puzzles/Feedback.tsx index 95223d7d..d7cdec94 100644 --- a/src/components/Puzzles/Feedback.tsx +++ b/src/components/Puzzles/Feedback.tsx @@ -1,7 +1,6 @@ import { Chess } from 'chess.ts' import { useMemo, Dispatch, SetStateAction } from 'react' -import { Markdown } from 'src/components' import { useTrainingController } from 'src/hooks' import { PuzzleGame, Status } from 'src/types/puzzle' @@ -14,6 +13,7 @@ interface Props { controller: ReturnType lastAttemptedMove: string | null setLastAttemptedMove: Dispatch> + solutionMoveSan: string | null embedded?: boolean } @@ -26,81 +26,91 @@ export const Feedback: React.FC = ({ controller: controller, lastAttemptedMove, setLastAttemptedMove, + solutionMoveSan, embedded = false, }: Props) => { - const { targetIndex } = game - const turn = new Chess(controller.gameTree.getLastMainlineNode().fen).turn() === 'w' ? 'white' : 'black' - const archivedContent = ` - ##### PUZZLE COMPLETED - You already solved this puzzle. Use the boxes on the left to navigate to another puzzle. -` + const content = useMemo(() => { + if (status === 'archived') { + return { + titlePrefix: null, + title: 'You already solved this puzzle.', + detail: 'Choose another puzzle from the history list.', + titleClass: 'text-primary', + accentPrefixClass: '', + } + } - const defaultContent = ` - ##### YOUR TURN - Find the best move for **${turn}**! - ` - const incorrectContent = ` - ##### ${lastAttemptedMove || 'Move'} is incorrect - Try again or give up to analyze the board and see the best move. - ` + if (status === 'forfeit') { + return { + titlePrefix: null, + title: `${solutionMoveSan || 'That move'} is the best move.`, + detail: 'Explore the position or try the next puzzle.', + titleClass: 'text-primary', + accentPrefixClass: '', + } + } - const correctContent = ` - ##### Correct! ${lastAttemptedMove || 'Move'} is the best move. - You can now explore and analyze the position by making moves, or train on another position. - ` + if (status === 'correct') { + return { + titlePrefix: 'Correct!', + title: ` ${lastAttemptedMove || 'That move'} is the best move.`, + detail: 'Explore the position or try the next puzzle.', + titleClass: 'text-primary', + accentPrefixClass: 'text-green-400', + } + } - const gaveUpContent = ` - ##### Explore the position - Explore the current position by making moves or train on another position.` + if (status === 'incorrect') { + return { + titlePrefix: 'Incorrect.', + title: ` ${lastAttemptedMove || 'That move'} is not the best move.`, + detail: 'Try again or give up to unlock analysis.', + titleClass: 'text-primary', + accentPrefixClass: 'text-human-2', + } + } - const content = useMemo(() => { - if (status === 'archived') { - return archivedContent - } else if (status === 'forfeit') { - return gaveUpContent - } else if (status === 'correct') { - return correctContent - } else if (status === 'incorrect') { - return incorrectContent - } else { - return defaultContent + return { + titlePrefix: null, + title: `Find the best move for ${turn}.`, + detail: 'Give up if you want to reveal the answer and analyze it.', + titleClass: 'text-primary', + accentPrefixClass: '', } - }, [defaultContent, incorrectContent, correctContent, status, targetIndex]) + }, [lastAttemptedMove, solutionMoveSan, status, turn]) return (
-
- {content.trim()} - {(status === 'forfeit' || status === 'correct') && ( -
-
- - arrow_outward - - Most Human Move -
-
- - arrow_outward +
+
+

+ {content.titlePrefix ? ( + + {content.titlePrefix} - Best Engine Move -

-
- )} + ) : null} + {content.title} + +

+ {content.detail} +

+
-
+
{status !== 'archived' && ( <> {status === 'incorrect' && ( @@ -110,7 +120,7 @@ export const Feedback: React.FC = ({ setLastAttemptedMove(null) controller.reset() }} - className="flex w-full justify-center rounded-sm bg-engine-3 py-1.5 text-sm font-medium text-primary transition duration-300 hover:bg-engine-4 disabled:bg-backdrop disabled:text-secondary" + className="flex w-full justify-center rounded-md border border-engine-4/40 bg-engine-4/15 py-2 text-sm font-medium text-primary transition duration-300 hover:bg-engine-4/25 disabled:bg-backdrop disabled:text-secondary" > Try Again @@ -118,7 +128,7 @@ export const Feedback: React.FC = ({ {status !== 'forfeit' && status !== 'correct' && ( @@ -128,7 +138,7 @@ export const Feedback: React.FC = ({ onClick={async () => { await getNewGame() }} - className="flex w-full justify-center rounded-sm bg-human-3 py-1.5 text-sm font-medium text-primary transition duration-300 hover:bg-human-4 disabled:bg-backdrop disabled:text-secondary" + className="flex w-full justify-center rounded-md bg-human-3 py-2 text-sm font-medium text-primary transition duration-300 hover:bg-human-4 disabled:bg-backdrop disabled:text-secondary" > Next Puzzle diff --git a/src/pages/puzzles.tsx b/src/pages/puzzles.tsx index aeb85172..7959c12c 100644 --- a/src/pages/puzzles.tsx +++ b/src/pages/puzzles.tsx @@ -16,7 +16,7 @@ import { useRouter } from 'next/router' import type { Key } from 'chessground/types' import type { DrawShape } from 'chessground/draw' import { Chess, PieceSymbol } from 'chess.ts' -import { AnimatePresence, motion } from 'framer-motion' +import { AnimatePresence, motion, useSpring, useTransform } from 'framer-motion' import { fetchPuzzle, logPuzzleGuesses, @@ -38,9 +38,13 @@ import { GameBoard, PromotionOverlay, DownloadModelModal, - Highlight, - BlunderMeter, AnalysisSidebar, + AnalysisArrowLegend, + AnalysisCompactBlunderMeter, + AnalysisMaiaWinrateBar, + AnalysisStockfishEvalBar, + SimplifiedAnalysisOverview, + MovesByRating, } from 'src/components' import { useTrainingController } from 'src/hooks/useTrainingController' import { useAnalysisController } from 'src/hooks/useAnalysisController' @@ -54,8 +58,17 @@ import { getAvailableMovesArray, requiresPromotion, } from 'src/lib/puzzle' +import { cpToWinrate } from 'src/lib/analysis' import { tourConfigs } from 'src/constants/tours' +const EVAL_BAR_RANGE = 4 +const DEFAULT_STOCKFISH_EVAL_BAR = { + hasEval: false, + pawns: 0, + displayPawns: 0, + label: '--', +} + const statsLoader = async () => { const stats = await fetchTrainingPlayerStats() return { @@ -82,6 +95,7 @@ const TrainPage: NextPage = () => { const [lastAttemptedMove, setLastAttemptedMove] = useState( null, ) + const [solutionMoveSan, setSolutionMoveSan] = useState(null) useEffect(() => { if (!initialTourCheck && tourState.ready) { @@ -107,6 +121,7 @@ const TrainPage: NextPage = () => { setStatus('default') setUserGuesses([]) setLastAttemptedMove(null) + setSolutionMoveSan(null) setCurrentIndex(trainingGames.length) setTrainingGames(trainingGames.concat([game])) setPreviousGameResults(previousGameResults.concat([{ ...game }])) @@ -144,6 +159,16 @@ const TrainPage: NextPage = () => { newGuesses, status === 'forfeit', ) + const solutionMoveUci = response.correct_moves?.[0] + const solutionSan = + solutionMoveUci && trainingGames[puzzleIdx] + ? (trainingGames[puzzleIdx].availableMoves?.[solutionMoveUci]?.san ?? + null) + : null + + if (solutionSan) { + setSolutionMoveSan(solutionSan) + } if (status === 'forfeit') { setPreviousGameResults((prev) => { @@ -243,24 +268,22 @@ const TrainPage: NextPage = () => { logGuess={logGuess} lastAttemptedMove={lastAttemptedMove} setLastAttemptedMove={setLastAttemptedMove} + solutionMoveSan={solutionMoveSan} gamesController={ - <> -
-
- -
-
- -
+
+
+
- +
+ +
+
} /> ) @@ -289,6 +312,7 @@ interface Props { ) => void lastAttemptedMove: string | null setLastAttemptedMove: Dispatch> + solutionMoveSan: string | null } const Train: React.FC = ({ @@ -301,6 +325,7 @@ const Train: React.FC = ({ logGuess, lastAttemptedMove, setLastAttemptedMove, + solutionMoveSan, }: Props) => { const controller = useTrainingController(trainingGame) @@ -314,7 +339,7 @@ const Train: React.FC = ({ false, // Disable auto-saving on puzzles page ) - const { width } = useContext(WindowSizeContext) + const { width, height } = useContext(WindowSizeContext) const isMobile = useMemo( () => width > 0 && width <= TABLET_BREAKPOINT_PX, [width], @@ -327,6 +352,14 @@ const Train: React.FC = ({ const [userAnalysisEnabled, setUserAnalysisEnabled] = useState< boolean | null >(null) // User's choice, null means not set + const desktopBoardHeaderStripRef = useRef(null) + const desktopBlunderMeterSectionRef = useRef(null) + const desktopBoardControllerSectionRef = useRef(null) + const [desktopMeasuredHeights, setDesktopMeasuredHeights] = useState({ + headerPx: 28, + blunderMeterPx: 126, + boardControllerPx: 44, + }) const showAnalysis = status === 'correct' || status === 'forfeit' || status === 'archived' @@ -365,6 +398,308 @@ const Train: React.FC = ({ [], ) + const compactBlunderMeterData = useMemo( + () => + analysisEnabled && showAnalysis + ? analysisController.blunderMeter + : emptyBlunderMeterData, + [ + analysisEnabled, + showAnalysis, + analysisController.blunderMeter, + emptyBlunderMeterData, + ], + ) + + 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?.stockfish, + ]) + + const displayedStockfishEvalText = useMemo(() => { + if (!analysisEnabled || !showAnalysis || !rawStockfishEvalBar.hasEval) { + return '--' + } + + if (rawStockfishEvalBar.label.startsWith('M')) { + return rawStockfishEvalBar.label + } + + const roundedPawns = Math.round(rawStockfishEvalBar.displayPawns * 10) / 10 + const safePawns = Math.abs(roundedPawns) < 0.05 ? 0 : roundedPawns + return `${safePawns > 0 ? '+' : ''}${safePawns.toFixed(1)}` + }, [ + analysisEnabled, + showAnalysis, + rawStockfishEvalBar.displayPawns, + rawStockfishEvalBar.hasEval, + rawStockfishEvalBar.label, + ]) + + const evalPositionPercent = useMemo(() => { + const normalized = + (rawStockfishEvalBar.pawns + EVAL_BAR_RANGE) / (EVAL_BAR_RANGE * 2) + return Math.max(0, Math.min(1, normalized)) * 100 + }, [rawStockfishEvalBar.pawns]) + + const smoothedEvalPosition = useSpring(50, { + stiffness: 520, + damping: 42, + mass: 0.25, + }) + const smoothedEvalVerticalPositionLabel = useTransform( + smoothedEvalPosition, + (value) => `${100 - value}%`, + ) + + useEffect(() => { + smoothedEvalPosition.set( + analysisEnabled && showAnalysis && rawStockfishEvalBar.hasEval + ? evalPositionPercent + : 50, + ) + }, [ + analysisEnabled, + showAnalysis, + rawStockfishEvalBar.hasEval, + evalPositionPercent, + smoothedEvalPosition, + ]) + + 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?.maia, + analysisController.moveEvaluation?.stockfish, + currentTurnForBars, + isCurrentPositionCheckmateForBars, + isInFirst10PlyForBars, + ]) + + const maiaWhiteWinPositionPercent = useMemo( + () => Math.max(0, Math.min(100, rawMaiaWhiteWinBar.percent)), + [rawMaiaWhiteWinBar.percent], + ) + const renderedMaiaWhiteWinBar = useMemo( + () => + analysisEnabled && showAnalysis + ? rawMaiaWhiteWinBar + : { hasValue: false, percent: 50, label: '--' }, + [analysisEnabled, showAnalysis, rawMaiaWhiteWinBar], + ) + + const smoothedMaiaWhiteWinPosition = useSpring(50, { + stiffness: 520, + damping: 42, + mass: 0.25, + }) + const smoothedMaiaWhiteWinVerticalPositionLabel = useTransform( + smoothedMaiaWhiteWinPosition, + (value) => `${100 - value}%`, + ) + + useEffect(() => { + smoothedMaiaWhiteWinPosition.set( + analysisEnabled && showAnalysis ? maiaWhiteWinPositionPercent : 50, + ) + }, [ + analysisEnabled, + showAnalysis, + maiaWhiteWinPositionPercent, + smoothedMaiaWhiteWinPosition, + ]) + + useEffect(() => { + if (isMobile) return + + const headerEl = desktopBoardHeaderStripRef.current + const blunderEl = desktopBlunderMeterSectionRef.current + const controllerEl = desktopBoardControllerSectionRef.current + + if (!headerEl && !blunderEl && !controllerEl) return + + const next = { + headerPx: + headerEl?.getBoundingClientRect().height ?? + desktopMeasuredHeights.headerPx, + blunderMeterPx: + blunderEl?.getBoundingClientRect().height ?? + desktopMeasuredHeights.blunderMeterPx, + boardControllerPx: + controllerEl?.getBoundingClientRect().height ?? + desktopMeasuredHeights.boardControllerPx, + } + + setDesktopMeasuredHeights((prev) => { + if ( + Math.abs(prev.headerPx - next.headerPx) < 0.5 && + Math.abs(prev.blunderMeterPx - next.blunderMeterPx) < 0.5 && + Math.abs(prev.boardControllerPx - next.boardControllerPx) < 0.5 + ) { + return prev + } + + return next + }) + }, [desktopMeasuredHeights, isMobile, showAnalysis, status, width]) + + 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 gapAllowancePx = showAnalysis ? 24 : 12 + const measuredNonBoardHeightPx = + desktopMeasuredHeights.headerPx + + (showAnalysis ? desktopMeasuredHeights.blunderMeterPx : 0) + + desktopMeasuredHeights.boardControllerPx + + gapAllowancePx + + return Math.max( + 340, + Math.floor(targetColumnHeightPx - measuredNonBoardHeightPx), + ) + }, [desktopMeasuredHeights, height, showAnalysis]) + 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 currentPlayer = useMemo(() => { const currentNode = analysisEnabled && showAnalysis @@ -379,9 +714,9 @@ const Train: React.FC = ({ ]) useEffect(() => { if (analysisEnabled && showAnalysis && !analysisSyncedRef.current) { - // Set the analysis controller to the current training controller's node - // Only sync once when analysis mode is first enabled - analysisController.setCurrentNode(controller.currentNode) + // Start post-puzzle analysis from the original puzzle position rather + // than the solution move that may have just been played. + analysisController.setCurrentNode(controller.puzzleStartingNode) analysisSyncedRef.current = true } else if (!showAnalysis || !analysisEnabled) { // Reset sync flag when exiting analysis mode @@ -391,7 +726,7 @@ const Train: React.FC = ({ analysisEnabled, showAnalysis, analysisController, - controller.currentNode, + controller.puzzleStartingNode, ]) const onSelectSquare = useCallback( @@ -706,14 +1041,13 @@ const Train: React.FC = ({ exit="exit" style={{ willChange: 'transform, opacity' }} > -
+
-
- {/* Header */} +
@@ -732,133 +1066,242 @@ const Train: React.FC = ({
+
- {/* Puzzle log */} -
-
- {gamesController} -
- - {/* Stats */} -
-
- -
+
+ {gamesController}
+ + + + -
- +
+
+ + White Win % + +
+
+ +
+
+ + SF Eval + +
+
+
+
+ +
+
+ + {promotionFromTo ? ( + + ) : null} +
+
+ +
+
+
+ +
+
+ ) : ( +
+ + {promotionFromTo ? ( + + ) : null} +
+ )} +
+ - {promotionFromTo ? ( - - ) : null} -
- -
-
@@ -869,6 +1312,12 @@ const Train: React.FC = ({ setHoverArrow={setHoverArrow} analysisEnabled={analysisEnabled} handleToggleAnalysis={handleToggleAnalysis} + hideDetailedBlunderMeter={true} + containerStyle={{ + width: 'clamp(23rem, 27vw, 26rem)', + minWidth: '23rem', + flexBasis: 'clamp(23rem, 27vw, 26rem)', + }} itemVariants={itemVariants} />
@@ -909,55 +1358,142 @@ const Train: React.FC = ({
-
- - {promotionFromTo ? ( - +
+
+ + Maia % + +
+
+ +
+
+ + SF Eval + +
+
+
+
+ +
+
+ + {promotionFromTo ? ( + + ) : null} +
+
+ +
+
+ - ) : null} -
+
+ ) : ( +
+ + {promotionFromTo ? ( + + ) : null} +
+ )}
= ({ getNewGame={getNewGame} lastAttemptedMove={lastAttemptedMove} setLastAttemptedMove={setLastAttemptedMove} + solutionMoveSan={solutionMoveSan} />
@@ -1051,59 +1588,81 @@ const Train: React.FC = ({
)} -
- void 0 - } - hover={analysisEnabled && showAnalysis ? hover : mockHover} - makeMove={ - analysisEnabled && showAnalysis ? makeMove : mockMakeMove - } - currentMaiaModel={ - analysisEnabled && showAnalysis - ? analysisController.currentMaiaModel - : 'maia_kdd_1500' - } - recommendations={ - analysisEnabled && showAnalysis - ? analysisController.moveRecommendations - : emptyRecommendations - } - moveEvaluation={ - analysisEnabled && showAnalysis - ? (analysisController.moveEvaluation as { - maia?: MaiaEvaluation - stockfish?: StockfishEvaluation - }) - : { - maia: undefined, - stockfish: undefined, - } - } - colorSanMapping={ - analysisEnabled && showAnalysis - ? analysisController.colorSanMapping - : {} - } - boardDescription={ - analysisEnabled && showAnalysis - ? analysisController.boardDescription - : { - segments: [ - { - type: 'text', - content: - 'Complete the puzzle to unlock analysis, or analysis is disabled.', - }, - ], - } - } +
+ void 0, + hover: analysisEnabled && showAnalysis ? hover : mockHover, + makeMove: + analysisEnabled && showAnalysis ? makeMove : mockMakeMove, + currentMaiaModel: + analysisEnabled && showAnalysis + ? analysisController.currentMaiaModel + : 'maia_kdd_1500', + recommendations: + analysisEnabled && showAnalysis + ? analysisController.moveRecommendations + : emptyRecommendations, + moveEvaluation: + analysisEnabled && showAnalysis + ? (analysisController.moveEvaluation as { + maia?: MaiaEvaluation + stockfish?: StockfishEvaluation + }) + : { + maia: undefined, + stockfish: undefined, + }, + colorSanMapping: + analysisEnabled && showAnalysis + ? analysisController.colorSanMapping + : {}, + boardDescription: + analysisEnabled && showAnalysis + ? analysisController.boardDescription + : { + segments: [ + { + type: 'text', + content: + 'Complete the puzzle to unlock analysis, or analysis is disabled.', + }, + ], + }, + currentNode: analysisController.currentNode ?? undefined, + simplified: true, + hideWhiteWinRateSummary: true, + hideStockfishEvalSummary: true, + }} + blunderMeterProps={{ + hover: analysisEnabled && showAnalysis ? hover : mockHover, + makeMove: + analysisEnabled && showAnalysis ? makeMove : mockMakeMove, + data: + analysisEnabled && showAnalysis + ? analysisController.blunderMeter + : emptyBlunderMeterData, + colorSanMapping: + analysisEnabled && showAnalysis + ? analysisController.colorSanMapping + : {}, + moveEvaluation: + analysisEnabled && showAnalysis + ? analysisController.moveEvaluation + : undefined, + playerToMove: + analysisEnabled && showAnalysis + ? (analysisController.currentNode?.turn ?? 'w') + : 'w', + }} + analysisEnabled={analysisEnabled && showAnalysis} + hideBlunderMeter={true} /> {!analysisEnabled && showAnalysis && ( -
+
lock @@ -1129,31 +1688,22 @@ const Train: React.FC = ({
- {!analysisEnabled && showAnalysis && (