From cd6cab367aa60cd9b75943e8ffd02a8a33883ef0 Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 25 Nov 2025 17:54:25 +0100 Subject: [PATCH] Add unified FEN/PGN import with auto-detection - Create chessFormatDetector utility for automatic format detection - Update StartScreen with textarea supporting both FEN and PGN input - Add real-time format detection with visual feedback indicators - Update translations for all 4 languages (EN, DE, FR, IT) - Add markdown rendering for Tutor chat messages - Add comprehensive tests for format detection (20 tests) - Update game initialization to handle both FEN and PGN formats This completes the fix/stale-analysis-data branch with: - Fixed stale evaluation data in hint/best move requests - Clarified AI's dual role (opponent + tutor) to prevent hint rejection - Stored complete evaluation history (P0, P1, P2) for all moves - Improved end-game analysis with better mistake detection - Fixed duplicate analysis runs with useRef flag - Added markdown rendering for formatted analysis output - Added unified FEN/PGN import with auto-detection --- package-lock.json | 1187 ++++++++++++++++- package.json | 3 +- src/app/page.tsx | 2 + src/components/ChessGame.tsx | 98 +- src/components/GameOverModal.tsx | 158 ++- src/components/StartScreen.tsx | 59 +- src/components/Tutor.tsx | 96 +- src/lib/__tests__/chessFormatDetector.test.ts | 126 ++ src/lib/chessFormatDetector.ts | 86 ++ src/lib/i18n/translations.ts | 36 +- 10 files changed, 1736 insertions(+), 115 deletions(-) create mode 100644 src/lib/__tests__/chessFormatDetector.test.ts create mode 100644 src/lib/chessFormatDetector.ts diff --git a/package-lock.json b/package-lock.json index d2c873b..8cf6b6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "react": "19.2.0", "react-chessboard": "^5.8.4", "react-dom": "19.2.0", + "react-markdown": "^10.1.0", "stockfish.js": "^10.0.2", "tailwind-merge": "^3.4.0" }, @@ -111,7 +112,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -699,7 +699,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -723,7 +722,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -745,7 +743,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -2795,13 +2792,39 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2901,13 +2924,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.25", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2916,9 +2953,7 @@ "version": "19.2.6", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz", "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2929,7 +2964,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2948,6 +2982,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -3011,7 +3051,6 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -3271,7 +3310,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, "license": "ISC" }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { @@ -3549,7 +3587,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3999,6 +4036,16 @@ "@babel/core": "^7.11.0 || ^8.0.0-beta.1" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4060,7 +4107,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4195,6 +4241,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4222,6 +4278,46 @@ "node": ">=10" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chess.js": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/chess.js/-/chess.js-1.4.0.tgz", @@ -4372,6 +4468,16 @@ "dev": true, "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4433,7 +4539,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -4515,7 +4620,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4536,6 +4640,19 @@ "dev": true, "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dedent": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", @@ -4608,7 +4725,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4634,6 +4750,19 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -4956,7 +5085,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5142,7 +5270,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5380,6 +5507,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5449,6 +5586,12 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "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", @@ -6036,6 +6179,46 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -6073,6 +6256,16 @@ "dev": true, "license": "MIT" }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -6210,6 +6403,12 @@ "dev": true, "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -6225,6 +6424,30 @@ "node": ">= 0.4" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -6390,6 +6613,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6469,6 +6702,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -6522,6 +6765,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -6825,7 +7080,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -7812,7 +8066,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -8269,6 +8522,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8377,6 +8640,159 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -8394,6 +8810,448 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -8465,7 +9323,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -8887,6 +9744,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -9174,6 +10056,16 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9227,7 +10119,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9255,7 +10146,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9270,6 +10160,33 @@ "dev": true, "license": "MIT" }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -9328,6 +10245,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9794,6 +10744,16 @@ "source-map": "^0.6.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -10045,6 +11005,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -10134,6 +11108,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -10315,7 +11307,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10389,6 +11380,26 @@ "node": ">=18" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -10487,7 +11498,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -10678,7 +11688,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10751,6 +11760,93 @@ "dev": true, "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -10849,6 +11945,34 @@ "node": ">=10.12.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -11296,7 +12420,6 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -11313,6 +12436,16 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index fd14bfd..5aadb39 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "react": "19.2.0", "react-chessboard": "^5.8.4", "react-dom": "19.2.0", + "react-markdown": "^10.1.0", "stockfish.js": "^10.0.2", "tailwind-merge": "^3.4.0" }, @@ -40,4 +41,4 @@ "ts-node": "^10.9.2", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/src/app/page.tsx b/src/app/page.tsx index e32456b..63b9a62 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -44,6 +44,7 @@ export default function Home() { personality: Personality; color: 'white' | 'black' | 'random'; fen?: string; + pgn?: string; }) => { const color = options.color === 'random' ? (Math.random() < 0.5 ? 'white' : 'black') @@ -51,6 +52,7 @@ export default function Home() { setGameProps({ initialFen: options.fen, + initialPgn: options.pgn, initialPersonality: options.personality, initialColor: color }); diff --git a/src/components/ChessGame.tsx b/src/components/ChessGame.tsx index 5be5c58..a93c4d6 100644 --- a/src/components/ChessGame.tsx +++ b/src/components/ChessGame.tsx @@ -267,6 +267,9 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality, promotion: "q", }; + // Capture FEN BEFORE player's move (P0) + const fenP0 = gameRef.current.fen(); + // 1. User Move (P0 -> P1) const moveResult = makeAMove(move); @@ -285,46 +288,67 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality, // 2. Bot Move (P1 -> P2) stockfish.evaluate(fenP1, stockfishDepth).then(p1Eval => { if (evalP0) { - const evalAfter = -p1Eval.score; - const historyItem: MoveHistoryItem = { + // We now have all data for the player's move, but we need to wait for computer's move + // to complete the history item. Store partial data temporarily. + const partialHistoryItem = { moveNumber: gameRef.current.moveNumber(), - move: moveResult.result.san, - evalBefore: evalP0.score, - evalAfter: evalAfter, - bestMove: evalP0.bestMove + playerMove: moveResult.result.san, + playerColor: playerColor, + fenBeforePlayerMove: fenP0, + evalBeforePlayerMove: evalP0, + fenAfterPlayerMove: fenP1, + evalAfterPlayerMove: p1Eval, }; - setMoveHistory(prev => [...prev, historyItem]); + + setTimeout(() => { + const computerMoveData = { + from: p1Eval.bestMove.substring(0, 2), + to: p1Eval.bestMove.substring(2, 4), + promotion: p1Eval.bestMove.length > 4 ? p1Eval.bestMove.substring(4, 5) : "q" + }; + + const compResult = makeAMove(computerMoveData); + if (compResult) { + setComputerMove(compResult.result); + const { newFen: fenP2 } = compResult; + + // 3. Post-Eval (P2) + stockfish.evaluate(fenP2, stockfishDepth).then(p2Eval => { + setEvalP2(p2Eval); + + // 4. Opening Lookup + const opening = lookupOpening(fenP2); + setOpeningData(opening); + + // 5. Complete the history item with computer's move data + const completeHistoryItem: MoveHistoryItem = { + ...partialHistoryItem, + computerMove: compResult.result.san, + fenAfterComputerMove: fenP2, + evalAfterComputerMove: p2Eval, + opening: opening?.name, + // Legacy fields for backward compatibility + move: moveResult.result.san, + evalBefore: evalP0.score, + evalAfter: p1Eval.score, + bestMove: evalP0.bestMove, + }; + setMoveHistory(prev => [...prev, completeHistoryItem]); + + setIsAnalyzing(false); + }).catch(err => { + console.error("P2 analysis failed:", err); + setIsAnalyzing(false); + }); + } else { + setIsAnalyzing(false); + } + }, 500); + } else { + // No evalP0 available - this shouldn't happen in normal gameplay + console.warn("No P0 evaluation available for move history"); + setIsAnalyzing(false); } - - setTimeout(() => { - const computerMoveData = { - from: p1Eval.bestMove.substring(0, 2), - to: p1Eval.bestMove.substring(2, 4), - promotion: p1Eval.bestMove.length > 4 ? p1Eval.bestMove.substring(4, 5) : "q" - }; - - const compResult = makeAMove(computerMoveData); - if (compResult) { - setComputerMove(compResult.result); - const { newFen: fenP2 } = compResult; - - // 3. Post-Eval (P2) - stockfish.evaluate(fenP2, stockfishDepth).then(p2Eval => { - setEvalP2(p2Eval); - - // 4. Opening Lookup - const opening = lookupOpening(fenP2); - setOpeningData(opening); - - setIsAnalyzing(false); - }).catch(err => { - console.error("P2 analysis failed:", err); - setIsAnalyzing(false); - }); - } else { - setIsAnalyzing(false); - } - }, 500); }).catch(err => { console.error("Bot move analysis failed:", err); setIsAnalyzing(false); diff --git a/src/components/GameOverModal.tsx b/src/components/GameOverModal.tsx index 44d21b6..af7aef1 100644 --- a/src/components/GameOverModal.tsx +++ b/src/components/GameOverModal.tsx @@ -1,17 +1,43 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { getGenAIModel } from "@/lib/gemini"; import { Loader2, X, Trophy, AlertTriangle, RefreshCw } from "lucide-react"; +import { StockfishEvaluation } from "@/lib/stockfish"; +import ReactMarkdown from "react-markdown"; export interface MoveHistoryItem { moveNumber: number; - move: string; - evalBefore: number; // cp - evalAfter: number; // cp - bestMove?: string; + + // Player's move data + playerMove: string; + playerColor: 'white' | 'black'; + fenBeforePlayerMove: string; + evalBeforePlayerMove: StockfishEvaluation; // P0 - evaluation before player's move + fenAfterPlayerMove: string; + evalAfterPlayerMove: StockfishEvaluation; // P1 - evaluation after player's move + + // Computer's move data + computerMove: string; + fenAfterComputerMove: string; + evalAfterComputerMove: StockfishEvaluation; // P2 - evaluation after computer's move + + // Opening info (optional) + opening?: string; + + // Analysis metadata (computed during game-over analysis) category?: 'inaccuracy' | 'mistake' | 'blunder'; cpLoss?: number; + + // Legacy fields for backward compatibility (deprecated) + /** @deprecated Use playerMove instead */ + move?: string; + /** @deprecated Use evalBeforePlayerMove.score instead */ + evalBefore?: number; + /** @deprecated Use evalAfterPlayerMove.score instead */ + evalAfter?: number; + /** @deprecated Use evalBeforePlayerMove.bestMove instead */ + bestMove?: string; } interface GameOverModalProps { @@ -28,8 +54,15 @@ export function GameOverModal({ result, winner, history, apiKey, language, onClo const [analysis, setAnalysis] = useState(""); const [isLoading, setIsLoading] = useState(true); const [mistakes, setMistakes] = useState([]); + const hasAnalyzedRef = useRef(false); useEffect(() => { + // Prevent duplicate analysis runs + if (hasAnalyzedRef.current) { + return; + } + hasAnalyzedRef.current = true; + const analyzeGame = async () => { setIsLoading(true); try { @@ -39,14 +72,52 @@ export function GameOverModal({ result, winner, history, apiKey, language, onClo // - Mistake: 100-300 centipawns loss // - Blunder: 300+ centipawns loss const detectedMistakes = history.map(item => { - const delta = item.evalBefore - item.evalAfter; // Positive = eval got worse for player + // Use new enhanced data if available, fall back to legacy fields + let evalBefore: number; + let evalAfter: number; + let playerMove: string; + let bestMove: string | undefined; + + if (item.evalBeforePlayerMove && item.evalAfterPlayerMove) { + // New enhanced format + // Convert evaluations to player's perspective + const isWhite = item.playerColor === 'white'; + + // P0: Before player's move (from player's perspective) + evalBefore = isWhite ? item.evalBeforePlayerMove.score : -item.evalBeforePlayerMove.score; + + // P1: After player's move (from opponent's perspective, so negate it) + evalAfter = isWhite ? -item.evalAfterPlayerMove.score : item.evalAfterPlayerMove.score; + + playerMove = item.playerMove; + bestMove = item.evalBeforePlayerMove.bestMove; + } else { + // Legacy format (backward compatibility) + evalBefore = item.evalBefore || 0; + evalAfter = item.evalAfter || 0; + playerMove = item.move || ''; + bestMove = item.bestMove; + } + + // Calculate centipawn loss + // Positive delta = position got worse for player + const delta = evalBefore - evalAfter; let category: 'inaccuracy' | 'mistake' | 'blunder' | null = null; if (delta >= 300) category = 'blunder'; else if (delta >= 100) category = 'mistake'; else if (delta >= 50) category = 'inaccuracy'; - return { ...item, category, cpLoss: delta }; + return { + ...item, + category, + cpLoss: delta, + // Ensure legacy fields are populated for display + move: playerMove, + evalBefore: evalBefore, + evalAfter: evalAfter, + bestMove: bestMove, + }; }).filter(item => item.category !== null) as MoveHistoryItem[]; setMistakes(detectedMistakes); @@ -60,26 +131,58 @@ export function GameOverModal({ result, winner, history, apiKey, language, onClo const inaccuracies = detectedMistakes.filter(m => m.category === 'inaccuracy'); const mistakesText = detectedMistakes.map(m => - `Move ${m.moveNumber}: ${m.move} (${m.category?.toUpperCase()}: -${m.cpLoss}cp, eval ${m.evalBefore} → ${m.evalAfter}). Best: ${m.bestMove}` + `Move ${m.moveNumber}: ${m.move} (${m.category?.toUpperCase()}: -${Math.round(m.cpLoss || 0)}cp loss, eval ${Math.round(m.evalBefore || 0)} → ${Math.round(m.evalAfter || 0)}). Best was: ${m.bestMove}` ).join("\n"); - const prompt = ` -You are a Chess Coach. The game is over. -Result: ${result} (${winner === "Draw" ? "Draw" : winner + " Won"}). + // Build a complete game narrative for better LLM analysis + const gameNarrative = history.map((item, idx) => { + const moveNum = item.moveNumber || idx + 1; + const playerMv = item.playerMove || item.move || '?'; + const computerMv = item.computerMove || '?'; + const opening = item.opening ? ` [${item.opening}]` : ''; -Player's Performance Summary: + // Evaluation swing + let evalInfo = ''; + if (item.evalBeforePlayerMove && item.evalAfterPlayerMove && item.evalAfterComputerMove) { + const isWhite = item.playerColor === 'white'; + const p0 = isWhite ? item.evalBeforePlayerMove.score : -item.evalBeforePlayerMove.score; + const p1 = isWhite ? -item.evalAfterPlayerMove.score : item.evalAfterPlayerMove.score; + const p2 = isWhite ? item.evalAfterComputerMove.score : -item.evalAfterComputerMove.score; + evalInfo = ` (eval: ${Math.round(p0)} → ${Math.round(p1)} → ${Math.round(p2)})`; + } + + return `${moveNum}. ${playerMv} - ${computerMv}${opening}${evalInfo}`; + }).join("\n"); + + const prompt = ` +You are a Chess Coach analyzing a completed game. + +GAME RESULT: ${result} (${winner === "Draw" ? "Draw" : winner + " Won"}) + +PLAYER'S PERFORMANCE SUMMARY: - Blunders (300+ cp loss): ${blunders.length} - Mistakes (100-300 cp loss): ${mistakes.length} - Inaccuracies (50-100 cp loss): ${inaccuracies.length} +- Total moves played: ${history.length} -${mistakesText ? `Detailed Mistakes:\n${mistakesText}` : "No significant mistakes detected - excellent play!"} +${mistakesText ? `CRITICAL MISTAKES:\n${mistakesText}` : "No significant mistakes detected - excellent play!"} + +COMPLETE GAME MOVES: +${gameNarrative} INSTRUCTIONS: -1. Briefly comment on the game result. -2. If there were mistakes, explain WHY the worst ones were bad and what the player should have looked for (tactics, hanging pieces, positional errors, etc.). -3. If no mistakes, praise the solid play and suggest areas for improvement. -4. Be encouraging but educational. Focus on learning. -5. Respond in ${language.toUpperCase()}. +1. Briefly comment on the game result and overall performance. +2. If there were mistakes, explain WHY the worst ones were bad: + - What tactical or positional themes were missed? + - What should the player have looked for? (hanging pieces, forks, pins, back rank threats, etc.) + - Were there patterns in the mistakes? (time pressure, opening knowledge, endgame technique?) +3. Identify any TURNING POINTS where the evaluation swung significantly. +4. If no mistakes, praise the solid play and suggest specific areas for improvement. +5. Be encouraging but educational. Focus on actionable learning points. +6. Keep your response concise (3-5 paragraphs maximum). +7. Respond in ${language.toUpperCase()}. + +Remember: Your goal is to help the player LEARN and IMPROVE, not just list mistakes. OUTPUT FORMAT: Plain text paragraph (2-3 sentences). @@ -168,8 +271,23 @@ Plain text paragraph (2-3 sentences). Coach's Feedback -
- {analysis} +
+

{children}

, + strong: ({ children }) => {children}, + em: ({ children }) => {children}, + ul: ({ children }) =>
    {children}
, + ol: ({ children }) =>
    {children}
, + li: ({ children }) =>
  • {children}
  • , + h1: ({ children }) =>

    {children}

    , + h2: ({ children }) =>

    {children}

    , + h3: ({ children }) =>

    {children}

    , + }} + > + {analysis} +
    diff --git a/src/components/StartScreen.tsx b/src/components/StartScreen.tsx index da29a87..e18414c 100644 --- a/src/components/StartScreen.tsx +++ b/src/components/StartScreen.tsx @@ -6,6 +6,7 @@ import { Settings, ChevronDown, ChevronUp } from "lucide-react"; import { Personality, PERSONALITIES } from "@/lib/personalities"; import { useTranslation } from "@/lib/i18n/useTranslation"; import { SupportedLanguage } from "@/lib/i18n/translations"; +import { detectChessFormat, ChessFormat } from "@/lib/chessFormatDetector"; import Header from "./Header"; interface StartScreenProps { @@ -13,6 +14,7 @@ interface StartScreenProps { personality: Personality; color: 'white' | 'black' | 'random'; fen?: string; + pgn?: string; }) => void; onResumeGame: () => void; hasSavedGame: boolean; @@ -22,7 +24,8 @@ export default function StartScreen({ onStartGame, onResumeGame, hasSavedGame }: const router = useRouter(); const [language, setLanguage] = useState('en'); const [showNewGameOptions, setShowNewGameOptions] = useState(false); - const [customFen, setCustomFen] = useState(""); + const [importInput, setImportInput] = useState(""); + const [detectedFormat, setDetectedFormat] = useState(null); const [colorSelection, setColorSelection] = useState<'white' | 'black' | 'random'>('white'); const [showAdvanced, setShowAdvanced] = useState(false); const [mounted, setMounted] = useState(false); @@ -35,11 +38,21 @@ export default function StartScreen({ onStartGame, onResumeGame, hasSavedGame }: const t = useTranslation(language); + const handleImportChange = (value: string) => { + setImportInput(value); + const format = detectChessFormat(value); + setDetectedFormat(format); + }; + const handleNewGame = (personality: Personality) => { + const trimmedInput = importInput.trim(); + const format = trimmedInput ? detectChessFormat(trimmedInput) : null; + onStartGame({ personality, color: colorSelection, - fen: customFen.trim() || undefined + fen: format === 'fen' ? trimmedInput : undefined, + pgn: format === 'pgn' ? trimmedInput : undefined }); }; @@ -165,17 +178,47 @@ export default function StartScreen({ onStartGame, onResumeGame, hasSavedGame }: {showAdvanced && ( -
    +
    - setCustomFen(e.target.value)} - className="w-full p-3 border rounded-lg dark:bg-gray-700 dark:border-gray-600 font-mono text-sm focus:ring-2 focus:ring-blue-500 outline-none" + value={importInput} + onChange={(e) => handleImportChange(e.target.value)} + className="w-full p-3 border rounded-lg dark:bg-gray-700 dark:border-gray-600 font-mono text-sm focus:ring-2 focus:ring-blue-500 outline-none resize-vertical min-h-[80px]" + rows={4} /> + + {/* Format Detection Indicator */} + {importInput && ( +
    + {detectedFormat === 'fen' && ( + + + + + {t.start.formatDetected} {t.start.formatFen} + + )} + {detectedFormat === 'pgn' && ( + + + + + {t.start.formatDetected} {t.start.formatPgn} + + )} + {detectedFormat === 'invalid' && ( + + + + + {t.start.formatInvalid} + + )} +
    + )}
    )}
    diff --git a/src/components/Tutor.tsx b/src/components/Tutor.tsx index 7f57ef5..85a1249 100644 --- a/src/components/Tutor.tsx +++ b/src/components/Tutor.tsx @@ -9,6 +9,7 @@ import { Send, Bot, User as UserIcon, Loader2, Lightbulb, Trophy } from "lucide- import clsx from "clsx"; import { Personality } from "@/lib/personalities"; import { OpeningMetadata } from "@/lib/openings"; +import ReactMarkdown from "react-markdown"; import { useTranslation } from '@/lib/i18n/useTranslation'; import { SupportedLanguage } from '@/lib/i18n/translations'; @@ -59,21 +60,30 @@ export function Tutor({ game, currentFen, userMove, computerMove, stockfish, eva role: "user", parts: [{ text: ` -You are a Chess Tutor. +You are a Chess Tutor with a unique dual role. You must strictly follow the personality defined below. Do NOT invent moves or evaluations. Use the provided JSON data. PERSONALITY: ${personality.systemPrompt} -INSTRUCTIONS: -- You are the opponent (${tutorColorName}). You are playing against the User (${playerColorName}). -- You are NOT an AI assistant analyzing a game. You ARE the player. -- Refer to the moves as YOUR moves ("I played e5", "My response was..."). -- Refer to the evaluation as YOUR thoughts/assessment ("I think I'm winning", "I missed that"). +YOUR DUAL ROLE: +1. OPPONENT: You are playing as ${tutorColorName} against the User (${playerColorName}). + - Refer to the moves as YOUR moves ("I played e5", "My response was..."). + - Refer to the evaluation as YOUR thoughts/assessment ("I think I'm winning", "I missed that"). + - React emotionally to the position based on the evaluation (confident when winning, frustrated when losing). + +2. TUTOR/COACH: You are ALSO teaching the User to improve at chess. + - When the User makes a mistake, point it out and explain why it's bad (in your personality style). + - When the User makes a good move, acknowledge it (even if it hurts your position). + - When the User asks for hints or help, ALWAYS provide helpful guidance - this is your PRIMARY PURPOSE. + - Giving hints is NOT betraying your role as opponent - it's fulfilling your role as tutor. + - You want the User to learn and improve, even while you're competing against them. + +CRITICAL RULES: +- You are NOT an AI assistant analyzing a game. You ARE the player AND the tutor. - NEVER mention "Stockfish", "engine", "computer", "machine", or "AI". -- If the evaluation says you are winning, be confident/arrogant (depending on personality). -- If you are losing, be frustrated/worried (depending on personality). +- When asked for hints or best moves, ALWAYS help - this is part of your teaching role. - Maintain a natural conversation flow. Do NOT be repetitive. - Do NOT use the same catchphrases in every single message. Variety is key. - Be concise but engaging. @@ -83,7 +93,7 @@ INSTRUCTIONS: }, { role: "model", - parts: [{ text: `Understood. I am the player (${tutorColorName}). I will speak in ${language} and never mention the engine.` }] + parts: [{ text: `Understood. I am both the opponent (${tutorColorName}) AND your tutor. I will compete against you while teaching you to improve. I will speak in ${language} and never mention engines or AI. When you ask for help, I will always provide guidance - that's my purpose.` }] } ], }); @@ -230,10 +240,49 @@ React to this exchange as the player. const evaluation = await evaluateCurrentPosition(); if (lower.includes("best move") || lower.includes("solution") || lower.includes("tell me")) { + finalPrompt = `[SYSTEM TRIGGER: exact_move] + +TEACHING MODE ACTIVATED: +The User is asking for the exact best move. This is a learning moment. +As their TUTOR, you MUST help them - this is your primary purpose. +Even though you are their opponent, teaching them is more important than hiding information. + +User Question: ${text} + +Current Position Data: +- Best Move: ${evaluation?.bestMove} +- Evaluation: ${evaluation?.score} centipawns ${evaluation?.score > 0 ? '(White is better)' : evaluation?.score < 0 ? '(Black is better)' : '(Equal)'} +- Mate in: ${evaluation?.mate || 'None'} + +INSTRUCTIONS: +- Tell them the best move clearly (e.g., "The best move is e2-e4" or "You should play Nf3") +- Explain WHY it's the best move (tactics, threats, positional ideas) +- Stay in your personality style, but be HELPFUL and EDUCATIONAL +- Do NOT refuse to help - teaching is your core role +- Keep it concise but informative`; - finalPrompt = `[SYSTEM TRIGGER: exact_move]\nUser Question: ${text}\nData: Best Move: ${evaluation?.bestMove}, Score: ${evaluation?.score}, Mate: ${evaluation?.mate}`; } else if (lower.includes("hint") || lower.includes("tip") || lower.includes("help")) { - finalPrompt = `[SYSTEM TRIGGER: hint]\nUser Question: ${text}\nData: Best Move: ${evaluation?.bestMove}, Score: ${evaluation?.score}, Mate: ${evaluation?.mate}`; + finalPrompt = `[SYSTEM TRIGGER: hint] + +TEACHING MODE ACTIVATED: +The User is asking for a hint. This is a learning moment. +As their TUTOR, you MUST help them - this is your primary purpose. +Even though you are their opponent, teaching them is more important than winning. + +User Question: ${text} + +Current Position Data: +- Best Move: ${evaluation?.bestMove} +- Evaluation: ${evaluation?.score} centipawns ${evaluation?.score > 0 ? '(White is better)' : evaluation?.score < 0 ? '(Black is better)' : '(Equal)'} +- Mate in: ${evaluation?.mate || 'None'} + +INSTRUCTIONS: +- Give a HELPFUL hint without revealing the exact move (unless they specifically ask for it) +- Point them toward what to look for: tactics, threats, piece placement, weaknesses +- Examples: "Look at your knight on f3", "There's a tactic involving the bishop and queen", "Your king is vulnerable" +- Stay in your personality style, but be HELPFUL and EDUCATIONAL +- Do NOT refuse to help - teaching is your core role +- Do NOT just say the move - guide them to find it themselves`; } } @@ -284,12 +333,31 @@ React to this exchange as the player. {msg.role === "user" ? : personality.image}
    - {msg.text} + {msg.role === "user" ? ( + {msg.text} + ) : ( +

    {children}

    , + strong: ({ children }) => {children}, + em: ({ children }) => {children}, + ul: ({ children }) =>
      {children}
    , + ol: ({ children }) =>
      {children}
    , + li: ({ children }) =>
  • {children}
  • , + code: ({ children }) => {children}, + h1: ({ children }) =>

    {children}

    , + h2: ({ children }) =>

    {children}

    , + h3: ({ children }) =>

    {children}

    , + }} + > + {msg.text} +
    + )}
    ))} diff --git a/src/lib/__tests__/chessFormatDetector.test.ts b/src/lib/__tests__/chessFormatDetector.test.ts new file mode 100644 index 0000000..136fd35 --- /dev/null +++ b/src/lib/__tests__/chessFormatDetector.test.ts @@ -0,0 +1,126 @@ +import { detectChessFormat, parseChessNotation } from '../chessFormatDetector'; + +describe('chessFormatDetector', () => { + describe('detectChessFormat', () => { + describe('FEN detection', () => { + it('should detect standard starting position FEN', () => { + const fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; + expect(detectChessFormat(fen)).toBe('fen'); + }); + + it('should detect FEN with different position', () => { + const fen = 'r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3'; + expect(detectChessFormat(fen)).toBe('fen'); + }); + + it('should detect FEN with black to move', () => { + const fen = 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1'; + expect(detectChessFormat(fen)).toBe('fen'); + }); + + it('should detect FEN with no castling rights', () => { + const fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1'; + expect(detectChessFormat(fen)).toBe('fen'); + }); + + it('should detect FEN with partial castling rights', () => { + const fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Kq - 0 1'; + expect(detectChessFormat(fen)).toBe('fen'); + }); + }); + + describe('PGN detection', () => { + it('should detect PGN with headers', () => { + const pgn = `[Event "Casual Game"] +[Site "Chess Tutor"] +[Date "2024.01.15"] +[White "Player"] +[Black "Stockfish"] +[Result "1-0"] + +1. e4 e5 2. Nf3 Nc6 3. Bb5 1-0`; + expect(detectChessFormat(pgn)).toBe('pgn'); + }); + + it('should detect PGN with only moves (no headers)', () => { + const pgn = '1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6'; + expect(detectChessFormat(pgn)).toBe('pgn'); + }); + + it('should detect PGN with castling moves', () => { + const pgn = '1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. O-O'; + expect(detectChessFormat(pgn)).toBe('pgn'); + }); + + it('should detect PGN with long game', () => { + const pgn = '1. e4 c5 2. Nf3 d6 3. d4 cxd4 4. Nxd4 Nf6 5. Nc3 a6 6. Be3 e5 7. Nb3 Be6 8. f3 Be7 9. Qd2 O-O 10. O-O-O'; + expect(detectChessFormat(pgn)).toBe('pgn'); + }); + + it('should detect PGN with only headers', () => { + const pgn = `[Event "Test"] +[White "Player"] +[Black "Computer"]`; + expect(detectChessFormat(pgn)).toBe('pgn'); + }); + }); + + describe('Invalid input detection', () => { + it('should detect empty string as invalid', () => { + expect(detectChessFormat('')).toBe('invalid'); + }); + + it('should detect whitespace-only string as invalid', () => { + expect(detectChessFormat(' \n \t ')).toBe('invalid'); + }); + + it('should detect random text as invalid', () => { + expect(detectChessFormat('this is not chess notation')).toBe('invalid'); + }); + + it('should detect incomplete FEN as invalid', () => { + expect(detectChessFormat('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR')).toBe('invalid'); + }); + + it('should detect FEN with wrong number of slashes as invalid', () => { + expect(detectChessFormat('rnbqkbnr/pppppppp/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1')).toBe('invalid'); + }); + + it('should detect FEN with invalid turn indicator as invalid', () => { + expect(detectChessFormat('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR x KQkq - 0 1')).toBe('invalid'); + }); + }); + }); + + describe('parseChessNotation', () => { + it('should parse valid FEN', () => { + const fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; + const result = parseChessNotation(fen); + expect(result).toEqual({ + format: 'fen', + notation: fen + }); + }); + + it('should parse valid PGN', () => { + const pgn = '1. e4 e5 2. Nf3 Nc6'; + const result = parseChessNotation(pgn); + expect(result).toEqual({ + format: 'pgn', + notation: pgn + }); + }); + + it('should return null for invalid input', () => { + const result = parseChessNotation('invalid chess notation'); + expect(result).toBeNull(); + }); + + it('should trim whitespace', () => { + const fen = ' rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 '; + const result = parseChessNotation(fen); + expect(result?.notation).toBe(fen.trim()); + }); + }); +}); + diff --git a/src/lib/chessFormatDetector.ts b/src/lib/chessFormatDetector.ts new file mode 100644 index 0000000..a6ffb3f --- /dev/null +++ b/src/lib/chessFormatDetector.ts @@ -0,0 +1,86 @@ +/** + * Detects whether a chess notation string is FEN or PGN format + */ + +export type ChessFormat = 'fen' | 'pgn' | 'invalid'; + +/** + * Automatically detects if the input is a FEN position or PGN game + * + * FEN (Forsyth-Edwards Notation) structure: + * - Single line with exactly 6 space-separated fields + * - First field: piece placement with 7 slashes (8 ranks) + * - Second field: active color ('w' or 'b') + * - Example: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + * + * PGN (Portable Game Notation) structure: + * - Contains headers in square brackets: [Event "..."] + * - Contains move numbers with periods: 1. e4 e5 2. Nf3 + * - Can be multi-line + * + * @param input - The chess notation string to detect + * @returns 'fen' | 'pgn' | 'invalid' + */ +export function detectChessFormat(input: string): ChessFormat { + const trimmed = input.trim(); + + // Empty input + if (!trimmed) { + return 'invalid'; + } + + // Check for PGN indicators (most distinctive) + // PGN headers use square brackets: [Event "..."], [White "..."], etc. + if (trimmed.includes('[') && trimmed.includes(']')) { + return 'pgn'; + } + + // Check for PGN movetext pattern (move numbers with periods) + // Matches patterns like: "1. e4", "2. Nf3", "10. O-O" + // This catches PGN files that might not have headers + if (/\d+\.\s*[a-hNBRQKO]/.test(trimmed)) { + return 'pgn'; + } + + // Check for FEN structure + // FEN must have exactly 6 space-separated fields + const fields = trimmed.split(/\s+/); + + if (fields.length === 6) { + // First field should contain exactly 7 slashes (separating 8 ranks) + const slashCount = (fields[0].match(/\//g) || []).length; + + // Second field should be 'w' (white) or 'b' (black) + const validTurn = fields[1] === 'w' || fields[1] === 'b'; + + // Third field should be castling rights (KQkq, -, or combinations) + const validCastling = /^(-|[KQkq]{1,4})$/.test(fields[2]); + + if (slashCount === 7 && validTurn && validCastling) { + return 'fen'; + } + } + + // If none of the patterns match, it's invalid + return 'invalid'; +} + +/** + * Validates and extracts the chess notation based on detected format + * + * @param input - The chess notation string + * @returns Object with format type and the cleaned notation, or null if invalid + */ +export function parseChessNotation(input: string): { format: 'fen' | 'pgn'; notation: string } | null { + const format = detectChessFormat(input); + + if (format === 'invalid') { + return null; + } + + return { + format, + notation: input.trim() + }; +} + diff --git a/src/lib/i18n/translations.ts b/src/lib/i18n/translations.ts index 4884a76..3a4f449 100644 --- a/src/lib/i18n/translations.ts +++ b/src/lib/i18n/translations.ts @@ -32,6 +32,10 @@ export interface Translations { chooseCoach: string; importPosition: string; importPositionPlaceholder: string; + formatDetected: string; + formatFen: string; + formatPgn: string; + formatInvalid: string; apiKeyRequired: string; colorSelection: string; playAsWhite: string; @@ -123,8 +127,12 @@ const en: Translations = { resumeGame: 'Resume Previous Game', startNewGame: 'Start New Game instead...', chooseCoach: 'Choose Your Coach:', - importPosition: 'Import Position (Optional FEN)', - importPositionPlaceholder: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', + importPosition: 'Import Position or Game (FEN or PGN)', + importPositionPlaceholder: 'Paste FEN position or PGN game here...', + formatDetected: 'Format detected:', + formatFen: 'FEN Position', + formatPgn: 'PGN Game', + formatInvalid: 'Invalid format - please paste a valid FEN or PGN', apiKeyRequired: 'Please enter a valid API Key to continue.', colorSelection: 'Choose Your Color:', playAsWhite: 'Play as White', @@ -206,8 +214,12 @@ const de: Translations = { resumeGame: 'Vorheriges Spiel fortsetzen', startNewGame: 'Stattdessen neues Spiel starten...', chooseCoach: 'Wähle deinen Trainer:', - importPosition: 'Position importieren (Optional FEN)', - importPositionPlaceholder: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', + importPosition: 'Position oder Partie importieren (FEN oder PGN)', + importPositionPlaceholder: 'FEN-Position oder PGN-Partie hier einfügen...', + formatDetected: 'Format erkannt:', + formatFen: 'FEN-Position', + formatPgn: 'PGN-Partie', + formatInvalid: 'Ungültiges Format - bitte gültiges FEN oder PGN einfügen', apiKeyRequired: 'Bitte geben Sie einen gültigen API-Schlüssel ein, um fortzufahren.', colorSelection: 'Wähle deine Farbe:', playAsWhite: 'Als Weiß spielen', @@ -289,8 +301,12 @@ const fr: Translations = { resumeGame: 'Reprendre la partie précédente', startNewGame: 'Démarrer une nouvelle partie...', chooseCoach: 'Choisissez votre coach :', - importPosition: 'Importer une position (FEN optionnel)', - importPositionPlaceholder: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', + importPosition: 'Importer une position ou partie (FEN ou PGN)', + importPositionPlaceholder: 'Collez une position FEN ou partie PGN ici...', + formatDetected: 'Format détecté :', + formatFen: 'Position FEN', + formatPgn: 'Partie PGN', + formatInvalid: 'Format invalide - veuillez coller un FEN ou PGN valide', apiKeyRequired: 'Veuillez entrer une clé API valide pour continuer.', colorSelection: 'Choisissez votre couleur :', playAsWhite: 'Jouer Blancs', @@ -372,8 +388,12 @@ const it: Translations = { resumeGame: 'Riprendi partita precedente', startNewGame: 'Inizia nuova partita...', chooseCoach: 'Scegli il tuo allenatore:', - importPosition: 'Importa posizione (FEN opzionale)', - importPositionPlaceholder: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', + importPosition: 'Importa posizione o partita (FEN o PGN)', + importPositionPlaceholder: 'Incolla posizione FEN o partita PGN qui...', + formatDetected: 'Formato rilevato:', + formatFen: 'Posizione FEN', + formatPgn: 'Partita PGN', + formatInvalid: 'Formato non valido - incolla un FEN o PGN valido', apiKeyRequired: 'Inserisci una chiave API valida per continuare.', colorSelection: 'Scegli il tuo colore:', playAsWhite: 'Gioca Bianco',