Squashed commit of the following:
commit d9209a78cff1c05be3e6a87e27cd1a5a4d5f91c5 Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com> Date: Wed Jul 24 11:55:35 2024 +0200 style : UI analysis panel adjustment commit 3c2e19bdb9d97f3bb7e8ceaefd630aad64d755c4 Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com> Date: Wed Jul 24 11:10:07 2024 +0200 feat : graph dot color match move classification commit 4a99ccb2fe19d3806ff320370ebc55af984d719a Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com> Date: Wed Jul 24 11:09:35 2024 +0200 fix : load pgn with no moves commit 9eeb0e7f2869e544700b7da963b74f707fa6ea2f Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com> Date: Wed Jul 24 00:09:03 2024 +0200 feat : add current move reference line in graph commit febb9962a0b366aeac1dc266e0470b75bd619e68 Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com> Date: Tue Jul 23 23:08:17 2024 +0200 fix : handle tab change on new game commit a105239a728dc05211a0ae99d8fd56f179108a0e Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com> Date: Tue Jul 23 03:46:49 2024 +0200 style : small chart UI tweaks commit 4878ebf87b4ddbac75db70619fe452a3a317ca09 Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com> Date: Tue Jul 23 03:38:40 2024 +0200 feat : add eval graph commit 29c5a001da03ee288d2a2c133426b1d2ca435930 Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com> Date: Tue Jul 23 00:30:25 2024 +0200 refacto : analysis directory commit a8b966cc07152bb117b8c68f54af3498ca2a5d2f Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com> Date: Tue Jul 23 00:07:07 2024 +0200 style : add settings floating button commit 7edc54f09ce7d4b4c4beb310a9c7f985363ff5ee Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com> Date: Sun Jul 21 22:29:48 2024 +0200 feat : tab analysis panel
This commit is contained in:
46
src/sections/analysis/panelBody/analysisTab/accuracies.tsx
Normal file
46
src/sections/analysis/panelBody/analysisTab/accuracies.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Grid, Typography } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { gameEvalAtom } from "../../states";
|
||||
|
||||
export default function Accuracies() {
|
||||
const gameEval = useAtomValue(gameEvalAtom);
|
||||
|
||||
if (!gameEval) return null;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
xs={12}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
columnGap={{ xs: "8vw", md: 10 }}
|
||||
>
|
||||
<Typography
|
||||
align="center"
|
||||
sx={{ backgroundColor: "white", color: "black" }}
|
||||
borderRadius="5px"
|
||||
lineHeight={1}
|
||||
padding={1}
|
||||
fontWeight="bold"
|
||||
border="1px solid #424242"
|
||||
>
|
||||
{`${gameEval?.accuracy.white.toFixed(1)} %`}
|
||||
</Typography>
|
||||
|
||||
<Typography align="center">Accuracies</Typography>
|
||||
|
||||
<Typography
|
||||
align="center"
|
||||
sx={{ backgroundColor: "black", color: "white" }}
|
||||
borderRadius="5px"
|
||||
lineHeight={1}
|
||||
padding={1}
|
||||
fontWeight="bold"
|
||||
border="1px solid #424242"
|
||||
>
|
||||
{`${gameEval?.accuracy.black.toFixed(1)} %`}
|
||||
</Typography>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
80
src/sections/analysis/panelBody/analysisTab/index.tsx
Normal file
80
src/sections/analysis/panelBody/analysisTab/index.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Grid, GridProps, List, Typography } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import {
|
||||
boardAtom,
|
||||
engineMultiPvAtom,
|
||||
engineNameAtom,
|
||||
gameAtom,
|
||||
} from "../../states";
|
||||
import LineEvaluation from "./lineEvaluation";
|
||||
import { useCurrentPosition } from "../../hooks/useCurrentPosition";
|
||||
import { LineEval } from "@/types/eval";
|
||||
import Accuracies from "./accuracies";
|
||||
import MoveInfo from "./moveInfo";
|
||||
import Opening from "./opening";
|
||||
|
||||
export default function AnalysisTab(props: GridProps) {
|
||||
const linesNumber = useAtomValue(engineMultiPvAtom);
|
||||
const engineName = useAtomValue(engineNameAtom);
|
||||
const position = useCurrentPosition(engineName);
|
||||
const game = useAtomValue(gameAtom);
|
||||
const board = useAtomValue(boardAtom);
|
||||
|
||||
const boardHistory = board.history();
|
||||
const gameHistory = game.history();
|
||||
|
||||
const isGameOver =
|
||||
boardHistory.length > 0 &&
|
||||
(board.isCheckmate() ||
|
||||
board.isDraw() ||
|
||||
boardHistory.join() === gameHistory.join());
|
||||
|
||||
const linesSkeleton: LineEval[] = Array.from({ length: linesNumber }).map(
|
||||
(_, i) => ({ pv: [`${i}`], depth: 0, multiPv: i + 1 })
|
||||
);
|
||||
|
||||
const engineLines = position?.eval?.lines?.length
|
||||
? position.eval.lines
|
||||
: linesSkeleton;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
xs={12}
|
||||
justifyContent="center"
|
||||
alignItems="start"
|
||||
height="100%"
|
||||
rowGap={1.2}
|
||||
{...props}
|
||||
sx={
|
||||
props.hidden
|
||||
? { display: "none" }
|
||||
: { overflow: "hidden", overflowY: "auto", ...props.sx }
|
||||
}
|
||||
>
|
||||
<Accuracies />
|
||||
|
||||
<MoveInfo />
|
||||
|
||||
<Opening />
|
||||
|
||||
{isGameOver && (
|
||||
<Grid item xs={12}>
|
||||
<Typography align="center" fontSize="0.9rem">
|
||||
Game is over
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
||||
<List sx={{ maxWidth: "95%", padding: 0 }}>
|
||||
{!board.isCheckmate() &&
|
||||
engineLines.map((line) => (
|
||||
<LineEvaluation key={line.multiPv} line={line} />
|
||||
))}
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { LineEval } from "@/types/eval";
|
||||
import { ListItem, Skeleton, Typography } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom } from "../../states";
|
||||
import { getLineEvalLabel, moveLineUciToSan } from "@/lib/chess";
|
||||
|
||||
interface Props {
|
||||
line: LineEval;
|
||||
}
|
||||
|
||||
export default function LineEvaluation({ line }: Props) {
|
||||
const board = useAtomValue(boardAtom);
|
||||
const lineLabel = getLineEvalLabel(line);
|
||||
|
||||
const isBlackCp =
|
||||
(line.cp !== undefined && line.cp < 0) ||
|
||||
(line.mate !== undefined && line.mate < 0);
|
||||
|
||||
const showSkeleton = line.depth < 6;
|
||||
|
||||
return (
|
||||
<ListItem disablePadding>
|
||||
<Typography
|
||||
marginRight={1.5}
|
||||
marginY={0.5}
|
||||
paddingY={0.2}
|
||||
noWrap
|
||||
overflow="visible"
|
||||
width="3.5em"
|
||||
textAlign="center"
|
||||
fontSize="0.8rem"
|
||||
sx={{
|
||||
backgroundColor: isBlackCp ? "black" : "white",
|
||||
color: isBlackCp ? "white" : "black",
|
||||
}}
|
||||
borderRadius="5px"
|
||||
border="1px solid #424242"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{showSkeleton ? (
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
animation="wave"
|
||||
sx={{ color: "transparent" }}
|
||||
>
|
||||
placeholder
|
||||
</Skeleton>
|
||||
) : (
|
||||
lineLabel
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
noWrap
|
||||
maxWidth={{ xs: "12em", sm: "25em", md: "30em", lg: "25em" }}
|
||||
fontSize="0.9rem"
|
||||
>
|
||||
{showSkeleton ? (
|
||||
<Skeleton variant="rounded" animation="wave" width="15em" />
|
||||
) : (
|
||||
line.pv.map(moveLineUciToSan(board.fen())).join(", ")
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
64
src/sections/analysis/panelBody/analysisTab/moveInfo.tsx
Normal file
64
src/sections/analysis/panelBody/analysisTab/moveInfo.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Grid, Typography } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom, currentPositionAtom } from "../../states";
|
||||
import { useMemo } from "react";
|
||||
import { moveLineUciToSan } from "@/lib/chess";
|
||||
import { MoveClassification } from "@/types/enums";
|
||||
|
||||
export default function MoveInfo() {
|
||||
const position = useAtomValue(currentPositionAtom);
|
||||
const board = useAtomValue(boardAtom);
|
||||
|
||||
const bestMove = position?.lastEval?.bestMove;
|
||||
|
||||
const bestMoveSan = useMemo(() => {
|
||||
if (!bestMove) return undefined;
|
||||
|
||||
const lastPosition = board.history({ verbose: true }).at(-1)?.before;
|
||||
if (!lastPosition) return undefined;
|
||||
|
||||
return moveLineUciToSan(lastPosition)(bestMove);
|
||||
}, [bestMove, board]);
|
||||
|
||||
if (!bestMoveSan) return null;
|
||||
|
||||
const moveClassification = position.eval?.moveClassification;
|
||||
const moveLabel = moveClassification
|
||||
? `${position.lastMove?.san} is ${moveClassificationLabels[moveClassification]}`
|
||||
: null;
|
||||
|
||||
const bestMoveLabel =
|
||||
moveClassification === MoveClassification.Best ||
|
||||
moveClassification === MoveClassification.Book ||
|
||||
moveClassification === MoveClassification.Brilliant ||
|
||||
moveClassification === MoveClassification.Great
|
||||
? null
|
||||
: `${bestMoveSan} was the best move`;
|
||||
|
||||
return (
|
||||
<Grid item container columnGap={5} xs={12} justifyContent="center">
|
||||
{moveLabel && (
|
||||
<Typography align="center" fontSize="0.9rem">
|
||||
{moveLabel}
|
||||
</Typography>
|
||||
)}
|
||||
{bestMoveLabel && (
|
||||
<Typography align="center" fontSize="0.9rem">
|
||||
{bestMoveLabel}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
const moveClassificationLabels: Record<MoveClassification, string> = {
|
||||
[MoveClassification.Book]: "a book move",
|
||||
[MoveClassification.Brilliant]: "brilliant !!",
|
||||
[MoveClassification.Great]: "a great move !",
|
||||
[MoveClassification.Best]: "the best move",
|
||||
[MoveClassification.Excellent]: "excellent",
|
||||
[MoveClassification.Good]: "good",
|
||||
[MoveClassification.Inaccuracy]: "an inaccuracy",
|
||||
[MoveClassification.Mistake]: "a mistake",
|
||||
[MoveClassification.Blunder]: "a blunder",
|
||||
};
|
||||
17
src/sections/analysis/panelBody/analysisTab/opening.tsx
Normal file
17
src/sections/analysis/panelBody/analysisTab/opening.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useCurrentPosition } from "../../hooks/useCurrentPosition";
|
||||
import { Grid, Typography } from "@mui/material";
|
||||
|
||||
export default function Opening() {
|
||||
const position = useCurrentPosition();
|
||||
|
||||
const opening = position?.eval?.opening;
|
||||
if (!opening) return null;
|
||||
|
||||
return (
|
||||
<Grid item xs={12}>
|
||||
<Typography align="center" fontSize="0.9rem">
|
||||
{opening}
|
||||
</Typography>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
24
src/sections/analysis/panelBody/classificationTab/index.tsx
Normal file
24
src/sections/analysis/panelBody/classificationTab/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Grid, GridProps } from "@mui/material";
|
||||
import MovesPanel from "./movesPanel";
|
||||
import MovesClassificationsRecap from "./movesClassificationsRecap";
|
||||
|
||||
export default function ClassificationTab(props: GridProps) {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
justifyContent="center"
|
||||
alignItems="start"
|
||||
height="100%"
|
||||
maxHeight="18rem"
|
||||
{...props}
|
||||
sx={
|
||||
props.hidden ? { display: "none" } : { overflow: "hidden", ...props.sx }
|
||||
}
|
||||
>
|
||||
<MovesPanel />
|
||||
|
||||
<MovesClassificationsRecap />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Color, MoveClassification } from "@/types/enums";
|
||||
import { Grid, Typography } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom, gameAtom, gameEvalAtom } from "../../../states";
|
||||
import { useMemo } from "react";
|
||||
import Image from "next/image";
|
||||
import { capitalize } from "@/lib/helpers";
|
||||
import { useChessActions } from "@/hooks/useChessActions";
|
||||
import { moveClassificationColors } from "@/lib/chess";
|
||||
|
||||
interface Props {
|
||||
classification: MoveClassification;
|
||||
}
|
||||
|
||||
export default function ClassificationRow({ classification }: Props) {
|
||||
const gameEval = useAtomValue(gameEvalAtom);
|
||||
const board = useAtomValue(boardAtom);
|
||||
const game = useAtomValue(gameAtom);
|
||||
const { goToMove } = useChessActions(boardAtom);
|
||||
|
||||
const whiteNb = useMemo(() => {
|
||||
if (!gameEval) return 0;
|
||||
return gameEval.positions.filter(
|
||||
(position, idx) =>
|
||||
idx % 2 !== 0 && position.moveClassification === classification
|
||||
).length;
|
||||
}, [gameEval, classification]);
|
||||
|
||||
const blackNb = useMemo(() => {
|
||||
if (!gameEval) return 0;
|
||||
return gameEval.positions.filter(
|
||||
(position, idx) =>
|
||||
idx % 2 === 0 && position.moveClassification === classification
|
||||
).length;
|
||||
}, [gameEval, classification]);
|
||||
|
||||
const handleClick = (color: Color) => {
|
||||
if (
|
||||
!gameEval ||
|
||||
(color === Color.White && !whiteNb) ||
|
||||
(color === Color.Black && !blackNb)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filterColor = (idx: number) =>
|
||||
(idx % 2 !== 0 && color === Color.White) ||
|
||||
(idx % 2 === 0 && color === Color.Black);
|
||||
const moveIdx = board.history().length;
|
||||
|
||||
const nextPositionIdx = gameEval.positions.findIndex(
|
||||
(position, idx) =>
|
||||
filterColor(idx) &&
|
||||
position.moveClassification === classification &&
|
||||
idx > moveIdx
|
||||
);
|
||||
|
||||
if (nextPositionIdx > 0) {
|
||||
goToMove(nextPositionIdx, game);
|
||||
} else {
|
||||
const firstPositionIdx = gameEval.positions.findIndex(
|
||||
(position, idx) =>
|
||||
filterColor(idx) && position.moveClassification === classification
|
||||
);
|
||||
if (firstPositionIdx > 0 && firstPositionIdx !== moveIdx) {
|
||||
goToMove(firstPositionIdx, game);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
xs={12}
|
||||
wrap="nowrap"
|
||||
color={moveClassificationColors[classification]}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
width={"3rem"}
|
||||
style={{ cursor: whiteNb ? "pointer" : "default" }}
|
||||
onClick={() => handleClick(Color.White)}
|
||||
fontSize="0.9rem"
|
||||
>
|
||||
{whiteNb}
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
width={"7rem"}
|
||||
gap={1}
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Image
|
||||
src={`/icons/${classification}.png`}
|
||||
alt="move-icon"
|
||||
width={18}
|
||||
height={18}
|
||||
style={{
|
||||
maxWidth: "3.5vw",
|
||||
maxHeight: "3.5vw",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography align="center" fontSize="0.9rem">
|
||||
{capitalize(classification)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
width={"3rem"}
|
||||
style={{ cursor: blackNb ? "pointer" : "default" }}
|
||||
onClick={() => handleClick(Color.Black)}
|
||||
fontSize="0.9rem"
|
||||
>
|
||||
{blackNb}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { usePlayersNames } from "@/hooks/usePlayerNames";
|
||||
import { Grid, Typography } from "@mui/material";
|
||||
import { gameAtom, gameEvalAtom } from "../../../states";
|
||||
import { MoveClassification } from "@/types/enums";
|
||||
import ClassificationRow from "./classificationRow";
|
||||
import { useAtomValue } from "jotai";
|
||||
|
||||
export default function MovesClassificationsRecap() {
|
||||
const { whiteName, blackName } = usePlayersNames(gameAtom);
|
||||
const gameEval = useAtomValue(gameEvalAtom);
|
||||
|
||||
if (!gameEval?.positions.length) return null;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
rowGap={1}
|
||||
xs={6}
|
||||
sx={{ scrollbarWidth: "thin", overflowY: "auto" }}
|
||||
maxHeight="100%"
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
alignItems="center"
|
||||
justifyContent="space-evenly"
|
||||
wrap="nowrap"
|
||||
xs={12}
|
||||
>
|
||||
<Typography width="12rem" align="center" noWrap fontSize="0.9rem">
|
||||
{whiteName}
|
||||
</Typography>
|
||||
|
||||
<Typography width="7rem" />
|
||||
|
||||
<Typography width="12rem" align="center" noWrap fontSize="0.9rem">
|
||||
{blackName}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{sortedMoveClassfications.map((classification) => (
|
||||
<ClassificationRow
|
||||
key={classification}
|
||||
classification={classification}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export const sortedMoveClassfications = [
|
||||
MoveClassification.Brilliant,
|
||||
MoveClassification.Great,
|
||||
MoveClassification.Best,
|
||||
MoveClassification.Excellent,
|
||||
MoveClassification.Good,
|
||||
MoveClassification.Book,
|
||||
MoveClassification.Inaccuracy,
|
||||
MoveClassification.Mistake,
|
||||
MoveClassification.Blunder,
|
||||
];
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Grid } from "@mui/material";
|
||||
import MovesLine from "./movesLine";
|
||||
import { useMemo } from "react";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { gameAtom, gameEvalAtom } from "../../../states";
|
||||
import { MoveClassification } from "@/types/enums";
|
||||
|
||||
export default function MovesPanel() {
|
||||
const game = useAtomValue(gameAtom);
|
||||
const gameEval = useAtomValue(gameEvalAtom);
|
||||
|
||||
const gameMoves = useMemo(() => {
|
||||
const history = game.history();
|
||||
if (!history.length) return undefined;
|
||||
|
||||
const moves: { san: string; moveClassification?: MoveClassification }[][] =
|
||||
[];
|
||||
|
||||
for (let i = 0; i < history.length; i += 2) {
|
||||
const items = [
|
||||
{
|
||||
san: history[i],
|
||||
moveClassification: gameEval?.positions[i + 1]?.moveClassification,
|
||||
},
|
||||
];
|
||||
|
||||
if (history[i + 1]) {
|
||||
items.push({
|
||||
san: history[i + 1],
|
||||
moveClassification: gameEval?.positions[i + 2]?.moveClassification,
|
||||
});
|
||||
}
|
||||
|
||||
moves.push(items);
|
||||
}
|
||||
|
||||
return moves;
|
||||
}, [game, gameEval]);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
justifyContent="center"
|
||||
alignItems="start"
|
||||
gap={0.8}
|
||||
sx={{ scrollbarWidth: "thin", overflowY: "auto" }}
|
||||
maxHeight="100%"
|
||||
xs={6}
|
||||
id="moves-panel"
|
||||
>
|
||||
{gameMoves?.map((moves, idx) => (
|
||||
<MovesLine
|
||||
key={`${moves.map(({ san }) => san).join()}-${idx}`}
|
||||
moves={moves}
|
||||
moveNb={idx + 1}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { MoveClassification } from "@/types/enums";
|
||||
import { Grid, Typography } from "@mui/material";
|
||||
import Image from "next/image";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom, currentPositionAtom, gameAtom } from "../../../states";
|
||||
import { useChessActions } from "@/hooks/useChessActions";
|
||||
import { useEffect } from "react";
|
||||
import { isInViewport } from "@/lib/helpers";
|
||||
import { moveClassificationColors } from "@/lib/chess";
|
||||
|
||||
interface Props {
|
||||
san: string;
|
||||
moveClassification?: MoveClassification;
|
||||
moveIdx: number;
|
||||
}
|
||||
|
||||
export default function MoveItem({ san, moveClassification, moveIdx }: Props) {
|
||||
const game = useAtomValue(gameAtom);
|
||||
const { goToMove } = useChessActions(boardAtom);
|
||||
const position = useAtomValue(currentPositionAtom);
|
||||
const color = getMoveColor(moveClassification);
|
||||
|
||||
const isCurrentMove = position?.currentMoveIdx === moveIdx;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCurrentMove) return;
|
||||
const moveItem = document.getElementById(`move-${moveIdx}`);
|
||||
if (!moveItem) return;
|
||||
|
||||
const movePanel = document.getElementById("moves-panel");
|
||||
if (!movePanel || !isInViewport(movePanel)) return;
|
||||
|
||||
moveItem.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}, [isCurrentMove, moveIdx]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (isCurrentMove) return;
|
||||
goToMove(moveIdx, game);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
width="5rem"
|
||||
wrap="nowrap"
|
||||
onClick={handleClick}
|
||||
paddingY={0.6}
|
||||
sx={(theme) => ({
|
||||
cursor: isCurrentMove ? undefined : "pointer",
|
||||
backgroundColor:
|
||||
isCurrentMove && theme.palette.mode === "dark"
|
||||
? "#4f4f4f"
|
||||
: undefined,
|
||||
border:
|
||||
isCurrentMove && theme.palette.mode === "light"
|
||||
? "1px solid #424242"
|
||||
: undefined,
|
||||
borderRadius: 1,
|
||||
})}
|
||||
id={`move-${moveIdx}`}
|
||||
>
|
||||
{color && (
|
||||
<Image
|
||||
src={`/icons/${moveClassification}.png`}
|
||||
alt="move-icon"
|
||||
width={14}
|
||||
height={14}
|
||||
style={{
|
||||
maxWidth: "3.5vw",
|
||||
maxHeight: "3.5vw",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Typography
|
||||
color={getMoveColor(moveClassification)}
|
||||
fontSize="0.9rem"
|
||||
lineHeight="0.9rem"
|
||||
>
|
||||
{san}
|
||||
</Typography>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
const getMoveColor = (moveClassification?: MoveClassification) => {
|
||||
if (
|
||||
!moveClassification ||
|
||||
moveClassificationsToIgnore.includes(moveClassification)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return moveClassificationColors[moveClassification];
|
||||
};
|
||||
|
||||
const moveClassificationsToIgnore: MoveClassification[] = [
|
||||
MoveClassification.Good,
|
||||
MoveClassification.Excellent,
|
||||
];
|
||||
@@ -0,0 +1,29 @@
|
||||
import { MoveClassification } from "@/types/enums";
|
||||
import { Grid, Typography } from "@mui/material";
|
||||
import MoveItem from "./moveItem";
|
||||
|
||||
interface Props {
|
||||
moves: { san: string; moveClassification?: MoveClassification }[];
|
||||
moveNb: number;
|
||||
}
|
||||
|
||||
export default function MovesLine({ moves, moveNb }: Props) {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
xs={12}
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Typography width="2rem" fontSize="0.9rem">
|
||||
{moveNb}.
|
||||
</Typography>
|
||||
|
||||
<MoveItem {...moves[0]} moveIdx={(moveNb - 1) * 2 + 1} />
|
||||
|
||||
<MoveItem {...moves[1]} moveIdx={(moveNb - 1) * 2 + 2} />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
39
src/sections/analysis/panelBody/graphTab/dot.tsx
Normal file
39
src/sections/analysis/panelBody/graphTab/dot.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { DotProps } from "recharts";
|
||||
import { ChartItemData } from "./types";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom, gameAtom } from "../../states";
|
||||
import { useChessActions } from "@/hooks/useChessActions";
|
||||
import { moveClassificationColors } from "@/lib/chess";
|
||||
|
||||
export default function CustomDot({
|
||||
cx,
|
||||
cy,
|
||||
r,
|
||||
payload,
|
||||
}: DotProps & { payload?: ChartItemData }) {
|
||||
const { goToMove } = useChessActions(boardAtom);
|
||||
const game = useAtomValue(gameAtom);
|
||||
|
||||
const handleDotClick = () => {
|
||||
if (!payload) return;
|
||||
goToMove(payload.moveNb, game);
|
||||
};
|
||||
|
||||
const moveColor = payload?.moveClassification
|
||||
? moveClassificationColors[payload.moveClassification]
|
||||
: "grey";
|
||||
|
||||
return (
|
||||
<circle
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={r}
|
||||
stroke={moveColor}
|
||||
strokeWidth={5}
|
||||
fill={moveColor}
|
||||
fillOpacity={1}
|
||||
onClick={handleDotClick}
|
||||
cursor="pointer"
|
||||
/>
|
||||
);
|
||||
}
|
||||
135
src/sections/analysis/panelBody/graphTab/index.tsx
Normal file
135
src/sections/analysis/panelBody/graphTab/index.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Box, Grid, GridProps } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
ReferenceLine,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { currentPositionAtom, gameEvalAtom } from "../../states";
|
||||
import { useMemo } from "react";
|
||||
import CustomTooltip from "./tooltip";
|
||||
import { ChartItemData } from "./types";
|
||||
import { PositionEval } from "@/types/eval";
|
||||
import { moveClassificationColors } from "@/lib/chess";
|
||||
import CustomDot from "./dot";
|
||||
|
||||
export default function GraphTab(props: GridProps) {
|
||||
const gameEval = useAtomValue(gameEvalAtom);
|
||||
const currentPosition = useAtomValue(currentPositionAtom);
|
||||
|
||||
const chartData: ChartItemData[] = useMemo(
|
||||
() => gameEval?.positions.map(formatEvalToChartData) ?? [],
|
||||
[gameEval]
|
||||
);
|
||||
|
||||
const boardMoveColor = currentPosition.eval?.moveClassification
|
||||
? moveClassificationColors[currentPosition.eval.moveClassification]
|
||||
: "grey";
|
||||
|
||||
if (!gameEval) return null;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
justifyContent="center"
|
||||
alignItems="start"
|
||||
height="100%"
|
||||
{...props}
|
||||
sx={
|
||||
props.hidden
|
||||
? { display: "none" }
|
||||
: { marginY: 1, overflow: "hidden", overflowY: "auto", ...props.sx }
|
||||
}
|
||||
>
|
||||
<Box
|
||||
width="max(35rem, 90%)"
|
||||
maxWidth="100%"
|
||||
height="max(8rem, 100%)"
|
||||
maxHeight="15rem"
|
||||
sx={{
|
||||
backgroundColor: "#2e2e2e",
|
||||
borderRadius: "15px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
width={500}
|
||||
height={400}
|
||||
data={chartData}
|
||||
margin={{ top: 0, left: 0, right: 0, bottom: 0 }}
|
||||
>
|
||||
<XAxis dataKey="moveNb" hide stroke="red" />
|
||||
<YAxis domain={[0, 20]} hide />
|
||||
<Tooltip
|
||||
content={<CustomTooltip />}
|
||||
isAnimationActive={false}
|
||||
cursor={{
|
||||
stroke: "grey",
|
||||
strokeWidth: 2,
|
||||
strokeOpacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke="none"
|
||||
fill="#ffffff"
|
||||
fillOpacity={1}
|
||||
activeDot={<CustomDot />}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<ReferenceLine
|
||||
y={10}
|
||||
stroke="grey"
|
||||
strokeWidth={2}
|
||||
strokeOpacity={0.4}
|
||||
/>
|
||||
<ReferenceLine
|
||||
x={currentPosition.currentMoveIdx}
|
||||
stroke={boardMoveColor}
|
||||
strokeWidth={4}
|
||||
strokeOpacity={0.6}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
const formatEvalToChartData = (
|
||||
position: PositionEval,
|
||||
index: number
|
||||
): ChartItemData => {
|
||||
const line = position.lines[0];
|
||||
|
||||
const chartItem: ChartItemData = {
|
||||
moveNb: index,
|
||||
value: 10,
|
||||
cp: line.cp,
|
||||
mate: line.mate,
|
||||
moveClassification: position.moveClassification,
|
||||
};
|
||||
|
||||
if (line.mate) {
|
||||
return {
|
||||
...chartItem,
|
||||
value: line.mate > 0 ? 20 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (line.cp) {
|
||||
return {
|
||||
...chartItem,
|
||||
value: Math.max(Math.min(line.cp / 100, 10), -10) + 10,
|
||||
};
|
||||
}
|
||||
|
||||
return chartItem;
|
||||
};
|
||||
27
src/sections/analysis/panelBody/graphTab/tooltip.tsx
Normal file
27
src/sections/analysis/panelBody/graphTab/tooltip.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { TooltipProps } from "recharts";
|
||||
import { ChartItemData } from "./types";
|
||||
import { getLineEvalLabel } from "@/lib/chess";
|
||||
|
||||
export default function CustomTooltip({
|
||||
active,
|
||||
payload,
|
||||
}: TooltipProps<number, number>) {
|
||||
if (!active || !payload?.length) return null;
|
||||
|
||||
const data = payload[0].payload as ChartItemData;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#f0f0f0",
|
||||
padding: 5,
|
||||
color: "black",
|
||||
opacity: 0.9,
|
||||
border: "1px solid black",
|
||||
borderRadius: 3,
|
||||
}}
|
||||
>
|
||||
{getLineEvalLabel(data)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
src/sections/analysis/panelBody/graphTab/types.ts
Normal file
9
src/sections/analysis/panelBody/graphTab/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { MoveClassification } from "@/types/enums";
|
||||
|
||||
export interface ChartItemData {
|
||||
moveNb: number;
|
||||
value: number;
|
||||
cp?: number;
|
||||
mate?: number;
|
||||
moveClassification?: MoveClassification;
|
||||
}
|
||||
Reference in New Issue
Block a user