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",
"react": "18.2.0",
"react-chessboard": "^4.6.0",
"react-dom": "18.2.0"
"react-dom": "18.2.0",
"recharts": "^2.12.7"
},
"devDependencies": {
"@types/node": "20.3.0",
@@ -1699,6 +1700,69 @@
"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": {
"version": "7.0.15",
"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",
"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": {
"version": "1.0.8",
"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": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
@@ -3405,6 +3596,12 @@
"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": {
"version": "3.1.3",
"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==",
"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": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -3947,6 +4153,15 @@
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
@@ -4507,6 +4722,12 @@
"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": {
"version": "4.3.0",
"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",
"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": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -5170,6 +5406,44 @@
"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": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
@@ -5738,6 +6012,12 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -5941,6 +6221,28 @@
"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": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",

View File

@@ -25,7 +25,8 @@
"next": "14.2.5",
"react": "18.2.0",
"react-chessboard": "^4.6.0",
"react-dom": "18.2.0"
"react-dom": "18.2.0",
"recharts": "^2.12.7"
},
"devDependencies": {
"@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 { Color, MoveClassification } from "@/types/enums";
import { Chess } from "chess.js";
import { getSquareRenderer, moveClassificationColors } from "./squareRenderer";
import { getSquareRenderer } from "./squareRenderer";
import { CurrentPosition } from "@/types/eval";
import EvaluationBar from "./evaluationBar";
import CapturedPieces from "./capturedPieces";
import { moveClassificationColors } from "@/lib/chess";
export interface Props {
id: string;

View File

@@ -7,6 +7,7 @@ import {
CustomSquareProps,
Square,
} from "react-chessboard/dist/chessboard/types";
import { moveClassificationColors } from "@/lib/chess";
export interface Props {
currentPositionAtom: PrimitiveAtom<CurrentPosition>;
@@ -74,18 +75,6 @@ export function getSquareRenderer({
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 = {
position: "absolute",
width: "100%",

View File

@@ -37,6 +37,15 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
const copyGame = useCallback(() => {
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());
return newGame;
}, [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 { Chess, PieceSymbol, Square } from "chess.js";
import { getPositionWinPercentage } from "./engine/helpers/winPercentage";
import { Color } from "@/types/enums";
import { Color, MoveClassification } from "@/types/enums";
export const getEvaluateGameParams = (game: Chess): EvaluateGameParams => {
const history = game.history({ verbose: true });
@@ -315,3 +315,29 @@ export const getCapturedPieces = (
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 Board from "@/sections/analysis/board";
import ReviewPanelBody from "@/sections/analysis/reviewPanelBody";
import ReviewPanelHeader from "@/sections/analysis/reviewPanelHeader";
import ReviewPanelToolBar from "@/sections/analysis/reviewPanelToolbar";
import PanelHeader from "@/sections/analysis/panelHeader";
import PanelToolBar from "@/sections/analysis/panelToolbar";
import AnalysisTab from "@/sections/analysis/panelBody/analysisTab";
import ClassificationTab from "@/sections/analysis/panelBody/classificationTab";
import {
boardAtom,
boardOrientationAtom,
gameAtom,
gameEvalAtom,
} 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 { useAtom, useSetAtom } from "jotai";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { useRouter } from "next/router";
import { useEffect } from "react";
import ClassificationPanel from "@/sections/analysis/reviewPanelBody/classificationPanel";
import { useEffect, useState } from "react";
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 [tab, setTab] = useState(0);
const isLgOrGreater = useMediaQuery(theme.breakpoints.up("lg"));
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const { reset: resetBoard } = useChessActions(boardAtom);
const { setPgn: setGamePgn } = useChessActions(gameAtom);
const [gameEval, setGameEval] = useAtom(gameEvalAtom);
const game = useAtomValue(gameAtom);
const setBoardOrientation = useSetAtom(boardOrientationAtom);
const router = useRouter();
@@ -37,13 +51,22 @@ export default function GameReport() {
}
}, [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 (
<Grid container gap={4} justifyContent="space-evenly" alignItems="center">
<Grid container gap={4} justifyContent="space-evenly" alignItems="start">
<Board />
<Grid
container
item
justifyContent="center"
alignItems="center"
borderRadius={2}
border={1}
borderColor={"secondary.main"}
@@ -59,52 +82,113 @@ export default function GameReport() {
style={{
maxWidth: "1200px",
}}
display="grid"
>
<Grid
container
item
justifyContent="center"
alignItems="center"
xs={12}
rowGap={2}
maxHeight={{ lg: "calc(95vh - 130px)", xs: "900px" }}
display="grid"
gridTemplateRows="repeat(4, auto) fit-content(100%)"
marginTop={isLgOrGreater && window.innerHeight > 780 ? 4 : 0}
>
{isLgOrGreater ? (
<>
<ReviewPanelHeader key="analysis-panel-header" />
<Divider sx={{ marginX: "5%" }} />
<ReviewPanelBody key="review-panel-body" />
<ClassificationPanel key="review-panel-class" />
<Divider sx={{ marginX: "5%" }} />
<ReviewPanelToolBar key="review-panel-toolbar" />
</>
<PanelHeader key="analysis-panel-header" />
) : (
<>
<ReviewPanelToolBar key="review-panel-toolbar" />
<PanelToolBar key="review-panel-toolbar" />
)}
<Divider sx={{ marginX: "5%" }} />
{!isLgOrGreater && !gameEval && <Divider sx={{ marginX: "5%" }} />}
{!isLgOrGreater && !gameEval && (
<PanelHeader key="analysis-panel-header" />
)}
{!gameEval && <ReviewPanelHeader key="analysis-panel-header" />}
{!gameEval && <Divider sx={{ marginX: "5%" }} />}
<Box
sx={{
borderBottom: 1,
borderColor: "divider",
marginX: { sm: "5%", xs: undefined },
}}
>
<Tabs
value={tab}
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
/>
<ReviewPanelBody key="review-panel-body" />
<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
/>
<ClassificationPanel key="review-panel-class" />
<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>
{gameEval && <Divider sx={{ marginX: "5%" }} />}
{gameEval && <ReviewPanelHeader key="analysis-panel-header" />}
</>
<AnalysisTab role="tabpanel" hidden={tab !== 0} id="tabContent0" />
<ClassificationTab
role="tabpanel"
hidden={tab !== 1}
id="tabContent1"
/>
<GraphTab role="tabpanel" hidden={tab !== 2} id="tabContent2" />
{isLgOrGreater && <Divider sx={{ marginX: "5%" }} />}
{isLgOrGreater && <PanelToolBar key="review-panel-toolbar" />}
{!isLgOrGreater && gameEval && <Divider sx={{ marginX: "5%" }} />}
{!isLgOrGreater && gameEval && (
<PanelHeader key="analysis-panel-header" />
)}
</Grid>
</Grid>
<EngineSettingsButton />
</Grid>
);
}

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

View File

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

View File

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