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

304
package-lock.json generated
View File

@@ -23,7 +23,8 @@
"next": "14.2.5", "next": "14.2.5",
"react": "18.2.0", "react": "18.2.0",
"react-chessboard": "^4.6.0", "react-chessboard": "^4.6.0",
"react-dom": "18.2.0" "react-dom": "18.2.0",
"recharts": "^2.12.7"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.3.0", "@types/node": "20.3.0",
@@ -1699,6 +1700,69 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
"integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz",
"integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz",
"integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2430,6 +2494,127 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
}, },
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -2503,6 +2688,12 @@
} }
} }
}, },
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/deep-equal": { "node_modules/deep-equal": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
@@ -3405,6 +3596,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3416,6 +3613,15 @@
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
"dev": true "dev": true
}, },
"node_modules/fast-equals": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
"integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -3947,6 +4153,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/is-arguments": { "node_modules/is-arguments": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
@@ -4507,6 +4722,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.camelcase": { "node_modules/lodash.camelcase": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@@ -5155,6 +5376,21 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
}, },
"node_modules/react-smooth": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz",
"integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==",
"license": "MIT",
"dependencies": {
"fast-equals": "^5.0.1",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-transition-group": { "node_modules/react-transition-group": {
"version": "4.4.5", "version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -5170,6 +5406,44 @@
"react-dom": ">=16.6.0" "react-dom": ">=16.6.0"
} }
}, },
"node_modules/recharts": {
"version": "2.12.7",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz",
"integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
"eventemitter3": "^4.0.1",
"lodash": "^4.17.21",
"react-is": "^16.10.2",
"react-smooth": "^4.0.0",
"recharts-scale": "^0.4.4",
"tiny-invariant": "^1.3.1",
"victory-vendor": "^36.6.8"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/recharts-scale": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
"license": "MIT",
"dependencies": {
"decimal.js-light": "^2.4.1"
}
},
"node_modules/recharts/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/redux": { "node_modules/redux": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
@@ -5738,6 +6012,12 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true "dev": true
}, },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/to-fast-properties": { "node_modules/to-fast-properties": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -5941,6 +6221,28 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/victory-vendor": {
"version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/websocket-driver": { "node_modules/websocket-driver": {
"version": "0.7.4", "version": "0.7.4",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",

View File

@@ -25,7 +25,8 @@
"next": "14.2.5", "next": "14.2.5",
"react": "18.2.0", "react": "18.2.0",
"react-chessboard": "^4.6.0", "react-chessboard": "^4.6.0",
"react-dom": "18.2.0" "react-dom": "18.2.0",
"recharts": "^2.12.7"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.3.0", "@types/node": "20.3.0",

View File

@@ -11,10 +11,11 @@ import { useChessActions } from "@/hooks/useChessActions";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Color, MoveClassification } from "@/types/enums"; import { Color, MoveClassification } from "@/types/enums";
import { Chess } from "chess.js"; import { Chess } from "chess.js";
import { getSquareRenderer, moveClassificationColors } from "./squareRenderer"; import { getSquareRenderer } from "./squareRenderer";
import { CurrentPosition } from "@/types/eval"; import { CurrentPosition } from "@/types/eval";
import EvaluationBar from "./evaluationBar"; import EvaluationBar from "./evaluationBar";
import CapturedPieces from "./capturedPieces"; import CapturedPieces from "./capturedPieces";
import { moveClassificationColors } from "@/lib/chess";
export interface Props { export interface Props {
id: string; id: string;

View File

@@ -7,6 +7,7 @@ import {
CustomSquareProps, CustomSquareProps,
Square, Square,
} from "react-chessboard/dist/chessboard/types"; } from "react-chessboard/dist/chessboard/types";
import { moveClassificationColors } from "@/lib/chess";
export interface Props { export interface Props {
currentPositionAtom: PrimitiveAtom<CurrentPosition>; currentPositionAtom: PrimitiveAtom<CurrentPosition>;
@@ -74,18 +75,6 @@ export function getSquareRenderer({
return squareRenderer; return squareRenderer;
} }
export const moveClassificationColors: Record<MoveClassification, string> = {
[MoveClassification.Book]: "#d5a47d",
[MoveClassification.Brilliant]: "#26c2a3",
[MoveClassification.Great]: "#4099ed",
[MoveClassification.Best]: "#3aab18",
[MoveClassification.Excellent]: "#3aab18",
[MoveClassification.Good]: "#81b64c",
[MoveClassification.Inaccuracy]: "#f7c631",
[MoveClassification.Mistake]: "#ffa459",
[MoveClassification.Blunder]: "#fa412d",
};
const rightClickSquareStyle: CSSProperties = { const rightClickSquareStyle: CSSProperties = {
position: "absolute", position: "absolute",
width: "100%", width: "100%",

View File

@@ -37,6 +37,15 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
const copyGame = useCallback(() => { const copyGame = useCallback(() => {
const newGame = new Chess(); const newGame = new Chess();
if (game.history().length === 0) {
const pgnSplitted = game.pgn().split("]");
if (pgnSplitted.at(-1)?.includes("1-0")) {
newGame.loadPgn(pgnSplitted.slice(0, -1).join("]") + "]");
return newGame;
}
}
newGame.loadPgn(game.pgn()); newGame.loadPgn(game.pgn());
return newGame; return newGame;
}, [game]); }, [game]);

View File

@@ -1,8 +1,8 @@
import { EvaluateGameParams, PositionEval } from "@/types/eval"; import { EvaluateGameParams, LineEval, PositionEval } from "@/types/eval";
import { Game } from "@/types/game"; import { Game } from "@/types/game";
import { Chess, PieceSymbol, Square } from "chess.js"; import { Chess, PieceSymbol, Square } from "chess.js";
import { getPositionWinPercentage } from "./engine/helpers/winPercentage"; import { getPositionWinPercentage } from "./engine/helpers/winPercentage";
import { Color } from "@/types/enums"; import { Color, MoveClassification } from "@/types/enums";
export const getEvaluateGameParams = (game: Chess): EvaluateGameParams => { export const getEvaluateGameParams = (game: Chess): EvaluateGameParams => {
const history = game.history({ verbose: true }); const history = game.history({ verbose: true });
@@ -315,3 +315,29 @@ export const getCapturedPieces = (
return capturedPieces; return capturedPieces;
}; };
export const getLineEvalLabel = (
line: Pick<LineEval, "cp" | "mate">
): string => {
if (line.cp !== undefined) {
return `${line.cp > 0 ? "+" : ""}${(line.cp / 100).toFixed(2)}`;
}
if (line.mate) {
return `${line.mate > 0 ? "+" : "-"}M${Math.abs(line.mate)}`;
}
return "?";
};
export const moveClassificationColors: Record<MoveClassification, string> = {
[MoveClassification.Book]: "#d5a47d",
[MoveClassification.Brilliant]: "#26c2a3",
[MoveClassification.Great]: "#4099ed",
[MoveClassification.Best]: "#3aab18",
[MoveClassification.Excellent]: "#3aab18",
[MoveClassification.Good]: "#81b64c",
[MoveClassification.Inaccuracy]: "#f7c631",
[MoveClassification.Mistake]: "#ffa459",
[MoveClassification.Blunder]: "#fa412d",
};

View File

@@ -1,28 +1,42 @@
import { useChessActions } from "@/hooks/useChessActions"; import { useChessActions } from "@/hooks/useChessActions";
import Board from "@/sections/analysis/board"; import Board from "@/sections/analysis/board";
import ReviewPanelBody from "@/sections/analysis/reviewPanelBody"; import PanelHeader from "@/sections/analysis/panelHeader";
import ReviewPanelHeader from "@/sections/analysis/reviewPanelHeader"; import PanelToolBar from "@/sections/analysis/panelToolbar";
import ReviewPanelToolBar from "@/sections/analysis/reviewPanelToolbar"; import AnalysisTab from "@/sections/analysis/panelBody/analysisTab";
import ClassificationTab from "@/sections/analysis/panelBody/classificationTab";
import { import {
boardAtom, boardAtom,
boardOrientationAtom, boardOrientationAtom,
gameAtom, gameAtom,
gameEvalAtom, gameEvalAtom,
} from "@/sections/analysis/states"; } from "@/sections/analysis/states";
import { Divider, Grid, useMediaQuery, useTheme } from "@mui/material"; import {
Box,
Divider,
Grid,
Tab,
Tabs,
useMediaQuery,
useTheme,
} from "@mui/material";
import { Chess } from "chess.js"; import { Chess } from "chess.js";
import { useAtom, useSetAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import ClassificationPanel from "@/sections/analysis/reviewPanelBody/classificationPanel"; import { Icon } from "@iconify/react";
import EngineSettingsButton from "@/sections/engineSettings/engineSettingsButton";
import GraphTab from "@/sections/analysis/panelBody/graphTab";
export default function GameReport() { export default function GameReview() {
const theme = useTheme(); const theme = useTheme();
const [tab, setTab] = useState(0);
const isLgOrGreater = useMediaQuery(theme.breakpoints.up("lg")); const isLgOrGreater = useMediaQuery(theme.breakpoints.up("lg"));
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const { reset: resetBoard } = useChessActions(boardAtom); const { reset: resetBoard } = useChessActions(boardAtom);
const { setPgn: setGamePgn } = useChessActions(gameAtom); const { setPgn: setGamePgn } = useChessActions(gameAtom);
const [gameEval, setGameEval] = useAtom(gameEvalAtom); const [gameEval, setGameEval] = useAtom(gameEvalAtom);
const game = useAtomValue(gameAtom);
const setBoardOrientation = useSetAtom(boardOrientationAtom); const setBoardOrientation = useSetAtom(boardOrientationAtom);
const router = useRouter(); const router = useRouter();
@@ -37,13 +51,22 @@ export default function GameReport() {
} }
}, [gameId, setGameEval, setBoardOrientation, resetBoard, setGamePgn]); }, [gameId, setGameEval, setBoardOrientation, resetBoard, setGamePgn]);
const isGameLoaded = game.history().length > 0;
useEffect(() => {
if (tab === 1 && !isGameLoaded) setTab(0);
if (tab === 2 && !gameEval) setTab(0);
}, [isGameLoaded, gameEval, tab]);
return ( return (
<Grid container gap={4} justifyContent="space-evenly" alignItems="center"> <Grid container gap={4} justifyContent="space-evenly" alignItems="start">
<Board /> <Board />
<Grid <Grid
container container
item item
justifyContent="center"
alignItems="center"
borderRadius={2} borderRadius={2}
border={1} border={1}
borderColor={"secondary.main"} borderColor={"secondary.main"}
@@ -59,52 +82,113 @@ export default function GameReport() {
style={{ style={{
maxWidth: "1200px", maxWidth: "1200px",
}} }}
rowGap={2}
maxHeight={{ lg: "calc(95vh - 130px)", xs: "900px" }}
display="grid" display="grid"
gridTemplateRows="repeat(4, auto) fit-content(100%)"
marginTop={isLgOrGreater && window.innerHeight > 780 ? 4 : 0}
> >
<Grid {isLgOrGreater ? (
container <PanelHeader key="analysis-panel-header" />
item ) : (
justifyContent="center" <PanelToolBar key="review-panel-toolbar" />
alignItems="center" )}
xs={12}
rowGap={2} {!isLgOrGreater && !gameEval && <Divider sx={{ marginX: "5%" }} />}
maxHeight={{ lg: "calc(95vh - 130px)", xs: "900px" }} {!isLgOrGreater && !gameEval && (
display="grid" <PanelHeader key="analysis-panel-header" />
gridTemplateRows="repeat(4, auto) fit-content(100%)" )}
<Box
sx={{
borderBottom: 1,
borderColor: "divider",
marginX: { sm: "5%", xs: undefined },
}}
> >
{isLgOrGreater ? ( <Tabs
<> value={tab}
<ReviewPanelHeader key="analysis-panel-header" /> onChange={(_, newValue) => setTab(newValue)}
aria-label="basic tabs example"
variant="fullWidth"
>
<Tab
label="Analysis"
id="tab0"
icon={
<Icon
icon="mdi:magnify"
color="#27f019"
height={isMobile ? 15 : 20}
/>
}
iconPosition="start"
sx={{ textTransform: "none", minHeight: 20, paddingX: 0 }}
disableFocusRipple
/>
<Divider sx={{ marginX: "5%" }} /> <Tab
label="Moves"
id="tab1"
icon={
<Icon
icon="mdi:format-list-bulleted"
color="#27f019"
height={isMobile ? 15 : 20}
/>
}
iconPosition="start"
sx={{
textTransform: "none",
minHeight: 20,
display: isGameLoaded ? undefined : "none",
paddingX: 0,
}}
disableFocusRipple
/>
<ReviewPanelBody key="review-panel-body" /> <Tab
label="Graph"
id="tab2"
icon={
<Icon
icon="mdi:chart-line"
color="#27f019"
height={isMobile ? 15 : 20}
/>
}
iconPosition="start"
sx={{
textTransform: "none",
minHeight: 20,
display: gameEval ? undefined : "none",
paddingX: 0,
}}
disableFocusRipple
/>
</Tabs>
</Box>
<ClassificationPanel key="review-panel-class" /> <AnalysisTab role="tabpanel" hidden={tab !== 0} id="tabContent0" />
<Divider sx={{ marginX: "5%" }} /> <ClassificationTab
role="tabpanel"
hidden={tab !== 1}
id="tabContent1"
/>
<ReviewPanelToolBar key="review-panel-toolbar" /> <GraphTab role="tabpanel" hidden={tab !== 2} id="tabContent2" />
</>
) : (
<>
<ReviewPanelToolBar key="review-panel-toolbar" />
<Divider sx={{ marginX: "5%" }} /> {isLgOrGreater && <Divider sx={{ marginX: "5%" }} />}
{isLgOrGreater && <PanelToolBar key="review-panel-toolbar" />}
{!gameEval && <ReviewPanelHeader key="analysis-panel-header" />} {!isLgOrGreater && gameEval && <Divider sx={{ marginX: "5%" }} />}
{!gameEval && <Divider sx={{ marginX: "5%" }} />} {!isLgOrGreater && gameEval && (
<PanelHeader key="analysis-panel-header" />
<ReviewPanelBody key="review-panel-body" /> )}
<ClassificationPanel key="review-panel-class" />
{gameEval && <Divider sx={{ marginX: "5%" }} />}
{gameEval && <ReviewPanelHeader key="analysis-panel-header" />}
</>
)}
</Grid>
</Grid> </Grid>
<EngineSettingsButton />
</Grid> </Grid>
); );
} }

View File

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

View File

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

View File

@@ -1,21 +1,19 @@
import { Icon } from "@iconify/react"; import { Grid, GridProps, List, Typography } from "@mui/material";
import { Grid, List, Typography } from "@mui/material";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { import {
boardAtom, boardAtom,
engineMultiPvAtom, engineMultiPvAtom,
engineNameAtom, engineNameAtom,
gameAtom, gameAtom,
} from "../states"; } from "../../states";
import LineEvaluation from "./lineEvaluation"; import LineEvaluation from "./lineEvaluation";
import { useCurrentPosition } from "../hooks/useCurrentPosition"; import { useCurrentPosition } from "../../hooks/useCurrentPosition";
import { LineEval } from "@/types/eval"; import { LineEval } from "@/types/eval";
import EngineSettingsButton from "@/sections/engineSettings/engineSettingsButton";
import Accuracies from "./accuracies"; import Accuracies from "./accuracies";
import MoveInfo from "./moveInfo"; import MoveInfo from "./moveInfo";
import Opening from "./opening"; import Opening from "./opening";
export default function ReviewPanelBody() { export default function AnalysisTab(props: GridProps) {
const linesNumber = useAtomValue(engineMultiPvAtom); const linesNumber = useAtomValue(engineMultiPvAtom);
const engineName = useAtomValue(engineNameAtom); const engineName = useAtomValue(engineNameAtom);
const position = useCurrentPosition(engineName); const position = useCurrentPosition(engineName);
@@ -45,41 +43,16 @@ export default function ReviewPanelBody() {
container container
xs={12} xs={12}
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="start"
height="100%"
rowGap={1.2} 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 /> <Accuracies />
<MoveInfo /> <MoveInfo />

View File

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

View File

@@ -1,6 +1,6 @@
import { Grid, Typography } from "@mui/material"; import { Grid, Typography } from "@mui/material";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { boardAtom, currentPositionAtom } from "../states"; import { boardAtom, currentPositionAtom } from "../../states";
import { useMemo } from "react"; import { useMemo } from "react";
import { moveLineUciToSan } from "@/lib/chess"; import { moveLineUciToSan } from "@/lib/chess";
import { MoveClassification } from "@/types/enums"; 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"; import { Grid, Typography } from "@mui/material";
export default function Opening() { 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 { useAtomValue } from "jotai";
import { boardAtom, gameAtom, gameEvalAtom } from "../../../states"; import { boardAtom, gameAtom, gameEvalAtom } from "../../../states";
import { useMemo } from "react"; import { useMemo } from "react";
import { moveClassificationColors } from "@/components/board/squareRenderer";
import Image from "next/image"; import Image from "next/image";
import { capitalize } from "@/lib/helpers"; import { capitalize } from "@/lib/helpers";
import { useChessActions } from "@/hooks/useChessActions"; import { useChessActions } from "@/hooks/useChessActions";
import { moveClassificationColors } from "@/lib/chess";
interface Props { interface Props {
classification: MoveClassification; classification: MoveClassification;

View File

@@ -1,12 +1,12 @@
import { MoveClassification } from "@/types/enums"; import { MoveClassification } from "@/types/enums";
import { Grid, Typography } from "@mui/material"; import { Grid, Typography } from "@mui/material";
import { moveClassificationColors } from "@/components/board/squareRenderer";
import Image from "next/image"; import Image from "next/image";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { boardAtom, currentPositionAtom, gameAtom } from "../../../states"; import { boardAtom, currentPositionAtom, gameAtom } from "../../../states";
import { useChessActions } from "@/hooks/useChessActions"; import { useChessActions } from "@/hooks/useChessActions";
import { useEffect } from "react"; import { useEffect } from "react";
import { isInViewport } from "@/lib/helpers"; import { isInViewport } from "@/lib/helpers";
import { moveClassificationColors } from "@/lib/chess";
interface Props { interface Props {
san: string; 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 { useAtomValue } from "jotai";
import { evaluationProgressAtom } from "../states"; import { evaluationProgressAtom } from "../states";
export default function ReviewPanelHeader() { export default function PanelHeader() {
const evaluationProgress = useAtomValue(evaluationProgressAtom); const evaluationProgress = useAtomValue(evaluationProgressAtom);
return ( return (
@@ -30,7 +30,7 @@ export default function ReviewPanelHeader() {
<Icon icon="streamline:clipboard-check" height={24} /> <Icon icon="streamline:clipboard-check" height={24} />
<Typography variant="h5" align="center"> <Typography variant="h5" align="center">
Game Analysis Game Review
</Typography> </Typography>
</Grid> </Grid>

View File

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

View File

@@ -1,4 +1,4 @@
import { IconButton, Tooltip } from "@mui/material"; import { Fab } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import EngineSettingsDialog from "./engineSettingsDialog"; import EngineSettingsDialog from "./engineSettingsDialog";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
@@ -8,11 +8,21 @@ export default function EngineSettingsButton() {
return ( return (
<> <>
<Tooltip title="Engine settings"> <Fab
<IconButton onClick={() => setOpenDialog(true)} sx={{ paddingY: 0.3 }}> title="Engine settings"
<Icon icon="ri:settings-3-line" height={20} /> color="secondary"
</IconButton> size="small"
</Tooltip> sx={{
top: "auto",
right: 16,
bottom: 16,
left: "auto",
position: "fixed",
}}
onClick={() => setOpenDialog(true)}
>
<Icon icon="mdi:settings" height={20} />
</Fab>
<EngineSettingsDialog <EngineSettingsDialog
open={openDialog} open={openDialog}

View File

@@ -58,8 +58,7 @@ export default function EngineSettingsDialog({ open, onClose }: Props) {
<Typography> <Typography>
Stockfish 16 Lite (HCE) is the default engine. It offers the best Stockfish 16 Lite (HCE) is the default engine. It offers the best
balance between speed and strength. Stockfish 16 is the strongest balance between speed and strength. Stockfish 16 is the strongest
engine available, but please note that it requires a one time download engine available, note that it requires a one time download of 40MB.
of 40MB.
</Typography> </Typography>
<Grid <Grid
marginTop={4} marginTop={4}