diff --git a/package-lock.json b/package-lock.json index 3cedb88..2db7e56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,10 @@ "playwright-test": "^14.1.12", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-markdown": "^10.1.0", "react-router-dom": "^7.10.1", - "recharts": "^3.4.1" + "recharts": "^3.4.1", + "remark-gfm": "^4.0.1" }, "devDependencies": { "@playwright/test": "^1.57.0", @@ -1494,19 +1496,60 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "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", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "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": "22.19.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", @@ -1517,12 +1560,34 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "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/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", @@ -1754,6 +1819,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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", @@ -2027,6 +2102,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", @@ -2073,6 +2158,46 @@ "node": ">=8" } }, + "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/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2229,6 +2354,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "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/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2346,6 +2481,13 @@ "node": ">=4" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT", + "peer": true + }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -2567,6 +2709,19 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "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/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2601,6 +2756,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2879,6 +3056,16 @@ "node": ">=0.8.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/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -3505,6 +3692,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/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -3517,6 +3744,16 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "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/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -3575,6 +3812,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "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", @@ -3598,6 +3841,30 @@ "node": ">=12" } }, + "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-arguments": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", @@ -3760,6 +4027,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", @@ -3826,6 +4103,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-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -4334,6 +4621,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4380,6 +4677,16 @@ "node": ">=10" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/matchit": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/matchit/-/matchit-1.1.0.tgz", @@ -4401,6 +4708,288 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.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/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.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-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.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-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@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-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/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -4440,6 +5029,569 @@ "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-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", @@ -5069,6 +6221,31 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "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": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -5998,6 +7175,16 @@ "node": ">= 0.6.0" } }, + "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/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6047,6 +7234,33 @@ "license": "MIT", "peer": true }, + "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/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", @@ -6266,6 +7480,72 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.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", @@ -6737,6 +8017,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/spawn-command": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", @@ -6952,6 +8242,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", @@ -7010,6 +8314,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/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -7293,6 +8615,26 @@ "tree-kill": "cli.js" } }, + "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/trouter": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/trouter/-/trouter-2.0.1.tgz", @@ -7455,6 +8797,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/unique-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", @@ -7470,6 +8831,74 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -7553,6 +8982,34 @@ "spdx-expression-parse": "^3.0.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/victory-vendor": { "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", @@ -7994,6 +9451,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "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 b40c362..34d7aca 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,10 @@ "playwright-test": "^14.1.12", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-markdown": "^10.1.0", "react-router-dom": "^7.10.1", - "recharts": "^3.4.1" + "recharts": "^3.4.1", + "remark-gfm": "^4.0.1" }, "devDependencies": { "@playwright/test": "^1.57.0", diff --git a/playwright-report/index.html b/playwright-report/index.html index fc99077..8968129 100644 --- a/playwright-report/index.html +++ b/playwright-report/index.html @@ -82,4 +82,4 @@ Error generating stack: `+n.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/server/.env b/server/.env new file mode 100644 index 0000000..8cc8b49 --- /dev/null +++ b/server/.env @@ -0,0 +1,22 @@ +# Generic + +# DEV +DATABASE_URL_DEV="file:./prisma/dev.db" +ADMIN_EMAIL_DEV="admin@gymflow.ai" +ADMIN_PASSWORD_DEV="admin123" + +# TEST +DATABASE_URL_TEST="file:./prisma/test.db" +ADMIN_EMAIL_TEST="admin@gymflow.ai" +ADMIN_PASSWORD_TEST="admin123" + +# PROD +DATABASE_URL_PROD="file:./prisma/prod.db" +ADMIN_EMAIL_PROD="admin-prod@gymflow.ai" +ADMIN_PASSWORD_PROD="secure-prod-password-change-me" + +# Fallback for Prisma CLI (Migrate default) +DATABASE_URL="file:./prisma/dev.db" + +GEMINI_API_KEY=AIzaSyC88SeFyFYjvSfTqgvEyr7iqLSvEhuadoE +DEFAULT_EXERCISES_CSV_PATH='default_exercises.csv' diff --git a/server/prisma/dev.db b/server/prisma/dev.db index 147078e..939d7a0 100644 Binary files a/server/prisma/dev.db and b/server/prisma/dev.db differ diff --git a/server/prisma/migrations/20251216135012_add_saved_messages/migration.sql b/server/prisma/migrations/20251216135012_add_saved_messages/migration.sql new file mode 100644 index 0000000..b278e2e --- /dev/null +++ b/server/prisma/migrations/20251216135012_add_saved_messages/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "SavedMessage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "content" TEXT NOT NULL, + "role" TEXT NOT NULL DEFAULT 'model', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "SavedMessage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 52f75e2..7fad9c9 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -24,6 +24,16 @@ model User { exercises Exercise[] plans WorkoutPlan[] weightRecords BodyWeightRecord[] + savedMessages SavedMessage[] +} + +model SavedMessage { + id String @id @default(uuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + content String + role String @default("model") + createdAt DateTime @default(now()) } model BodyWeightRecord { diff --git a/server/prod.db b/server/prod.db index 05771a2..a45f0c2 100644 Binary files a/server/prod.db and b/server/prod.db differ diff --git a/server/src/controllers/bookmarks.controller.ts b/server/src/controllers/bookmarks.controller.ts new file mode 100644 index 0000000..da2dd30 --- /dev/null +++ b/server/src/controllers/bookmarks.controller.ts @@ -0,0 +1,79 @@ +import { Request, Response } from 'express'; +import prisma from '../lib/prisma'; + +export class BookmarksController { + static async getAll(req: Request, res: Response) { + try { + const userId = (req as any).user?.userId; + if (!userId) { + return res.status(401).json({ success: false, error: 'Unauthorized' }); + } + + const messages = await prisma.savedMessage.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + }); + + return res.json({ success: true, data: messages }); + } catch (error) { + console.error('Failed to get bookmarks:', error); + return res.status(500).json({ success: false, error: 'Internal server error' }); + } + } + + static async create(req: Request, res: Response) { + try { + const userId = (req as any).user?.userId; + if (!userId) { + return res.status(401).json({ success: false, error: 'Unauthorized' }); + } + + const { content, role } = req.body; + if (!content) { + return res.status(400).json({ success: false, error: 'Content is required' }); + } + + const message = await prisma.savedMessage.create({ + data: { + userId, + content, + role: role || 'model', + }, + }); + + return res.json({ success: true, data: message }); + } catch (error) { + console.error('Failed to create bookmark:', error); + return res.status(500).json({ success: false, error: 'Internal server error' }); + } + } + + static async delete(req: Request, res: Response) { + try { + const userId = (req as any).user?.userId; + if (!userId) { + return res.status(401).json({ success: false, error: 'Unauthorized' }); + } + + const { id } = req.params; + + // Verify ownership + const existing = await prisma.savedMessage.findFirst({ + where: { id, userId }, + }); + + if (!existing) { + return res.status(404).json({ success: false, error: 'Bookmark not found' }); + } + + await prisma.savedMessage.delete({ + where: { id }, + }); + + return res.json({ success: true }); + } catch (error) { + console.error('Failed to delete bookmark:', error); + return res.status(500).json({ success: false, error: 'Internal server error' }); + } + } +} diff --git a/server/src/index.ts b/server/src/index.ts index 25df378..d2eaf5b 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -11,6 +11,7 @@ import sessionRoutes from './routes/sessions'; import planRoutes from './routes/plans'; import aiRoutes from './routes/ai'; import weightRoutes from './routes/weight'; +import bookmarksRoutes from './routes/bookmarks'; import bcrypt from 'bcryptjs'; import { PrismaClient } from '@prisma/client'; @@ -88,6 +89,7 @@ app.use('/api/sessions', sessionRoutes); app.use('/api/plans', planRoutes); app.use('/api/ai', aiRoutes); app.use('/api/weight', weightRoutes); +app.use('/api/bookmarks', bookmarksRoutes); app.get('/', (req, res) => { diff --git a/server/src/routes/bookmarks.ts b/server/src/routes/bookmarks.ts new file mode 100644 index 0000000..d0954dd --- /dev/null +++ b/server/src/routes/bookmarks.ts @@ -0,0 +1,13 @@ +import express from 'express'; +import { BookmarksController } from '../controllers/bookmarks.controller'; +import { authenticateToken } from '../middleware/auth'; + +const router = express.Router(); + +router.use(authenticateToken); + +router.get('/', BookmarksController.getAll); +router.post('/', BookmarksController.create); +router.delete('/:id', BookmarksController.delete); + +export default router; diff --git a/server/test.db b/server/test.db index f752d40..29ea0d8 100644 Binary files a/server/test.db and b/server/test.db differ diff --git a/specs/gymflow-test-plan.md b/specs/gymflow-test-plan.md index 2d176bc..facd3e4 100644 --- a/specs/gymflow-test-plan.md +++ b/specs/gymflow-test-plan.md @@ -333,23 +333,30 @@ Comprehensive test plan for the GymFlow web application, covering authentication **Expected Results:** - All exercises are created successfully with their respective types. -#### 2.14. A. Workout Plans - Create Plan with AI +#### 2.14. A. Workout Plans - Create Plan with AI (Parametrized) -**File:** `tests/workout-management.spec.ts` +**File:** `tests/ai-plan-creation.spec.ts` **Steps:** 1. Log in as a regular user. 2. Navigate to the 'Plans' section. 3. Click the '+' FAB button. 4. Select 'With AI' option. - 5. In the AI Side Sheet, enter a prompt (e.g., 'Create a short leg workout with lunges'). - 6. Click 'Generate'. - 7. Wait for the AI response. + 5. **Verify Defaults**: Duration 60, Equipment 'No equipment', Level 'Intermediate', Intensity 'Moderate'. + 6. **Modify Inputs**: + - Set Duration to 45 mins. + - Set Equipment to 'Free weights'. + - Set Level to 'Advanced'. + 7. Click 'Generate' (mocks AI response). + 8. **Iterative Flow**: + - Verify preview table appears. + - Click 'Generate' again. + - Click 'Save Plan'. **Expected Results:** - - A new plan is created and appears in the plans list. - - If 'Lunges' did not exist in the user's exercise library, it is created automatically. - - The plan contains the exercises described in the prompt. + - A new plan is created with the AI-suggested content. + - The plan appears in the plans list. + - New exercises are created with correct `type` and `isUnilateral` flags. #### 2.15. B. Tracker - Empty State AI Prompt @@ -1075,3 +1082,57 @@ Comprehensive test plan for the GymFlow web application, covering authentication **Expected Results:** - The volume, set count, and body weight charts resize and re-render correctly, maintaining readability and data integrity across different screen sizes. + +### 7. VII. AI Coach Features + +**Seed:** `tests/ai-coach.spec.ts` + +#### 7.1. A. AI Coach - Basic Conversation & Markdown +**File:** `tests/ai-coach.spec.ts` + +**Steps:** +1. Log in as a regular user. +2. Navigate to 'AI Coach'. +3. Type a message (e.g., "How to do a pushup?"). +4. Click 'Send'. +5. Verify response appears. + +**Expected Results:** +- AI responds with a message. +- Response renders Markdown correctly (e.g., bullet points, bold text). + +#### 7.2. A. AI Coach - Bookmark Message +**File:** `tests/ai-coach.spec.ts` + +**Steps:** +1. Send a message to AI Coach and receive a response. +2. Click the 'Bookmark' icon on the AI's response. +3. Verify a success notification (Snackbar). +4. Reload the page. + +**Expected Results:** +- The message remains bookmarked (icon state persists). + +#### 7.3. A. AI Coach - View Saved Messages +**File:** `tests/ai-coach.spec.ts` + +**Steps:** +1. Bookmark at least one message. +2. Click the 'Saved Messages' icon in the top bar. +3. Verify the Saved Messages sheet opens. + +**Expected Results:** +- The sheet displays the bookmarked message content. +- The content is rendered in Markdown. + +#### 7.4. A. AI Coach - Delete Bookmark +**File:** `tests/ai-coach.spec.ts` + +**Steps:** +1. Open Saved Messages sheet. +2. Click 'Delete' (trash icon) on a saved message. +3. Confirm if necessary (or verify immediate deletion). + +**Expected Results:** +- The message is removed from the list. +- The bookmark icon in the main chat (if message is visible) updates to unbookmarked state. diff --git a/specs/requirements.md b/specs/requirements.md index 663d853..7c34d20 100644 --- a/specs/requirements.md +++ b/specs/requirements.md @@ -68,18 +68,21 @@ Users can structure their training via Plans. * **Trigger**: "Create with AI" option in Plans FAB Menu, or "Ask your AI coach" link from Tracker (when no plans exist). * **UI Flow**: * Opens a dedicated Side Sheet in the Plans view. - * User enters a text prompt describing desired workout (e.g., "Create a 20-minute HIIT workout"). - * "Generate" button initiates AI call. + * **Inputs**: + * **Duration**: Slider (5 min to 2+ hours, 5 min step). Default 60 min. + * **Equipment**: Selector (No equipment, Essentials, Free weights, Complete gym). Default "No equipment". + * **Level**: Selector (Beginner, Intermediate, Advanced). Default "Intermediate". + * **Intensity**: Selector (Low, Moderate, High). Default "Moderate". + * **Additional Constraints**: Textarea (optional). + * **Action**: "Generate" button initiates AI call. + * **Preview**: Displays generated plan table. User can "Generate" again to retry, or "Save Plan" to finalize. * **AI Logic**: - * System sends prompt to AI service (`geminiService`). - * AI returns a structured JSON object containing: `name`, `description`, and `exercises` array. - * Each exercise object contains: `name`, `isWeighted` (boolean), `restTimeSeconds` (number). - * For **new exercises** (not in user's library), AI also provides: `type` ('reps' or 'time'), `unilateral` (boolean). - * **Auto-Creation of Exercises**: - * System parses AI response. - * For each exercise in the response, checks if it exists in the user's exercise library by name. - * If not found, creates a new `Exercise` record with AI-provided attributes (type, unilateral flag) via `saveExercise`. - * Links the new/existing exercise ID to the plan step. + * System sends structured prompt to AI service (`geminiService`) embedding all parameters. + * **Naming Rules**: + * Exercise names must NOT contain "Weighted" (use `isWeighted` flag). + * Exclude variants (e.g. "or ...") and form notes. + * **Structure**: Each item in list = ONE set. + * AI returns JSON with `name`, `description`, `exercises` (with `type`, `unilateral` for new ones). * **Result**: Saves the generated `WorkoutPlan` to DB and displays it in the Plans list. ### 3.3. Exercise Library @@ -196,6 +199,15 @@ Accessible only if `User.role === 'ADMIN'`. * **Delete User**: Permanent removal. * **Reset Password**: Admin can manually trigger password reset flows. +### 3.8. AI Coach +- **Conversational Interface**: Chat-like interface for asking fitness-related questions. +- **Context Awareness**: Access to user's workout history and profile for personalized advice. +- **RAG Integration**: Retrieval Augmented Generation using recent workout logs. +- **Plan Generation**: Ability to generate structured workout plans based on user prompt. +- **Markdown Support**: Rich text formatting for AI responses (bold, lists, code blocks). +- **Bookmarking**: Users can save helpful AI messages for later reference. +- **History Persistence**: Chat history is preserved locally across reloads. + ## 4. Technical Constants & Constraints * **Database**: SQLite (via Prisma). * **API Schema**: REST-like (JSON). diff --git a/src/components/AICoach.tsx b/src/components/AICoach.tsx index f29e5d5..a98d966 100644 --- a/src/components/AICoach.tsx +++ b/src/components/AICoach.tsx @@ -1,13 +1,18 @@ - -import React, { useState, useRef, useEffect } from 'react'; -import { Send, Bot, User, Loader2, AlertTriangle } from 'lucide-react'; +import React, { useState, useRef, useEffect, useMemo } from 'react'; +import { Send, MessageSquare, Loader2, AlertTriangle, Bookmark, BookmarkCheck, BookmarkIcon } from 'lucide-react'; import { createFitnessChat } from '../services/geminiService'; -import { WorkoutSession, Language, UserProfile, WorkoutPlan } from '../types'; -import { Chat, GenerateContentResponse } from '@google/genai'; +import { Language } from '../types'; +import { GenerateContentResponse } from '@google/genai'; import { t } from '../services/i18n'; import { generateId } from '../utils/uuid'; import { useAuth } from '../context/AuthContext'; import { useSession } from '../context/SessionContext'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { TopBar } from './ui/TopBar'; +import SavedMessagesSheet from './SavedMessagesSheet'; +import { createBookmark, deleteBookmark, SavedMessage, getBookmarks } from '../services/bookmarks'; +import Snackbar from './Snackbar'; interface AICoachProps { lang: Language; @@ -17,27 +22,83 @@ interface Message { id: string; role: 'user' | 'model'; text: string; + isBookmarked?: boolean; + savedMessageId?: string; + timestamp: number; } +const STORAGE_KEY_PREFIX = 'ai_coach_history_'; + const AICoach: React.FC = ({ lang }) => { const { currentUser } = useAuth(); const { sessions: history, plans } = useSession(); const userProfile = currentUser?.profile; + const userId = currentUser?.id; + + // Load initial messages from local storage + const [messages, setMessages] = useState(() => { + if (!userId) return []; + const saved = localStorage.getItem(`${STORAGE_KEY_PREFIX}${userId}`); + if (saved) { + try { + return JSON.parse(saved); + } catch (e) { + console.error("Failed to parse saved chat history", e); + } + } + return [{ id: 'intro', role: 'model', text: t('ai_intro', lang), timestamp: Date.now() }]; + }); - const [messages, setMessages] = useState([ - { id: 'intro', role: 'model', text: t('ai_intro', lang) } - ]); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const chatSessionRef = useRef(null); + const [showSavedMessages, setShowSavedMessages] = useState(false); + const [savedMessages, setSavedMessages] = useState([]); + const [snackbar, setSnackbar] = useState<{ open: boolean, message: string }>({ open: false, message: '' }); + + const chatSessionRef = useRef(null); // Type 'Chat' is hard to import perfectly here without errors if not careful const messagesEndRef = useRef(null); + // Sync with local storage + useEffect(() => { + if (userId) { + localStorage.setItem(`${STORAGE_KEY_PREFIX}${userId}`, JSON.stringify(messages)); + } + }, [messages, userId]); + + // Load bookmarks on mount to sync status + useEffect(() => { + if (!userId) return; + const loadBookmarks = async () => { + try { + const bookmarks = await getBookmarks(); + setSavedMessages(bookmarks); + // Update bookmarked status in local messages + setMessages(prev => prev.map(msg => { + const found = bookmarks.find(b => b.content === msg.text); + if (found) { + return { ...msg, isBookmarked: true, savedMessageId: found.id }; + } + return msg; + })); + } catch (e: any) { + if (e.message !== 'Unauthorized') { + console.warn("Failed to load bookmarks:", e); + } + } + }; + loadBookmarks(); + }, [userId, showSavedMessages]); + useEffect(() => { try { const chat = createFitnessChat(history, lang, userProfile, plans); if (chat) { chatSessionRef.current = chat; + // Restore history context + // Note: Gemini SDK doesn't easily allow "restoring" state without re-sending history + // This is a simplification; for full context restoration we'd need to rebuild history + // For now, we start fresh session context but display old UI messages } else { setError(t('ai_error', lang)); } @@ -57,7 +118,7 @@ const AICoach: React.FC = ({ lang }) => { const handleSend = async () => { if (!input.trim() || !chatSessionRef.current || loading) return; - const userMsg: Message = { id: generateId(), role: 'user', text: input }; + const userMsg: Message = { id: generateId(), role: 'user', text: input, timestamp: Date.now() }; setMessages(prev => [...prev, userMsg]); setInput(''); setLoading(true); @@ -69,27 +130,42 @@ const AICoach: React.FC = ({ lang }) => { const aiMsg: Message = { id: generateId(), role: 'model', - text: text || "Error generating response." + text: text || "Error generating response.", + timestamp: Date.now() }; setMessages(prev => [...prev, aiMsg]); } catch (err) { console.error(err); let errorText = 'Connection error.'; if (err instanceof Error) { - try { - const json = JSON.parse(err.message); - if (json.error) errorText = json.error; - else errorText = err.message; - } catch { - errorText = err.message; - } + errorText = err.message; } - setMessages(prev => [...prev, { id: generateId(), role: 'model', text: errorText }]); + setMessages(prev => [...prev, { id: generateId(), role: 'model', text: errorText, timestamp: Date.now() }]); } finally { setLoading(false); } }; + const toggleBookmark = async (msg: Message) => { + if (msg.role !== 'model') return; + + if (msg.isBookmarked && msg.savedMessageId) { + // Unbookmark + const success = await deleteBookmark(msg.savedMessageId); + if (success) { + setMessages(prev => prev.map(m => m.id === msg.id ? { ...m, isBookmarked: false, savedMessageId: undefined } : m)); + setSnackbar({ open: true, message: 'Bookmark removed' }); + } + } else { + // Bookmark + const newBookmark = await createBookmark(msg.text, msg.role); + if (newBookmark) { + setMessages(prev => prev.map(m => m.id === msg.id ? { ...m, isBookmarked: true, savedMessageId: newBookmark.id } : m)); + setSnackbar({ open: true, message: 'Message saved' }); + } + } + }; + if (error) { return (
@@ -101,23 +177,46 @@ const AICoach: React.FC = ({ lang }) => { return (
- {/* Header */} -
-
- -
-

{t('ai_expert', lang)}

-
+ setShowSavedMessages(true)} + className="p-2 text-on-surface hover:bg-surface-container-high rounded-full transition-colors" + title="Saved Messages" + > + + + } + /> {/* Messages */}
{messages.map((msg) => (
-
- {msg.text} +
+
+
+ + {msg.text} + +
+
+ + {/* Bookmark Action */} + {msg.role === 'model' && ( + + )}
))} @@ -152,6 +251,21 @@ const AICoach: React.FC = ({ lang }) => {
+ + setShowSavedMessages(false)} + onUnbookmark={(id) => { + setMessages(prev => prev.map(m => m.savedMessageId === id ? { ...m, isBookmarked: false, savedMessageId: undefined } : m)); + }} + /> + + setSnackbar({ ...snackbar, open: false })} + type="success" + />
); }; diff --git a/src/components/History.tsx b/src/components/History.tsx index f235883..602efc9 100644 --- a/src/components/History.tsx +++ b/src/components/History.tsx @@ -1,5 +1,7 @@ + import React, { useState } from 'react'; -import { Calendar, Clock, TrendingUp, Gauge, Pencil, Trash2, X, Save, ArrowRight, ArrowUp, Timer, Activity, Dumbbell, Percent } from 'lucide-react'; +import { Trash2, Calendar, Clock, ChevronDown, ChevronUp, History as HistoryIcon, Dumbbell, Ruler, Timer, Weight, Edit2, Gauge, Pencil, Save } from 'lucide-react'; +import { TopBar } from './ui/TopBar'; import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types'; import { t } from '../services/i18n'; import { formatSetMetrics } from '../utils/setFormatting'; @@ -84,7 +86,7 @@ const History: React.FC = ({ lang }) => { const formatDateForInput = (timestamp: number) => { const d = new Date(timestamp); - const pad = (n: number) => n < 10 ? '0' + n : n; + const pad = (n: number) => (n < 10 ? '0' + n : n); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; }; @@ -170,10 +172,7 @@ const History: React.FC = ({ lang }) => { return (
-
-

{t('tab_history', lang)}

-
- +
{/* Regular Workout Sessions */} diff --git a/src/components/Plans.tsx b/src/components/Plans.tsx index f7f8ab1..19115db 100644 --- a/src/components/Plans.tsx +++ b/src/components/Plans.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { Plus, Trash2, PlayCircle, Dumbbell, Save, X, List, ArrowUp, Pencil, User, Flame, Timer as TimerIcon, Ruler, Footprints, Percent, CheckCircle, GripVertical, Bot, Loader2 } from 'lucide-react'; +import { Plus, Trash2, PlayCircle, Dumbbell, Save, X, List, ArrowUp, Pencil, User, Flame, Timer as TimerIcon, Ruler, Footprints, Percent, CheckCircle, GripVertical, Bot, Loader2, ClipboardList } from 'lucide-react'; +import { TopBar } from './ui/TopBar'; import { DndContext, closestCenter, @@ -517,20 +518,24 @@ const Plans: React.FC = ({ lang }) => { if (isEditing) { return (
-
- -

{t('plan_editor', lang)}

- -
+ + + +
+ } + />
= ({ lang }) => { return (
-
-

{t('my_plans', lang)}

-
+
{plans.length === 0 ? ( diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx index ff16667..0916022 100644 --- a/src/components/Profile.tsx +++ b/src/components/Profile.tsx @@ -11,6 +11,8 @@ import { t } from '../services/i18n'; import Snackbar from './Snackbar'; import { Button } from './ui/Button'; import { Card } from './ui/Card'; +import { TopBar } from './ui/TopBar'; + import { Modal } from './ui/Modal'; import { SideSheet } from './ui/SideSheet'; import { Checkbox } from './ui/Checkbox'; @@ -267,15 +269,15 @@ const Profile: React.FC = ({ user, onLogout, lang, onLanguageChang return (
-
-

- - {t('profile_title', lang)} -

- -
+ + {t('logout', lang)} + + } + />
diff --git a/src/components/SavedMessagesSheet.tsx b/src/components/SavedMessagesSheet.tsx new file mode 100644 index 0000000..7b352f1 --- /dev/null +++ b/src/components/SavedMessagesSheet.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useState } from 'react'; +import { SideSheet } from './ui/SideSheet'; +import { SavedMessage, getBookmarks, deleteBookmark } from '../services/bookmarks'; +import { Trash2, AlertCircle, BookmarkX, Loader2 } from 'lucide-react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; + +interface SavedMessagesSheetProps { + isOpen: boolean; + onClose: () => void; + onUnbookmark?: (id: string) => void; +} + +const SavedMessagesSheet: React.FC = ({ isOpen, onClose, onUnbookmark }) => { + const [bookmarks, setBookmarks] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchBookmarks = async () => { + setLoading(true); + setError(null); + try { + const data = await getBookmarks(); + setBookmarks(data); + } catch (err: any) { + if (err.message !== 'Unauthorized') { + console.warn("Failed to fetch bookmarks:", err); + } + // Fallback to empty if API fails + setBookmarks([]); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (isOpen) { + fetchBookmarks(); + } + }, [isOpen]); + + const handleDelete = async (id: string, e: React.MouseEvent) => { + e.stopPropagation(); + try { + const success = await deleteBookmark(id); + if (success) { + setBookmarks(prev => prev.filter(b => b.id !== id)); + if (onUnbookmark) { + onUnbookmark(id); + } + } else { + // Optimistic update fallback + setBookmarks(prev => prev.filter(b => b.id !== id)); + if (onUnbookmark) { + onUnbookmark(id); + } + } + } catch (error) { + console.error("Delete failed", error); + } + }; + + return ( + +
+ {loading ? ( +
+ +

Loading...

+
+ ) : error ? ( +
+ +

{error}

+
+ ) : bookmarks.length === 0 ? ( +
+ +

No saved messages yet.

+
+ ) : ( +
+ {bookmarks.map((msg) => ( +
+
+ + {msg.content} + +
+
+ {new Date(msg.createdAt).toLocaleDateString()} + +
+
+ ))} +
+ )} +
+
+ ); +}; + +export default SavedMessagesSheet; diff --git a/src/components/Stats.tsx b/src/components/Stats.tsx index 9453198..192b844 100644 --- a/src/components/Stats.tsx +++ b/src/components/Stats.tsx @@ -4,6 +4,8 @@ import { getWeightHistory } from '../services/weight'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts'; import { t } from '../services/i18n'; import { useSession } from '../context/SessionContext'; +import { TopBar } from './ui/TopBar'; +import { BarChart2 } from 'lucide-react'; interface StatsProps { lang: Language; @@ -112,89 +114,91 @@ const Stats: React.FC = ({ lang }) => { } return ( -
-

{t('progress', lang)}

+
+ - {/* Volume Chart */} -
-
-
-

{t('volume_title', lang)}

-

{t('volume_subtitle', lang)}

+
+ {/* Volume Chart */} +
+
+
+

{t('volume_title', lang)}

+

{t('volume_subtitle', lang)}

+
+
+
+ + + + + `${(val / 1000).toFixed(1)}k`} /> + [`${val.toLocaleString()} kg`, t('volume_title', lang)]} + /> + + +
-
- - - - - `${(val / 1000).toFixed(1)}k`} /> - [`${val.toLocaleString()} kg`, t('volume_title', lang)]} - /> - - - -
-
- {/* Sessions Count Chart */} -
-

{t('sessions_count_title', lang)}

-
- - - - - - - - - + {/* Sessions Count Chart */} +
+

{t('sessions_count_title', lang)}

+
+ + + + + + + + + +
-
- {/* Sets Chart */} -
-

{t('sets_title', lang)}

-
- - - - - - - - - + {/* Sets Chart */} +
+

{t('sets_title', lang)}

+
+ + + + + + + + + +
-
- {/* Body Weight Chart */} -
-

{t('weight_title', lang)}

-
- - - - - - [`${val} kg`, t('weight_kg', lang)]} - /> - - - + {/* Body Weight Chart */} +
+

{t('weight_title', lang)}

+
+ + + + + + [`${val} kg`, t('weight_kg', lang)]} + /> + + + +
diff --git a/src/components/Tracker/IdleView.tsx b/src/components/Tracker/IdleView.tsx index 4d764d4..e5c3cbf 100644 --- a/src/components/Tracker/IdleView.tsx +++ b/src/components/Tracker/IdleView.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Dumbbell, User, PlayCircle, Plus, ArrowRight } from 'lucide-react'; +import { TopBar } from '../ui/TopBar'; import { Language } from '../../types'; import { t } from '../../services/i18n'; import { useTracker } from './useTracker'; @@ -77,110 +78,113 @@ const IdleView: React.FC = ({ tracker, lang }) => { const content = getDaysOffContent(); return ( -
-
-
-
- -
-
-

{content.title}

-

{content.subtitle}

-
-
- -
- -
- setUserBodyWeight(e.target.value)} - /> -
-

{t('change_in_profile', lang)}

-
- -
- - - -
- - {plans.length > 0 ? ( -
-

{t('or_choose_plan', lang)}

-
- {plans.map(plan => ( - - ))} +
+ +
+
+
+
+ +
+
+

{content.title}

+

{content.subtitle}

- ) : ( -
-

{t('no_plans_yet', lang)}

-
- - {t('ask_ai_to_create', lang)} - - - {t('create_manually', lang)} - + +
+ +
+ setUserBodyWeight(e.target.value)} + /> +
+

{t('change_in_profile', lang)}

+
+ +
+ + + +
+ + {plans.length > 0 ? ( +
+

{t('or_choose_plan', lang)}

+
+ {plans.map(plan => ( + + ))} +
+
+ ) : ( + + )} +
+ + {showPlanPrep && ( +
+
+

{showPlanPrep.name}

+
+
{t('prep_title', lang)}
+ {showPlanPrep.description || t('prep_no_instructions', lang)} +
+
+ + +
)}
- - {showPlanPrep && ( -
-
-

{showPlanPrep.name}

-
-
{t('prep_title', lang)}
- {showPlanPrep.description || t('prep_no_instructions', lang)} -
-
- - -
-
-
- )}
); }; diff --git a/src/components/ui/TopBar.tsx b/src/components/ui/TopBar.tsx new file mode 100644 index 0000000..f0493ce --- /dev/null +++ b/src/components/ui/TopBar.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { LucideIcon } from 'lucide-react'; + +interface TopBarProps { + title: string; + icon?: LucideIcon; + actions?: React.ReactNode; +} + +export const TopBar: React.FC = ({ title, icon: Icon, actions }) => { + return ( +
+ {Icon && ( +
+ +
+ )} +

{title}

+ {actions &&
{actions}
} +
+ ); +}; diff --git a/src/services/api.ts b/src/services/api.ts index cbd3466..9ce9fdc 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -12,11 +12,30 @@ const headers = () => { }; }; +const handleResponse = async (res: Response) => { + if (res.status === 401) { + removeAuthToken(); + if (window.location.pathname !== '/login') { + window.location.href = '/login'; + } + throw new Error('Unauthorized'); + } + if (!res.ok) { + const text = await res.text(); + try { + const json = JSON.parse(text); + throw new Error(json.message || json.error || text); + } catch { + throw new Error(text); + } + } + return res.json(); +}; + export const api = { get: async (endpoint: string): Promise => { const res = await fetch(`${API_URL}${endpoint}`, { headers: headers() }); - if (!res.ok) throw new Error(await res.text()); - return res.json(); + return handleResponse(res); }, post: async (endpoint: string, data: any): Promise => { const res = await fetch(`${API_URL}${endpoint}`, { @@ -24,8 +43,7 @@ export const api = { headers: headers(), body: JSON.stringify(data) }); - if (!res.ok) throw new Error(await res.text()); - return res.json(); + return handleResponse(res); }, put: async (endpoint: string, data: any): Promise => { const res = await fetch(`${API_URL}${endpoint}`, { @@ -33,16 +51,14 @@ export const api = { headers: headers(), body: JSON.stringify(data) }); - if (!res.ok) throw new Error(await res.text()); - return res.json(); + return handleResponse(res); }, delete: async (endpoint: string): Promise => { const res = await fetch(`${API_URL}${endpoint}`, { method: 'DELETE', headers: headers() }); - if (!res.ok) throw new Error(await res.text()); - return res.json(); + return handleResponse(res); }, patch: async (endpoint: string, data: any): Promise => { const res = await fetch(`${API_URL}${endpoint}`, { @@ -50,7 +66,6 @@ export const api = { headers: headers(), body: JSON.stringify(data) }); - if (!res.ok) throw new Error(await res.text()); - return res.json(); + return handleResponse(res); } }; diff --git a/src/services/bookmarks.ts b/src/services/bookmarks.ts new file mode 100644 index 0000000..c901267 --- /dev/null +++ b/src/services/bookmarks.ts @@ -0,0 +1,44 @@ +import { api } from './api'; + +export interface SavedMessage { + id: string; + content: string; + role: string; + createdAt: string; +} + +interface ApiResponse { + success: boolean; + data: T; + error?: string; +} + +export const getBookmarks = async (): Promise => { + try { + const res = await api.get>('/bookmarks'); + return res.data || []; + } catch (e) { + console.error('Failed to fetch bookmarks:', e); + return []; + } +}; + +export const createBookmark = async (content: string, role: string = 'model'): Promise => { + try { + const res = await api.post>('/bookmarks', { content, role }); + return res.data || null; + } catch (e) { + console.error('Failed to create bookmark:', e); + return null; + } +}; + +export const deleteBookmark = async (id: string): Promise => { + try { + await api.delete(`/bookmarks/${id}`); + return true; + } catch (e) { + console.error('Failed to delete bookmark:', e); + return false; + } +}; diff --git a/tests/ai-coach.spec.ts b/tests/ai-coach.spec.ts new file mode 100644 index 0000000..e1563ec --- /dev/null +++ b/tests/ai-coach.spec.ts @@ -0,0 +1,92 @@ + +import { test, expect } from './fixtures'; + +test.describe('VII. AI Coach Features', () => { + + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); + + // Helper to handle first login if needed (copied from core-auth) + async function handleFirstLogin(page: any) { + try { + const heading = page.getByRole('heading', { name: /Change Password/i }); + const dashboard = page.getByText('Free Workout'); + + await expect(heading).toBeVisible({ timeout: 5000 }); + await page.getByLabel('New Password').fill('StrongNewPass123!'); + await page.getByRole('button', { name: /Save|Change/i }).click(); + await expect(dashboard).toBeVisible(); + } catch (e) { + if (await page.getByText('Free Workout').isVisible()) return; + } + } + + test('7.1 AI Coach - Basic Conversation & Markdown', async ({ page, createUniqueUser }) => { + const user = await createUniqueUser(); + // Login + await page.getByLabel('Email').fill(user.email); + await page.getByLabel('Password').fill(user.password); + await page.getByRole('button', { name: 'Login' }).click(); + await handleFirstLogin(page); + + // Navigate to AI Coach + await page.getByText('AI Coach').click(); + + // Type message + const input = page.getByPlaceholder(/Ask your AI coach/i); + await input.fill('How to do a pushup?'); + await page.getByRole('button', { name: /Send/i }).click(); + + // Verify response (Mocked or Real - expecting Real from previous context) + // Since we can't easily mock backend without more setup, we wait for *any* response + await expect(page.locator('.prose')).toBeVisible({ timeout: 15000 }); + + // Check for markdown rendering (e.g., strong tags or list items if AI returns them) + // This is a bit flaky with real AI, but checking for visibility is a good start. + }); + + test('7.2, 7.3, 7.4 AI Coach - Bookmark Flow', async ({ page, createUniqueUser }) => { + const user = await createUniqueUser(); + // Login + await page.getByLabel('Email').fill(user.email); + await page.getByLabel('Password').fill(user.password); + await page.getByRole('button', { name: 'Login' }).click(); + await handleFirstLogin(page); + + await page.getByText('AI Coach').click(); + + // Send message + await page.getByPlaceholder(/Ask your AI coach/i).fill('Tell me a short fitness tip'); + await page.getByRole('button', { name: /Send/i }).click(); + + // Wait for response bubble + const responseBubble = page.locator('.prose').first(); + await expect(responseBubble).toBeVisible({ timeout: 15000 }); + + // 7.2 Bookmark + // Find bookmark button within the message container. + // Assuming the layout puts actions near the message. + // We look for the Bookmark icon button. + const bookmarkBtn = page.getByRole('button', { name: /Bookmark/i }).first(); + await bookmarkBtn.click(); + + // Expect success snackbar + await expect(page.getByText(/Message saved/i)).toBeVisible(); + + // 7.3 View Saved + await page.getByRole('button', { name: /Saved/i }).click(); // The TopBar action + await expect(page.getByText('Saved Messages')).toBeVisible(); // Sheet title + + // Verify content is there + await expect(page.getByText(/fitness tip/i)).toBeVisible(); // Part of our prompt/response context usually + + // 7.4 Delete Bookmark + const deleteBtn = page.getByRole('button', { name: /Delete/i }).first(); + await deleteBtn.click(); + + // Verify removal + await expect(deleteBtn).not.toBeVisible(); + }); + +}); diff --git a/tests/repro_edit_fields.spec.ts b/tests/repro_edit_fields.spec.ts new file mode 100644 index 0000000..fae7e66 --- /dev/null +++ b/tests/repro_edit_fields.spec.ts @@ -0,0 +1,136 @@ +import { test, expect } from './fixtures'; + +test.describe('Reproduction - Edit Modal Fields', () => { + + test('Verify Edit Fields for different Exercise Types', async ({ page, createUniqueUser, request }) => { + const user = await createUniqueUser(); + // Login + await page.goto('/'); + await page.getByLabel('Email').fill(user.email); + await page.getByLabel('Password').fill(user.password); + await page.getByRole('button', { name: 'Login' }).click(); + + // Wait for dashboard or password change + try { + const heading = page.getByRole('heading', { name: /Change Password/i }); + const dashboard = page.getByText('Free Workout'); + await expect(heading.or(dashboard)).toBeVisible({ timeout: 10000 }); + if (await heading.isVisible()) { + await page.getByLabel('New Password').fill('StrongNewPass123!'); + await page.getByRole('button', { name: /Save|Change/i }).click(); + await expect(dashboard).toBeVisible(); + } + } catch (e) { + console.log('Login flow exception (might be benign if already logged in):', e); + } + + // Seed exercises of different types + const types = [ + { type: 'PLYOMETRIC', name: 'Plyo Test', expectedFields: ['Reps'] }, + { type: 'STRENGTH', name: 'Strength Test', expectedFields: ['Weight', 'Reps'] }, + { type: 'CARDIO', name: 'Cardio Test', expectedFields: ['Time', 'Distance'] }, + { type: 'STATIC', name: 'Static Test', expectedFields: ['Time', 'Weight', 'Body Weight'] }, // Check if Weight is expected based on History.tsx analysis + { type: 'BODYWEIGHT', name: 'Bodyweight Test', expectedFields: ['Reps', 'Body Weight', 'Weight'] }, + { type: 'HIGH_JUMP', name: 'High Jump Test', expectedFields: ['Height'] }, + { type: 'LONG_JUMP', name: 'Long Jump Test', expectedFields: ['Distance'] }, + ]; + + const exIds: Record = {}; + + for (const t of types) { + const resp = await request.post('/api/exercises', { + data: { name: t.name, type: t.type }, + headers: { 'Authorization': `Bearer ${user.token}` } + }); + expect(resp.ok()).toBeTruthy(); + const created = await resp.json(); + // Adjust if the response structure is different (e.g. created.exercise) + exIds[t.name] = created.id || created.exercise?.id || created.data?.id; + } + + await page.reload(); + + // Construct a session payload + const now = Date.now(); + const setsStub = types.map(t => { + const set: any = { + exerciseId: exIds[t.name], + timestamp: now + 1000, + completed: true + }; + if (t.type === 'STRENGTH' || t.type === 'BODYWEIGHT' || t.type === 'PLYOMETRIC') set.reps = 10; + if (t.type === 'STRENGTH' || t.type === 'BODYWEIGHT' || t.type === 'STATIC') set.weight = 50; + if (t.type === 'BODYWEIGHT' || t.type === 'STATIC') set.bodyWeightPercentage = 100; + if (t.type === 'CARDIO' || t.type === 'STATIC') set.durationSeconds = 60; + if (t.type === 'CARDIO' || t.type === 'LONG_JUMP') set.distanceMeters = 100; + if (t.type === 'HIGH_JUMP') set.height = 150; + return set; + }); + + + + const sessionResp = await request.post('/api/sessions', { + data: { + startTime: now, + endTime: now + 3600000, + type: 'STANDARD', // History shows STANDARD sessions differently than QUICK_LOG + sets: setsStub + }, + headers: { 'Authorization': `Bearer ${user.token}` } + }); + if (!sessionResp.ok()) { + console.log('Session Create Error:', await sessionResp.text()); + } + expect(sessionResp.ok()).toBeTruthy(); + + // Go to History + await page.getByRole('button', { name: 'History' }).first().click(); + + // Find the session card and click Edit (Pencil icon) + // There should be only one session + await page.locator('.lucide-pencil').first().click(); + + await expect(page.getByText('Edit', { exact: true })).toBeVisible(); + + // Now verify fields for each exercise in the modal + for (const t of types) { + const exRow = page.locator('div').filter({ hasText: t.name }).last(); // Find the row for this exercise + // This locator might be tricky if the row structure is complex. + // In History.tsx: + // {editingSession.sets.map((set, idx) => ( + //
+ // ... {set.exerciseName} ... + //
inputs here
+ //
+ // ))} + + // So we find the container that has the exercise name, then look for inputs inside it. + const row = page.locator('.bg-surface-container-low').filter({ hasText: t.name }).first(); + await expect(row).toBeVisible(); + + console.log(`Checking fields for ${t.type} (${t.name})...`); + + for (const field of t.expectedFields) { + // Map field name to label text actually used in History.tsx + // t('weight_kg', lang) -> "Weight" (assuming en) + // t('reps', lang) -> "Reps" + // t('time_sec', lang) -> "Time" + // t('dist_m', lang) -> "Distance" + // t('height_cm', lang) -> "Height" + // t('body_weight_percent', lang) -> "Body Weight %" + + let labelPattern: RegExp; + if (field === 'Weight') labelPattern = /Weight/i; + else if (field === 'Reps') labelPattern = /Reps/i; + else if (field === 'Time') labelPattern = /Time/i; + else if (field === 'Distance') labelPattern = /Distance|Dist/i; + else if (field === 'Height') labelPattern = /Height/i; + else if (field === 'Body Weight') labelPattern = /Body Weight/i; + else labelPattern = new RegExp(field, 'i'); + + await expect(row.getByLabel(labelPattern).first()).toBeVisible({ timeout: 2000 }) + .catch(() => { throw new Error(`Missing field '${field}' for type '${t.type}'`); }); + } + } + }); +});