diff --git a/src/components/panels/analysis/AnalysisPanel.css.ts b/src/components/panels/analysis/AnalysisPanel.css.ts new file mode 100644 index 00000000..4691a4f5 --- /dev/null +++ b/src/components/panels/analysis/AnalysisPanel.css.ts @@ -0,0 +1,8 @@ +import { style } from "@vanilla-extract/css"; + +export const label = style({ + cursor: "pointer", + ":hover": { + textDecoration: "underline", + }, +}); diff --git a/src/components/panels/analysis/AnalysisPanel.tsx b/src/components/panels/analysis/AnalysisPanel.tsx index 179612d4..bf4b4e37 100644 --- a/src/components/panels/analysis/AnalysisPanel.tsx +++ b/src/components/panels/analysis/AnalysisPanel.tsx @@ -10,8 +10,15 @@ import { import { events } from "@/bindings"; import EvalChart from "@/components/common/EvalChart"; import ProgressButton from "@/components/common/ProgressButton"; -import { TreeStateContext } from "@/components/common/TreeStateContext"; -import { ANNOTATION_INFO, isBasicAnnotation } from "@/utils/annotation"; +import { + TreeDispatchContext, + TreeStateContext, +} from "@/components/common/TreeStateContext"; +import { + ANNOTATION_INFO, + type Annotation, + isBasicAnnotation, +} from "@/utils/annotation"; import { getGameStats, getVariationLine } from "@/utils/chess"; import { getPiecesCount, hasCaptures, positionFromFen } from "@/utils/chessops"; import type { Engine } from "@/utils/engines"; @@ -41,9 +48,11 @@ import { IconZoomCheck, } from "@tabler/icons-react"; import { useNavigate } from "@tanstack/react-router"; +import cx from "clsx"; import { useAtom, useAtomValue } from "jotai"; import { memo, useContext, useMemo } from "react"; import React from "react"; +import { label } from "./AnalysisPanel.css"; import BestMoves, { arrowColors } from "./BestMoves"; import EngineSelection from "./EngineSelection"; import LogsPanel from "./LogsPanel"; @@ -395,6 +404,18 @@ type Stats = ReturnType; const GameStats = memo( function GameStats({ whiteAnnotations, blackAnnotations }: Stats) { + const dispatch = useContext(TreeDispatchContext); + + function goToAnnotation(annotation: Annotation, color: "white" | "black") { + dispatch({ + type: "GO_TO_ANNOTATION", + payload: { + annotation, + color, + }, + }); + } + return ( @@ -408,9 +429,15 @@ const GameStats = memo( return ( 0 && label)} span={4} style={{ textAlign: "center" }} c={w > 0 ? color : undefined} + onClick={() => { + if (w > 0) { + goToAnnotation(s, "white"); + } + }} > {w} @@ -420,7 +447,16 @@ const GameStats = memo( 0 ? color : undefined}> {name} - 0 ? color : undefined}> + 0 && label)} + span={2} + c={b > 0 ? color : undefined} + onClick={() => { + if (b > 0) { + goToAnnotation(s, "black"); + } + }} + > {b} diff --git a/src/utils/treeReducer.ts b/src/utils/treeReducer.ts index 1de6f12b..a92fbf11 100644 --- a/src/utils/treeReducer.ts +++ b/src/utils/treeReducer.ts @@ -1,6 +1,6 @@ import type { BestMoves, Score } from "@/bindings"; import type { DrawShape } from "chessground/draw"; -import { type Move, isNormal } from "chessops"; +import { type Color, type Move, isNormal } from "chessops"; import { INITIAL_FEN, makeFen } from "chessops/fen"; import { makeSan, parseSan } from "chessops/san"; import { match } from "ts-pattern"; @@ -237,6 +237,10 @@ export type TreeAction = | { type: "GO_TO_NEXT" } | { type: "GO_TO_PREVIOUS" } | { type: "GO_TO_MOVE"; payload: number[] } + | { + type: "GO_TO_ANNOTATION"; + payload: { annotation: Annotation; color: Color }; + } | { type: "DELETE_MOVE"; payload?: number[] } | { type: "SET_ANNOTATION"; payload: Annotation } | { type: "SET_COMMENT"; payload: string } @@ -360,6 +364,30 @@ const treeReducer = (state: TreeState, action: TreeAction) => { .with({ type: "GO_TO_MOVE" }, ({ payload }) => { state.position = payload; }) + .with({ type: "GO_TO_ANNOTATION" }, ({ payload }) => { + const color = payload.color === "white" ? 1 : 0; + + let p: number[] = state.position; + let node = getNodeAtPath(state.root, p); + while (true) { + if (node.children.length === 0) { + p = []; + } else { + p.push(0); + } + + node = getNodeAtPath(state.root, p); + + if ( + node.annotations.includes(payload.annotation) && + node.halfMoves % 2 === color + ) { + break; + } + } + + state.position = p; + }) .with({ type: "DELETE_MOVE" }, (action) => { state.dirty = true; deleteMove(state, action.payload || state.position);