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:
GuillaumeSD
2024-07-24 11:58:42 +02:00
parent 9d5b088ae9
commit 2baf9b76ad
35 changed files with 754 additions and 156 deletions

View File

@@ -50,7 +50,13 @@ export const useCurrentPosition = (engineName?: EngineName) => {
setCurrentPosition(position);
if (!position.eval && engine?.isReady() && engineName) {
if (
!position.eval &&
engine?.isReady() &&
engineName &&
!board.isCheckmate() &&
!board.isStalemate()
) {
const getFenEngineEval = async (
fen: string,
setPartialEval?: (positionEval: PositionEval) => void

View File

@@ -1,6 +1,6 @@
import { Grid, Typography } from "@mui/material";
import { useAtomValue } from "jotai";
import { gameEvalAtom } from "../states";
import { gameEvalAtom } from "../../states";
export default function Accuracies() {
const gameEval = useAtomValue(gameEvalAtom);

View File

@@ -1,21 +1,19 @@
import { Icon } from "@iconify/react";
import { Grid, List, Typography } from "@mui/material";
import { Grid, GridProps, List, Typography } from "@mui/material";
import { useAtomValue } from "jotai";
import {
boardAtom,
engineMultiPvAtom,
engineNameAtom,
gameAtom,
} from "../states";
} from "../../states";
import LineEvaluation from "./lineEvaluation";
import { useCurrentPosition } from "../hooks/useCurrentPosition";
import { useCurrentPosition } from "../../hooks/useCurrentPosition";
import { LineEval } from "@/types/eval";
import EngineSettingsButton from "@/sections/engineSettings/engineSettingsButton";
import Accuracies from "./accuracies";
import MoveInfo from "./moveInfo";
import Opening from "./opening";
export default function ReviewPanelBody() {
export default function AnalysisTab(props: GridProps) {
const linesNumber = useAtomValue(engineMultiPvAtom);
const engineName = useAtomValue(engineNameAtom);
const position = useCurrentPosition(engineName);
@@ -45,41 +43,16 @@ export default function ReviewPanelBody() {
container
xs={12}
justifyContent="center"
alignItems="center"
alignItems="start"
height="100%"
rowGap={1.2}
{...props}
sx={
props.hidden
? { display: "none" }
: { overflow: "hidden", overflowY: "auto", ...props.sx }
}
>
<Grid
item
container
xs={12}
justifyContent="space-between"
alignItems="center"
>
<Grid item xs={1} />
<Grid
item
container
xs
justifyContent="center"
alignItems="center"
columnGap={1}
>
<Icon
icon="pepicons-pop:star-filled-circle"
color="#27f019"
height={25}
/>
<Typography variant="h6" align="center" lineHeight="25px">
Engine evaluation
</Typography>
</Grid>
<Grid item container xs={1} justifyContent="center">
<EngineSettingsButton />
</Grid>
</Grid>
<Accuracies />
<MoveInfo />

View File

@@ -1,8 +1,8 @@
import { LineEval } from "@/types/eval";
import { ListItem, Skeleton, Typography } from "@mui/material";
import { useAtomValue } from "jotai";
import { boardAtom } from "../states";
import { moveLineUciToSan } from "@/lib/chess";
import { boardAtom } from "../../states";
import { getLineEvalLabel, moveLineUciToSan } from "@/lib/chess";
interface Props {
line: LineEval;
@@ -10,12 +10,7 @@ interface Props {
export default function LineEvaluation({ line }: Props) {
const board = useAtomValue(boardAtom);
const lineLabel =
line.cp !== undefined
? `${line.cp > 0 ? "+" : ""}${(line.cp / 100).toFixed(2)}`
: line.mate
? `${line.mate > 0 ? "+" : "-"}M${Math.abs(line.mate)}`
: "?";
const lineLabel = getLineEvalLabel(line);
const isBlackCp =
(line.cp !== undefined && line.cp < 0) ||

View File

@@ -1,6 +1,6 @@
import { Grid, Typography } from "@mui/material";
import { useAtomValue } from "jotai";
import { boardAtom, currentPositionAtom } from "../states";
import { boardAtom, currentPositionAtom } from "../../states";
import { useMemo } from "react";
import { moveLineUciToSan } from "@/lib/chess";
import { MoveClassification } from "@/types/enums";

View File

@@ -1,4 +1,4 @@
import { useCurrentPosition } from "../hooks/useCurrentPosition";
import { useCurrentPosition } from "../../hooks/useCurrentPosition";
import { Grid, Typography } from "@mui/material";
export default function Opening() {

View 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>
);
}

View File

@@ -3,10 +3,10 @@ import { Grid, Typography } from "@mui/material";
import { useAtomValue } from "jotai";
import { boardAtom, gameAtom, gameEvalAtom } from "../../../states";
import { useMemo } from "react";
import { moveClassificationColors } from "@/components/board/squareRenderer";
import Image from "next/image";
import { capitalize } from "@/lib/helpers";
import { useChessActions } from "@/hooks/useChessActions";
import { moveClassificationColors } from "@/lib/chess";
interface Props {
classification: MoveClassification;

View File

@@ -1,12 +1,12 @@
import { MoveClassification } from "@/types/enums";
import { Grid, Typography } from "@mui/material";
import { moveClassificationColors } from "@/components/board/squareRenderer";
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;

View 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"
/>
);
}

View 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;
};

View 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>
);
}

View File

@@ -0,0 +1,9 @@
import { MoveClassification } from "@/types/enums";
export interface ChartItemData {
moveNb: number;
value: number;
cp?: number;
mate?: number;
moveClassification?: MoveClassification;
}

View File

@@ -7,7 +7,7 @@ import LinearProgressBar from "@/components/LinearProgressBar";
import { useAtomValue } from "jotai";
import { evaluationProgressAtom } from "../states";
export default function ReviewPanelHeader() {
export default function PanelHeader() {
const evaluationProgress = useAtomValue(evaluationProgressAtom);
return (
@@ -30,7 +30,7 @@ export default function ReviewPanelHeader() {
<Icon icon="streamline:clipboard-check" height={24} />
<Typography variant="h5" align="center">
Game Analysis
Game Review
</Typography>
</Grid>

View File

@@ -10,7 +10,7 @@ import SaveButton from "./saveButton";
import { useEffect } from "react";
import { getStartingFen } from "@/lib/chess";
export default function ReviewPanelToolBar() {
export default function PanelToolBar() {
const board = useAtomValue(boardAtom);
const { reset: resetBoard, undoMove: undoBoardMove } =
useChessActions(boardAtom);

View File

@@ -1,31 +0,0 @@
import { Divider, Grid } from "@mui/material";
import MovesPanel from "./movesPanel";
import MovesClassificationsRecap from "./movesClassificationsRecap";
import { useAtomValue } from "jotai";
import { gameAtom } from "../../states";
export default function ClassificationPanel() {
const game = useAtomValue(gameAtom);
if (!game.history().length) return null;
return (
<>
<Divider sx={{ marginX: "5%" }} />
<Grid
container
item
justifyContent="center"
alignItems="start"
height="100%"
minHeight={{ lg: "50px", xs: undefined }}
sx={{ overflow: "hidden" }}
>
<MovesPanel />
<MovesClassificationsRecap />
</Grid>
</>
);
}