From 23f3b4c6b401c9868b4694f0a73afea4a0885c88 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 27 Nov 2025 00:21:06 -0500 Subject: [PATCH 01/21] simple ai assistant ui --- cash-ui/package-lock.json | 1336 +++++++++++++++++++++-- cash-ui/package.json | 4 +- cash-ui/src/App.jsx | 2 + cash-ui/src/api/api.js | 46 +- cash-ui/src/components/ChatInput.jsx | 77 ++ cash-ui/src/components/ChatMessages.jsx | 85 ++ cash-ui/src/components/ChatPopup.jsx | 59 + cash-ui/src/components/Chatbot.jsx | 86 ++ cash-ui/src/components/Spinner.jsx | 10 + cash-ui/src/hooks/useAutoScroll.js | 63 ++ cash-ui/src/hooks/useAutosize.js | 22 + 11 files changed, 1718 insertions(+), 72 deletions(-) create mode 100644 cash-ui/src/components/ChatInput.jsx create mode 100644 cash-ui/src/components/ChatMessages.jsx create mode 100644 cash-ui/src/components/ChatPopup.jsx create mode 100644 cash-ui/src/components/Chatbot.jsx create mode 100644 cash-ui/src/components/Spinner.jsx create mode 100644 cash-ui/src/hooks/useAutoScroll.js create mode 100644 cash-ui/src/hooks/useAutosize.js diff --git a/cash-ui/package-lock.json b/cash-ui/package-lock.json index 2360ca5..9e40f26 100644 --- a/cash-ui/package-lock.json +++ b/cash-ui/package-lock.json @@ -15,7 +15,9 @@ "axios": "^1.13.2", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-router-dom": "^7.9.6" + "react-markdown": "^10.1.0", + "react-router-dom": "^7.9.6", + "use-immer": "^0.11.0" }, "devDependencies": { "@eslint/js": "^9.36.0", @@ -1763,13 +1765,39 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1777,6 +1805,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -1817,6 +1860,18 @@ "@types/react": "*" } }, + "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/@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.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz", @@ -1933,6 +1988,16 @@ "npm": ">=6" } }, + "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", @@ -2038,6 +2103,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", @@ -2055,6 +2130,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "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/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2096,6 +2211,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2182,6 +2307,19 @@ } } }, + "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/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2198,6 +2336,28 @@ "node": ">=0.4.0" } }, + "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/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -2514,6 +2674,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2524,6 +2694,12 @@ "node": ">=0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2814,6 +2990,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/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -2829,6 +3045,16 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "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/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2839,6 +3065,17 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.0.0.tgz", + "integrity": "sha512-XtRG4SINt4dpqlnJvs70O2j6hH7H0X8fUzFsjMn1rwnETaxwp83HLNimXBjZ78MrKl3/d3/pkzDH0o0Lkxm37Q==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -2865,6 +3102,36 @@ "node": ">=0.8.19" } }, + "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/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-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2886,6 +3153,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", @@ -2909,6 +3186,28 @@ "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-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3040,6 +3339,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3071,93 +3380,688 @@ "node": ">= 0.4" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "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", - "engines": { - "node": ">= 0.6" + "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/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "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": { - "mime-db": "1.52.0" + "@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" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", + "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": { - "brace-expansion": "^1.1.7" + "@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" }, - "engines": { - "node": "*" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "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", - "bin": { - "nanoid": "bin/nanoid.cjs" + "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" }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "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", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, + "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/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -3215,6 +4119,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -3359,6 +4288,16 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "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/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -3402,6 +4341,33 @@ "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", "license": "MIT" }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -3466,6 +4432,39 @@ "react-dom": ">=16.6.0" } }, + "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/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -3601,6 +4600,30 @@ "node": ">=0.10.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/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-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3614,6 +4637,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/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -3662,6 +4703,26 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "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/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3675,6 +4736,93 @@ "node": ">= 0.8.0" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -3716,6 +4864,44 @@ "punycode": "^2.1.0" } }, + "node_modules/use-immer": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.11.0.tgz", + "integrity": "sha512-RNAqi3GqsWJ4bcCd4LMBgdzvPmTABam24DUaFiKfX9s3MSorNRz9RDZYJkllJoMHUxVLMDetwAuCDeyWNrp1yA==", + "license": "MIT", + "peerDependencies": { + "immer": ">=8.0.0", + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.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/vite": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", @@ -3851,6 +5037,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/cash-ui/package.json b/cash-ui/package.json index b862ac5..94f5237 100644 --- a/cash-ui/package.json +++ b/cash-ui/package.json @@ -17,7 +17,9 @@ "axios": "^1.13.2", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-router-dom": "^7.9.6" + "react-markdown": "^10.1.0", + "react-router-dom": "^7.9.6", + "use-immer": "^0.11.0" }, "devDependencies": { "@eslint/js": "^9.36.0", diff --git a/cash-ui/src/App.jsx b/cash-ui/src/App.jsx index d9a88de..a4421d8 100644 --- a/cash-ui/src/App.jsx +++ b/cash-ui/src/App.jsx @@ -10,6 +10,7 @@ import NotFound from './pages/NotFound'; import Catalogue from './pages/Catalogue'; import MyItems from './pages/MyItems'; import PastAuctions from './pages/PastAuctions'; +import ChatPopup from './components/ChatPopup'; function App() { const [isAuthenticated] = useState(!!localStorage.getItem('access_token')); @@ -17,6 +18,7 @@ function App() { return ( + } /> } /> diff --git a/cash-ui/src/api/api.js b/cash-ui/src/api/api.js index 990862b..7437bfb 100644 --- a/cash-ui/src/api/api.js +++ b/cash-ui/src/api/api.js @@ -1,8 +1,10 @@ import axios from 'axios'; +const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/'; + // Create an axios instance const apiClient = axios.create({ - baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8080/api/', + baseURL: BASE_URL, headers: { 'Content-Type': 'application/json', }, @@ -36,4 +38,46 @@ apiClient.interceptors.response.use( } ); +async function buildUserChatRequest(overrides) { + if (overrides) { + return overrides; + } + + const userId = localStorage.getItem('userId'); + const jwtToken = localStorage.getItem('access_token'); + + if (!userId || !jwtToken) { + throw new Error('Missing authentication context for chat.'); + } + + const { data } = await apiClient.get(`/users/${userId}`); + const { email = '', username = '', firstName, first_name } = data || {}; + + return { + user_id: Number(userId), + email, + username, + first_name: firstName ?? first_name ?? '', + jwt_token: jwtToken, + }; +} + +export async function createChat(userContextOverrides) { + const payload = await buildUserChatRequest(userContextOverrides); + const { data } = await apiClient.post('/chats', payload); + return data; +} + +export async function getChatHistory(chatId) { + const { data } = await apiClient.get(`/chats/${chatId}`); + return data; +} + +export async function sendChatMessage(chatId, message) { + const { data } = await apiClient.post(`/chats/${chatId}/message`, { + message, + }); + return data; +} + export default apiClient; diff --git a/cash-ui/src/components/ChatInput.jsx b/cash-ui/src/components/ChatInput.jsx new file mode 100644 index 0000000..4e9b4c8 --- /dev/null +++ b/cash-ui/src/components/ChatInput.jsx @@ -0,0 +1,77 @@ +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import InputBase from '@mui/material/InputBase'; +import IconButton from '@mui/material/IconButton'; +import SendIcon from '@mui/icons-material/Send'; + +function ChatInput({ newMessage, isLoading, setNewMessage, submitNewMessage }) { + const trimmedMessage = newMessage.trim(); + + const handleSubmit = (event) => { + event.preventDefault(); + if (!trimmedMessage || isLoading) return; + submitNewMessage(); + }; + + const handleKeyDown = (event) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + handleSubmit(event); + } + }; + + return ( + + + setNewMessage(e.target.value)} + onKeyDown={handleKeyDown} + sx={{ + flexGrow: 1, + fontSize: 16, + lineHeight: 1.4, + pr: 1, + }} + /> + + + + + + ); +} + +export default ChatInput; diff --git a/cash-ui/src/components/ChatMessages.jsx b/cash-ui/src/components/ChatMessages.jsx new file mode 100644 index 0000000..392215f --- /dev/null +++ b/cash-ui/src/components/ChatMessages.jsx @@ -0,0 +1,85 @@ +import Markdown from 'react-markdown'; +import useAutoScroll from '../hooks/useAutoScroll'; +import Spinner from './Spinner'; +import WarningIcon from '@mui/icons-material/Warning'; +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; + +const USER_BUBBLE_COLOR = '#007aff'; +const ASSISTANT_BUBBLE_COLOR = '#8f8f8fff'; + +function ChatMessages({ messages, isLoading }) { + const scrollContentRef = useAutoScroll(isLoading); + + return ( + + {messages.map(({ role, content, loading, error }, idx) => { + const isUser = role === 'user'; + return ( + + + + {loading && !content ? ( + + ) : role === 'assistant' ? ( + {content} + ) : ( + + {content} + + )} + + {error && ( + + + + Error generating the response + + + )} + + + ); + })} + + ); +} + +export default ChatMessages; diff --git a/cash-ui/src/components/ChatPopup.jsx b/cash-ui/src/components/ChatPopup.jsx new file mode 100644 index 0000000..2a5390c --- /dev/null +++ b/cash-ui/src/components/ChatPopup.jsx @@ -0,0 +1,59 @@ +import { useState } from 'react'; +import Chatbot from './Chatbot'; +import IconButton from '@mui/material/IconButton'; +import Box from '@mui/material/Box'; +import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; + +export default function ChatPopup() { + const [open, setOpen] = useState(false); + return ( + <> + + setOpen((prev) => !prev)} + sx={{ + bgcolor: 'primary.main', + color: 'white', + width: 56, + height: 56, + borderRadius: '50%', + boxShadow: 6, + '&:hover': { + bgcolor: 'primary.dark', + }, + }} + > + + + + {open && ( + + + + )} + + ); +} diff --git a/cash-ui/src/components/Chatbot.jsx b/cash-ui/src/components/Chatbot.jsx new file mode 100644 index 0000000..d7ec902 --- /dev/null +++ b/cash-ui/src/components/Chatbot.jsx @@ -0,0 +1,86 @@ +import { useState } from 'react'; +import { useImmer } from 'use-immer'; +import { createChat, sendChatMessage } from '../api/api'; +import ChatMessages from './ChatMessages'; +import ChatInput from './ChatInput'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; + +function Chatbot() { + const [chatId, setChatId] = useState(null); + const [messages, setMessages] = useImmer([ + { + role: 'assistant', + content: 'Hey, ', + }, + ]); + const [newMessage, setNewMessage] = useState(''); + + const lastMessage = messages[messages.length - 1]; + const isLoading = Boolean(lastMessage?.loading); + + async function submitNewMessage() { + const trimmedMessage = newMessage.trim(); + if (!trimmedMessage || isLoading) return; + + setMessages((draft) => [ + ...draft, + { role: 'user', content: trimmedMessage }, + { role: 'assistant', content: '', sources: [], loading: true }, + ]); + setNewMessage(''); + + let chatIdOrNew = chatId; + try { + if (!chatId) { + const { id } = await createChat(); + setChatId(id); + chatIdOrNew = id; + } + + const response = await sendChatMessage(chatIdOrNew, trimmedMessage); + const assistantText = + response?.reply || response?.message || response?.content || ''; + const assistantSources = response?.sources || []; + + setMessages((draft) => { + const assistantMessage = draft[draft.length - 1]; + assistantMessage.content = assistantText; + assistantMessage.sources = assistantSources; + assistantMessage.loading = false; + }); + } catch (err) { + console.log(err); + setMessages((draft) => { + draft[draft.length - 1].loading = false; + draft[draft.length - 1].error = true; + }); + } + } + + return ( + + + + + + + ); +} + +export default Chatbot; diff --git a/cash-ui/src/components/Spinner.jsx b/cash-ui/src/components/Spinner.jsx new file mode 100644 index 0000000..8130fd8 --- /dev/null +++ b/cash-ui/src/components/Spinner.jsx @@ -0,0 +1,10 @@ +function Spinner() { + return ( +
+ + +
+ ); +} + +export default Spinner; diff --git a/cash-ui/src/hooks/useAutoScroll.js b/cash-ui/src/hooks/useAutoScroll.js new file mode 100644 index 0000000..fa208e9 --- /dev/null +++ b/cash-ui/src/hooks/useAutoScroll.js @@ -0,0 +1,63 @@ +import { useEffect, useLayoutEffect, useRef } from 'react'; + +const SCROLL_THRESHOLD = 10; + +function useAutoScroll(active) { + const scrollContentRef = useRef(null); + const isDisabled = useRef(false); + const prevScrollTop = useRef(null); + + useEffect(() => { + const resizeObserver = new ResizeObserver(() => { + const { scrollHeight, clientHeight, scrollTop } = + document.documentElement; + if (!isDisabled.current && scrollHeight - clientHeight > scrollTop) { + document.documentElement.scrollTo({ + top: scrollHeight - clientHeight, + behavior: 'smooth', + }); + } + }); + + if (scrollContentRef.current) { + resizeObserver.observe(scrollContentRef.current); + } + + return () => resizeObserver.disconnect(); + }, []); + + useLayoutEffect(() => { + if (!active) { + isDisabled.current = true; + return; + } + + function onScroll() { + const { scrollHeight, clientHeight, scrollTop } = + document.documentElement; + if ( + !isDisabled.current && + window.scrollY < prevScrollTop.current && + scrollHeight - clientHeight > scrollTop + SCROLL_THRESHOLD + ) { + isDisabled.current = true; + } else if ( + isDisabled.current && + scrollHeight - clientHeight <= scrollTop + SCROLL_THRESHOLD + ) { + isDisabled.current = false; + } + prevScrollTop.current = window.scrollY; + } + + isDisabled.current = false; + prevScrollTop.current = document.documentElement.scrollTop; + window.addEventListener('scroll', onScroll); + + return () => window.removeEventListener('scroll', onScroll); + }, [active]); + + return scrollContentRef; +} + +export default useAutoScroll; diff --git a/cash-ui/src/hooks/useAutosize.js b/cash-ui/src/hooks/useAutosize.js new file mode 100644 index 0000000..08d4dbd --- /dev/null +++ b/cash-ui/src/hooks/useAutosize.js @@ -0,0 +1,22 @@ +import { useState, useLayoutEffect, useRef } from 'react'; + +function useAutosize(value) { + const ref = useRef(null); + const [borderWidth, setBorderWidth] = useState(0); + + useLayoutEffect(() => { + const style = window.getComputedStyle(ref.current); + setBorderWidth( + parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth) + ); + }, []); + + useLayoutEffect(() => { + ref.current.style.height = 'inherit'; + ref.current.style.height = `${ref.current.scrollHeight + borderWidth}px`; + }, [value, borderWidth]); + + return ref; +} + +export default useAutosize; From f3b2671335a48ec6cab1326c449b97a511fe24b7 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 27 Nov 2025 00:31:00 -0500 Subject: [PATCH 02/21] fix local storage --- cash-ui/src/pages/Login.jsx | 5 ++++- cash-ui/src/pages/Signup.jsx | 5 ----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/cash-ui/src/pages/Login.jsx b/cash-ui/src/pages/Login.jsx index 44a96ef..20425b5 100644 --- a/cash-ui/src/pages/Login.jsx +++ b/cash-ui/src/pages/Login.jsx @@ -49,7 +49,10 @@ export default function Login() { // Save access_token for other pages to detect login localStorage.setItem('access_token', data.jwt); localStorage.setItem('userId', data.userId); - + localStorage.setItem('username', data.username); + localStorage.setItem('email', data.email); + localStorage.setItem('firstName', data.firstName); + localStorage.setItem('lastName', data.lastName); navigate('/catalogue'); } catch (err) { console.error(err); diff --git a/cash-ui/src/pages/Signup.jsx b/cash-ui/src/pages/Signup.jsx index 107235d..d1bb442 100644 --- a/cash-ui/src/pages/Signup.jsx +++ b/cash-ui/src/pages/Signup.jsx @@ -75,11 +75,6 @@ export default function Signup() { setError('Signup failed.'); return; } - localStorage.setItem('username', res.data.username); - localStorage.setItem('email', res.data.email); - localStorage.setItem('firstName', res.data.firstName); - localStorage.setItem('lastName', res.data.lastName); - setSuccess(res.data.message || 'Account created successfully!'); setTimeout(() => navigate('/login'), 1500); } catch (err) { From 551772c99c7229affd59cad8bd541927e6f061d6 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 27 Nov 2025 01:26:49 -0500 Subject: [PATCH 03/21] integrate with agent backend --- cash-ui/src/api/api.js | 16 +++++-- cash-ui/src/components/ChatInput.jsx | 5 ++- cash-ui/src/components/ChatMessages.jsx | 56 +++++++++++++++++-------- cash-ui/src/components/ChatPopup.jsx | 6 ++- cash-ui/src/components/Chatbot.jsx | 10 +++-- cash-ui/src/pages/Login.jsx | 17 ++++++-- cash-ui/src/services/auth.js | 4 ++ 7 files changed, 81 insertions(+), 33 deletions(-) diff --git a/cash-ui/src/api/api.js b/cash-ui/src/api/api.js index 7437bfb..f5fafbe 100644 --- a/cash-ui/src/api/api.js +++ b/cash-ui/src/api/api.js @@ -1,6 +1,8 @@ import axios from 'axios'; const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/'; +const AI_BASE_URL = + import.meta.env.VITE_AI_API_URL || 'http://localhost:8000/api'; // Create an axios instance const apiClient = axios.create({ @@ -38,6 +40,14 @@ apiClient.interceptors.response.use( } ); +// AI Chat API functions (use AI_BASE_URL) +const aiClient = axios.create({ + baseURL: AI_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + async function buildUserChatRequest(overrides) { if (overrides) { return overrides; @@ -64,17 +74,17 @@ async function buildUserChatRequest(overrides) { export async function createChat(userContextOverrides) { const payload = await buildUserChatRequest(userContextOverrides); - const { data } = await apiClient.post('/chats', payload); + const { data } = await aiClient.post('/chats', payload); return data; } export async function getChatHistory(chatId) { - const { data } = await apiClient.get(`/chats/${chatId}`); + const { data } = await aiClient.get(`/chats/${chatId}`); return data; } export async function sendChatMessage(chatId, message) { - const { data } = await apiClient.post(`/chats/${chatId}/message`, { + const { data } = await aiClient.post(`/chats/${chatId}/message`, { message, }); return data; diff --git a/cash-ui/src/components/ChatInput.jsx b/cash-ui/src/components/ChatInput.jsx index 4e9b4c8..a2eb71e 100644 --- a/cash-ui/src/components/ChatInput.jsx +++ b/cash-ui/src/components/ChatInput.jsx @@ -31,7 +31,7 @@ function ChatInput({ newMessage, isLoading, setNewMessage, submitNewMessage }) { sx={{ display: 'flex', alignItems: 'center', - borderRadius: 999, + // borderRadius: 999, px: 2, py: 1, gap: 1, @@ -41,10 +41,11 @@ function ChatInput({ newMessage, isLoading, setNewMessage, submitNewMessage }) { multiline minRows={1} maxRows={6} - placeholder="Type a message" + placeholder="Type your message" value={newMessage} onChange={(e) => setNewMessage(e.target.value)} onKeyDown={handleKeyDown} + inputProps={{ maxLength: 500 }} sx={{ flexGrow: 1, fontSize: 16, diff --git a/cash-ui/src/components/ChatMessages.jsx b/cash-ui/src/components/ChatMessages.jsx index 392215f..132cab6 100644 --- a/cash-ui/src/components/ChatMessages.jsx +++ b/cash-ui/src/components/ChatMessages.jsx @@ -1,26 +1,33 @@ import Markdown from 'react-markdown'; -import useAutoScroll from '../hooks/useAutoScroll'; -import Spinner from './Spinner'; import WarningIcon from '@mui/icons-material/Warning'; import Box from '@mui/material/Box'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; +import { useEffect, useRef } from 'react'; -const USER_BUBBLE_COLOR = '#007aff'; -const ASSISTANT_BUBBLE_COLOR = '#8f8f8fff'; +const ASSISTANT_BUBBLE_COLOR = '#1976d2'; +const USER_BUBBLE_COLOR = '#dededeff'; function ChatMessages({ messages, isLoading }) { - const scrollContentRef = useAutoScroll(isLoading); + const scrollRef = useRef(null); + + // Auto-scroll to bottom when messages change + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [messages]); return ( {messages.map(({ role, content, loading, error }, idx) => { @@ -29,6 +36,7 @@ function ChatMessages({ messages, isLoading }) { {loading && !content ? ( - + + Thinking... + ) : role === 'assistant' ? ( {content} ) : ( - + {content} )} @@ -65,11 +82,14 @@ function ChatMessages({ messages, isLoading }) { alignItems: 'center', gap: 0.5, color: 'error.light', - mt: 1, }} > - + Error generating the response diff --git a/cash-ui/src/components/ChatPopup.jsx b/cash-ui/src/components/ChatPopup.jsx index 2a5390c..1fe1523 100644 --- a/cash-ui/src/components/ChatPopup.jsx +++ b/cash-ui/src/components/ChatPopup.jsx @@ -45,10 +45,12 @@ export default function ChatPopup() { borderColor: 'divider', boxShadow: 6, borderRadius: 3, - width: 350, - height: 500, + width: 500, + height: 600, zIndex: 9999, overflow: 'hidden', + display: 'flex', + flexDirection: 'column', }} > diff --git a/cash-ui/src/components/Chatbot.jsx b/cash-ui/src/components/Chatbot.jsx index d7ec902..5058d83 100644 --- a/cash-ui/src/components/Chatbot.jsx +++ b/cash-ui/src/components/Chatbot.jsx @@ -7,11 +7,13 @@ import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; function Chatbot() { + const userName = localStorage.getItem('firstName') || 'there'; + const [chatId, setChatId] = useState(null); const [messages, setMessages] = useImmer([ { role: 'assistant', - content: 'Hey, ', + content: `Hey, ${userName}! How can I assist you today?`, }, ]); const [newMessage, setNewMessage] = useState(''); @@ -33,9 +35,9 @@ function Chatbot() { let chatIdOrNew = chatId; try { if (!chatId) { - const { id } = await createChat(); - setChatId(id); - chatIdOrNew = id; + const { chat_id } = await createChat(); + setChatId(chat_id); + chatIdOrNew = chat_id; } const response = await sendChatMessage(chatIdOrNew, trimmedMessage); diff --git a/cash-ui/src/pages/Login.jsx b/cash-ui/src/pages/Login.jsx index 20425b5..efd1622 100644 --- a/cash-ui/src/pages/Login.jsx +++ b/cash-ui/src/pages/Login.jsx @@ -49,10 +49,19 @@ export default function Login() { // Save access_token for other pages to detect login localStorage.setItem('access_token', data.jwt); localStorage.setItem('userId', data.userId); - localStorage.setItem('username', data.username); - localStorage.setItem('email', data.email); - localStorage.setItem('firstName', data.firstName); - localStorage.setItem('lastName', data.lastName); + + const user = await fetch(`/api/users/${data.userId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${data.jwt}`, + }, + }); + const userData = await user.json(); + localStorage.setItem('username', userData.username); + localStorage.setItem('email', userData.email); + localStorage.setItem('firstName', userData.firstName); + localStorage.setItem('lastName', userData.lastName); navigate('/catalogue'); } catch (err) { console.error(err); diff --git a/cash-ui/src/services/auth.js b/cash-ui/src/services/auth.js index 9011080..2631edd 100644 --- a/cash-ui/src/services/auth.js +++ b/cash-ui/src/services/auth.js @@ -9,6 +9,10 @@ export async function logoutUser(jwt, userId) { if (res.data?.success) { localStorage.removeItem('access_token'); localStorage.removeItem('userId'); + localStorage.removeItem('username'); + localStorage.removeItem('email'); + localStorage.removeItem('firstName'); + localStorage.removeItem('lastName'); return { success: true }; } else { return { success: false, message: res.data?.message || 'Logout failed' }; From 4b3302ec7837573788e11764e946e944aa9aebd1 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 28 Nov 2025 02:29:59 -0500 Subject: [PATCH 04/21] fix agent ui --- cash-ui/src/App.jsx | 54 ++++-------- cash-ui/src/auth/AuthContext.jsx | 107 ++++++++++++++++++++++++ cash-ui/src/components/ChatInput.jsx | 4 +- cash-ui/src/components/ChatMessages.jsx | 10 ++- cash-ui/src/components/ChatPopup.jsx | 25 +++++- cash-ui/src/components/Chatbot.jsx | 4 +- cash-ui/src/components/Navbar.jsx | 10 +-- cash-ui/src/pages/Home.jsx | 68 +++++++++------ cash-ui/src/pages/Login.jsx | 14 ++-- cash-ui/src/pages/Profile.jsx | 14 ++-- 10 files changed, 219 insertions(+), 91 deletions(-) create mode 100644 cash-ui/src/auth/AuthContext.jsx diff --git a/cash-ui/src/App.jsx b/cash-ui/src/App.jsx index d9d1303..747f01a 100644 --- a/cash-ui/src/App.jsx +++ b/cash-ui/src/App.jsx @@ -4,7 +4,7 @@ import { Route, Navigate, } from 'react-router-dom'; -import { useState, useEffect } from 'react'; +import { useAuth, AuthProvider } from './auth/AuthContext'; import Signup from './pages/Signup'; import Login from './pages/Login'; @@ -20,53 +20,25 @@ import PastAuctions from './pages/PastAuctions'; import ChatPopup from './components/ChatPopup'; import Home from './pages/Home'; -function App() { - const [isAuthenticated, setIsAuthenticated] = useState( - !!localStorage.getItem('access_token') - ); - - useEffect(() => { - const handleAuthChange = () => { - setIsAuthenticated(!!localStorage.getItem('access_token')); - }; - - window.addEventListener('auth-changed', handleAuthChange); - return () => window.removeEventListener('auth-changed', handleAuthChange); - }, []); - +function AppRoutes() { + const { isAuthenticated } = useAuth(); return ( + {isAuthenticated && } } /> - } /> } /> } /> } /> {isAuthenticated && ( <> - - : } - /> - : } - /> - : } - /> - : } - /> - : } - /> + } /> + } /> + } /> + } /> + } /> )} } /> @@ -75,4 +47,12 @@ function App() { ); } +function App() { + return ( + + + + ); +} + export default App; diff --git a/cash-ui/src/auth/AuthContext.jsx b/cash-ui/src/auth/AuthContext.jsx new file mode 100644 index 0000000..fb6a76c --- /dev/null +++ b/cash-ui/src/auth/AuthContext.jsx @@ -0,0 +1,107 @@ +import React, { + createContext, + useContext, + useState, + useEffect, + useCallback, +} from 'react'; + +// Helper: decode JWT and check expiration (simple, not verifying signature) +function isTokenValid(token) { + if (!token) return false; + try { + const [, payload] = token.split('.'); + if (!payload) return false; + const decoded = JSON.parse( + atob(payload.replace(/-/g, '+').replace(/_/g, '/')) + ); + if (!decoded.exp) return false; + // exp is in seconds + return Date.now() < decoded.exp * 1000; + } catch (e) { + return false; + } +} + +import { logoutUser } from '../services/auth'; + +const AuthContext = createContext({ + isAuthenticated: false, + user: null, + login: () => {}, + logout: async () => {}, + refresh: () => {}, + loading: false, +}); + +export function AuthProvider({ children }) { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + // Check token validity and set user info + const checkAuth = useCallback(() => { + const token = localStorage.getItem('access_token'); + if (isTokenValid(token)) { + setIsAuthenticated(true); + setUser({ + id: localStorage.getItem('userId'), + username: localStorage.getItem('username'), + email: localStorage.getItem('email'), + firstName: localStorage.getItem('firstName'), + lastName: localStorage.getItem('lastName'), + }); + } else { + setIsAuthenticated(false); + setUser(null); + localStorage.removeItem('access_token'); + } + setLoading(false); + }, []); + + useEffect(() => { + checkAuth(); + const handler = () => checkAuth(); + window.addEventListener('auth-changed', handler); + return () => window.removeEventListener('auth-changed', handler); + }, [checkAuth]); + + const login = useCallback(() => { + checkAuth(); + window.dispatchEvent(new Event('auth-changed')); + }, [checkAuth]); + + const logout = useCallback(async () => { + const jwt = localStorage.getItem('access_token'); + const userId = localStorage.getItem('userId'); + await logoutUser(jwt, userId); // always try, even if fails, clear local + // Remove all user-related localStorage + localStorage.removeItem('access_token'); + localStorage.removeItem('userId'); + localStorage.removeItem('username'); + localStorage.removeItem('email'); + localStorage.removeItem('firstName'); + localStorage.removeItem('lastName'); + setIsAuthenticated(false); + setUser(null); + window.dispatchEvent(new Event('auth-changed')); + }, []); + + const refresh = checkAuth; + + if (loading) { + return
; + } + + return ( + + {children} + + ); +} + +export function useAuth() { + return useContext(AuthContext); +} diff --git a/cash-ui/src/components/ChatInput.jsx b/cash-ui/src/components/ChatInput.jsx index a2eb71e..2c02ed6 100644 --- a/cash-ui/src/components/ChatInput.jsx +++ b/cash-ui/src/components/ChatInput.jsx @@ -21,9 +21,7 @@ function ChatInput({ newMessage, isLoading, setNewMessage, submitNewMessage }) { }; return ( - + - + + CashBot + + + + )} diff --git a/cash-ui/src/components/Chatbot.jsx b/cash-ui/src/components/Chatbot.jsx index 5058d83..eafc515 100644 --- a/cash-ui/src/components/Chatbot.jsx +++ b/cash-ui/src/components/Chatbot.jsx @@ -67,8 +67,8 @@ function Chatbot() { flexGrow: 1, display: 'flex', flexDirection: 'column', - gap: 2, - pt: 2, + // gap: 2, + // pt: 2, height: '100%', }} > diff --git a/cash-ui/src/components/Navbar.jsx b/cash-ui/src/components/Navbar.jsx index b7d390a..ac67c23 100644 --- a/cash-ui/src/components/Navbar.jsx +++ b/cash-ui/src/components/Navbar.jsx @@ -16,12 +16,13 @@ import Logout from '@mui/icons-material/Logout'; import Login from '@mui/icons-material/Login'; import LocalAtmRoundedIcon from '@mui/icons-material/LocalAtmRounded'; import AddCircleIcon from '@mui/icons-material/AddCircle'; -import { logoutUser } from '../services/auth'; + +import { useAuth } from '../auth/AuthContext'; const pages = ['Catalogue', 'My Items', 'Past Auctions', 'Contact']; export default function Navbar() { - const [isAuthenticated] = useState(!!localStorage.getItem('access_token')); + const { isAuthenticated, logout } = useAuth(); const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); @@ -38,9 +39,7 @@ export default function Navbar() { window.location.href = '/profile'; }; const handleLogout = async () => { - const jwt = localStorage.getItem('access_token'); - const userId = localStorage.getItem('userId'); - await logoutUser(jwt, userId); + await logout(); window.location.href = '/login'; }; return ( @@ -104,6 +103,7 @@ export default function Navbar() { { const interval = setInterval(() => { @@ -72,32 +74,50 @@ export default function HomePage() { you're signed in. - - - - + + ) : ( + - Sign Up - - + + + + )} {/* Feature Cards with AI box */} diff --git a/cash-ui/src/pages/Login.jsx b/cash-ui/src/pages/Login.jsx index 9889b90..f69c4e3 100644 --- a/cash-ui/src/pages/Login.jsx +++ b/cash-ui/src/pages/Login.jsx @@ -9,12 +9,13 @@ import { Alert, } from '@mui/material'; import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; -import { useNavigate } from 'react-router-dom'; + +import { useAuth } from '../auth/AuthContext'; export default function Login() { - const navigate = useNavigate(); const [form, setForm] = useState({ username: '', password: '' }); const [error, setError] = useState(''); + const { login } = useAuth(); const handleChange = (e) => setForm({ ...form, [e.target.name]: e.target.value }); @@ -48,7 +49,6 @@ export default function Login() { localStorage.setItem('access_token', data.jwt); localStorage.setItem('userId', data.userId); - window.dispatchEvent(new Event('auth-changed')); const user = await fetch(`/api/users/${data.userId}`, { method: 'GET', @@ -62,7 +62,8 @@ export default function Login() { localStorage.setItem('email', userData.email); localStorage.setItem('firstName', userData.firstName); localStorage.setItem('lastName', userData.lastName); - navigate('/catalogue'); + login(); + window.location.href = '/catalogue'; } catch (err) { console.error(err); setError('Could not connect to the server. Please try again.'); @@ -122,12 +123,13 @@ export default function Login() { - diff --git a/cash-ui/src/pages/Profile.jsx b/cash-ui/src/pages/Profile.jsx index c96822d..a954fc3 100644 --- a/cash-ui/src/pages/Profile.jsx +++ b/cash-ui/src/pages/Profile.jsx @@ -8,7 +8,7 @@ import Button from '@mui/material/Button'; import { deepOrange } from '@mui/material/colors'; import LogoutIcon from '@mui/icons-material/Logout'; import apiClient from '../api/api'; -import { logoutUser } from '../services/auth'; +import { useAuth } from '../auth/AuthContext'; const Profile = () => { const [profile, setProfile] = useState(null); @@ -16,9 +16,9 @@ const Profile = () => { const [error, setError] = useState(''); const [signoutLoading, setSignoutLoading] = useState(false); - // Get userId and jwt from localStorage (or your auth context) + // Get userId from localStorage (for fetching profile) const userId = localStorage.getItem('userId'); - const jwt = localStorage.getItem('access_token'); + const { logout } = useAuth(); useEffect(() => { const fetchProfile = async () => { @@ -43,12 +43,8 @@ const Profile = () => { const handleSignOut = async () => { setSignoutLoading(true); setError(''); - const result = await logoutUser(jwt, userId); - if (result.success) { - window.location.href = '/login'; - } else { - setError(result.message || 'Logout failed'); - } + await logout(); + window.location.href = '/login'; setSignoutLoading(false); }; From 468be625d8105bf4741f4576f1ac22c4271e996b Mon Sep 17 00:00:00 2001 From: David Zinkiv <120414753+dzinkiv@users.noreply.github.com> Date: Fri, 28 Nov 2025 02:32:06 -0500 Subject: [PATCH 05/21] Delete cash-ui/src/hooks/useAutoScroll.js --- cash-ui/src/hooks/useAutoScroll.js | 63 ------------------------------ 1 file changed, 63 deletions(-) delete mode 100644 cash-ui/src/hooks/useAutoScroll.js diff --git a/cash-ui/src/hooks/useAutoScroll.js b/cash-ui/src/hooks/useAutoScroll.js deleted file mode 100644 index fa208e9..0000000 --- a/cash-ui/src/hooks/useAutoScroll.js +++ /dev/null @@ -1,63 +0,0 @@ -import { useEffect, useLayoutEffect, useRef } from 'react'; - -const SCROLL_THRESHOLD = 10; - -function useAutoScroll(active) { - const scrollContentRef = useRef(null); - const isDisabled = useRef(false); - const prevScrollTop = useRef(null); - - useEffect(() => { - const resizeObserver = new ResizeObserver(() => { - const { scrollHeight, clientHeight, scrollTop } = - document.documentElement; - if (!isDisabled.current && scrollHeight - clientHeight > scrollTop) { - document.documentElement.scrollTo({ - top: scrollHeight - clientHeight, - behavior: 'smooth', - }); - } - }); - - if (scrollContentRef.current) { - resizeObserver.observe(scrollContentRef.current); - } - - return () => resizeObserver.disconnect(); - }, []); - - useLayoutEffect(() => { - if (!active) { - isDisabled.current = true; - return; - } - - function onScroll() { - const { scrollHeight, clientHeight, scrollTop } = - document.documentElement; - if ( - !isDisabled.current && - window.scrollY < prevScrollTop.current && - scrollHeight - clientHeight > scrollTop + SCROLL_THRESHOLD - ) { - isDisabled.current = true; - } else if ( - isDisabled.current && - scrollHeight - clientHeight <= scrollTop + SCROLL_THRESHOLD - ) { - isDisabled.current = false; - } - prevScrollTop.current = window.scrollY; - } - - isDisabled.current = false; - prevScrollTop.current = document.documentElement.scrollTop; - window.addEventListener('scroll', onScroll); - - return () => window.removeEventListener('scroll', onScroll); - }, [active]); - - return scrollContentRef; -} - -export default useAutoScroll; From 67ef20ea4d48a2bda9ca97aaef0463428f91f5d3 Mon Sep 17 00:00:00 2001 From: David Zinkiv <120414753+dzinkiv@users.noreply.github.com> Date: Fri, 28 Nov 2025 02:32:23 -0500 Subject: [PATCH 06/21] Delete cash-ui/src/hooks/useAutosize.js --- cash-ui/src/hooks/useAutosize.js | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 cash-ui/src/hooks/useAutosize.js diff --git a/cash-ui/src/hooks/useAutosize.js b/cash-ui/src/hooks/useAutosize.js deleted file mode 100644 index 08d4dbd..0000000 --- a/cash-ui/src/hooks/useAutosize.js +++ /dev/null @@ -1,22 +0,0 @@ -import { useState, useLayoutEffect, useRef } from 'react'; - -function useAutosize(value) { - const ref = useRef(null); - const [borderWidth, setBorderWidth] = useState(0); - - useLayoutEffect(() => { - const style = window.getComputedStyle(ref.current); - setBorderWidth( - parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth) - ); - }, []); - - useLayoutEffect(() => { - ref.current.style.height = 'inherit'; - ref.current.style.height = `${ref.current.scrollHeight + borderWidth}px`; - }, [value, borderWidth]); - - return ref; -} - -export default useAutosize; From f434233609a51e313cf661fdaac276d9d27d377f Mon Sep 17 00:00:00 2001 From: david Date: Fri, 28 Nov 2025 03:06:27 -0500 Subject: [PATCH 07/21] add dockerfile --- cash-ui/Dockerfile | 33 +++++++++++++++++++++++++++++++++ cash-ui/nginx.conf | 18 ++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 cash-ui/Dockerfile create mode 100644 cash-ui/nginx.conf diff --git a/cash-ui/Dockerfile b/cash-ui/Dockerfile new file mode 100644 index 0000000..2e0196b --- /dev/null +++ b/cash-ui/Dockerfile @@ -0,0 +1,33 @@ +# Use official Node.js image as the build environment +FROM node:20-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy package.json and package-lock.json (if present) +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy the rest of the app source code +COPY . . + +# Build the app for production +RUN npm run build + +# --- +# Production image: serve with nginx +FROM nginx:alpine + +# Copy built assets from builder +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy custom nginx config (for SPA routing) +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port 80 +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/cash-ui/nginx.conf b/cash-ui/nginx.conf new file mode 100644 index 0000000..a1b9525 --- /dev/null +++ b/cash-ui/nginx.conf @@ -0,0 +1,18 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + # Optional: serve static files directly + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } +} From 11123c9c71ae8a1be9dfe933cb61d3b5aec09a39 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 28 Nov 2025 03:09:46 -0500 Subject: [PATCH 08/21] add start script to initialize cash-ui development environment --- start.sh | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 start.sh diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..c847ff4 --- /dev/null +++ b/start.sh @@ -0,0 +1,2 @@ +cd cash-ui +npm run dev \ No newline at end of file From 0e8863c3397c3b093abc344f94dc17305e77955b Mon Sep 17 00:00:00 2001 From: david Date: Fri, 28 Nov 2025 03:43:31 -0500 Subject: [PATCH 09/21] fix client --- cash-ui/.env.example | 2 ++ cash-ui/src/pages/ForgotPassword.jsx | 25 ++++++++++------------- cash-ui/src/pages/Login.jsx | 30 +++++++--------------------- start.sh | 1 + 4 files changed, 20 insertions(+), 38 deletions(-) create mode 100644 cash-ui/.env.example diff --git a/cash-ui/.env.example b/cash-ui/.env.example new file mode 100644 index 0000000..82e2796 --- /dev/null +++ b/cash-ui/.env.example @@ -0,0 +1,2 @@ +VITE_API_URL=https://router-service-production.up.railway.app/api +VITE_AI_API_URL=https://ai-agent-production-3e4a.up.railway.app/api \ No newline at end of file diff --git a/cash-ui/src/pages/ForgotPassword.jsx b/cash-ui/src/pages/ForgotPassword.jsx index b0bc57e..b66a37d 100644 --- a/cash-ui/src/pages/ForgotPassword.jsx +++ b/cash-ui/src/pages/ForgotPassword.jsx @@ -9,7 +9,8 @@ import { Alert, } from '@mui/material'; import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; -import { useNavigate } from 'react-router-dom'; + +import apiClient from '../api/api'; export default function ForgotPassword() { const navigate = useNavigate(); @@ -23,23 +24,17 @@ export default function ForgotPassword() { setError(''); setNotice(''); - const res = await fetch('/api/users/forgot-password', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, email }), - }); - - if (!res.ok) { - const data = await res.json(); + try { + await apiClient.post('/users/forgot-password', { username, email }); + setNotice( + 'If the username and email exist, a password reset link was sent.' + ); + } catch (err) { setError( - data.message || 'Unable to send reset link. Email may not exist.' + err.response?.data?.message || + 'Unable to send reset link. Email may not exist.' ); - return; } - - setNotice( - 'If the username and email exist, a password reset link was sent.' - ); }; return ( diff --git a/cash-ui/src/pages/Login.jsx b/cash-ui/src/pages/Login.jsx index f69c4e3..ab9b0c3 100644 --- a/cash-ui/src/pages/Login.jsx +++ b/cash-ui/src/pages/Login.jsx @@ -11,6 +11,7 @@ import { import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import { useAuth } from '../auth/AuthContext'; +import apiClient from '../api/api'; export default function Login() { const [form, setForm] = useState({ username: '', password: '' }); @@ -25,22 +26,11 @@ export default function Login() { setError(''); try { - const res = await fetch('/api/users/signin', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - username: form.username, - password: form.password, - }), - credentials: 'include', + const res = await apiClient.post('/users/signin', { + username: form.username, + password: form.password, }); - - const data = await res.json(); - - if (!res.ok) { - setError(data.message || 'Invalid username or password.'); - return; - } + const data = res.data; if (!data.jwt) { setError('Login failed. No access token returned.'); @@ -50,14 +40,8 @@ export default function Login() { localStorage.setItem('access_token', data.jwt); localStorage.setItem('userId', data.userId); - const user = await fetch(`/api/users/${data.userId}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${data.jwt}`, - }, - }); - const userData = await user.json(); + const userRes = await apiClient.get(`/users/${data.userId}`); + const userData = userRes.data; localStorage.setItem('username', userData.username); localStorage.setItem('email', userData.email); localStorage.setItem('firstName', userData.firstName); diff --git a/start.sh b/start.sh index c847ff4..3412efb 100644 --- a/start.sh +++ b/start.sh @@ -1,2 +1,3 @@ cd cash-ui +npm install npm run dev \ No newline at end of file From 3765136252ec119a9c559d8daa2dfb8dd809bc0e Mon Sep 17 00:00:00 2001 From: david Date: Fri, 28 Nov 2025 03:55:07 -0500 Subject: [PATCH 10/21] add GitHub Actions workflow to publish Docker image to GHCR --- .github/workflows/publish-docker-image.yml | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/publish-docker-image.yml diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml new file mode 100644 index 0000000..1f42fb7 --- /dev/null +++ b/.github/workflows/publish-docker-image.yml @@ -0,0 +1,44 @@ +name: Publish Docker image to GHCR + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + packages: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/cash-ecomm/ai-agent + tags: | + type=ref,event=branch,branch=main + type=sha + latest + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} From 2a04ce0055d35cb3d123ea284485a5b5670a5c94 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 28 Nov 2025 03:56:58 -0500 Subject: [PATCH 11/21] remove start script and reintroduce GitHub Actions workflow for Docker image publishing --- .../.github}/workflows/publish-docker-image.yml | 0 start.sh | 3 --- 2 files changed, 3 deletions(-) rename {.github => cash-ui/.github}/workflows/publish-docker-image.yml (100%) delete mode 100644 start.sh diff --git a/.github/workflows/publish-docker-image.yml b/cash-ui/.github/workflows/publish-docker-image.yml similarity index 100% rename from .github/workflows/publish-docker-image.yml rename to cash-ui/.github/workflows/publish-docker-image.yml diff --git a/start.sh b/start.sh deleted file mode 100644 index 3412efb..0000000 --- a/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -cd cash-ui -npm install -npm run dev \ No newline at end of file From 6d24f45a5911cc583576a93501a60d1283047171 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 28 Nov 2025 03:58:58 -0500 Subject: [PATCH 12/21] reintroduce GitHub Actions workflow for Docker image publishing --- .../.github => .github}/workflows/publish-docker-image.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename {cash-ui/.github => .github}/workflows/publish-docker-image.yml (89%) diff --git a/cash-ui/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml similarity index 89% rename from cash-ui/.github/workflows/publish-docker-image.yml rename to .github/workflows/publish-docker-image.yml index 1f42fb7..db0d2ce 100644 --- a/cash-ui/.github/workflows/publish-docker-image.yml +++ b/.github/workflows/publish-docker-image.yml @@ -29,7 +29,7 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ghcr.io/cash-ecomm/ai-agent + images: ghcr.io/cash-ecomm/web-app tags: | type=ref,event=branch,branch=main type=sha @@ -38,7 +38,8 @@ jobs: - name: Build and push Docker image uses: docker/build-push-action@v6 with: - context: . + context: ./cash-ui + file: ./cash-ui/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From e7b25622cfc2fb56cccccea5d882e42c041a514c Mon Sep 17 00:00:00 2001 From: david Date: Sun, 30 Nov 2025 20:30:26 -0500 Subject: [PATCH 13/21] fix env example --- cash-ui/.env.example | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cash-ui/.env.example b/cash-ui/.env.example index 82e2796..4f9d6d1 100644 --- a/cash-ui/.env.example +++ b/cash-ui/.env.example @@ -1,2 +1,7 @@ -VITE_API_URL=https://router-service-production.up.railway.app/api -VITE_AI_API_URL=https://ai-agent-production-3e4a.up.railway.app/api \ No newline at end of file +# Local development environment variables +VITE_API_URL=localhost:8080/api +VITE_AI_API_URL=localhost:8000/api + +# Production environment variables +# VITE_API_URL=https://router-service-production.up.railway.app/api +# VITE_AI_API_URL=https://ai-agent-production-3e4a.up.railway.app/api \ No newline at end of file From b0071328306c2b3bd7775ac3366e9d0c11478033 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 30 Nov 2025 20:47:51 -0500 Subject: [PATCH 14/21] fix example env --- cash-ui/.env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cash-ui/.env.example b/cash-ui/.env.example index 4f9d6d1..26c4af0 100644 --- a/cash-ui/.env.example +++ b/cash-ui/.env.example @@ -1,6 +1,6 @@ # Local development environment variables -VITE_API_URL=localhost:8080/api -VITE_AI_API_URL=localhost:8000/api +VITE_API_URL=http://localhost:8080/api +VITE_AI_API_URL=http://localhost:8000/api # Production environment variables # VITE_API_URL=https://router-service-production.up.railway.app/api From 20cfbe26988c70bed13c395f4c01c05e0837a4fb Mon Sep 17 00:00:00 2001 From: david Date: Sun, 30 Nov 2025 22:54:57 -0500 Subject: [PATCH 15/21] remove Spinner component --- cash-ui/src/components/Spinner.jsx | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 cash-ui/src/components/Spinner.jsx diff --git a/cash-ui/src/components/Spinner.jsx b/cash-ui/src/components/Spinner.jsx deleted file mode 100644 index 8130fd8..0000000 --- a/cash-ui/src/components/Spinner.jsx +++ /dev/null @@ -1,10 +0,0 @@ -function Spinner() { - return ( -
- - -
- ); -} - -export default Spinner; From 50aabdcf86ca8a85dc15b73df4fa83cb3faf93b6 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 1 Dec 2025 01:31:27 -0500 Subject: [PATCH 16/21] fix login error --- cash-ui/src/api/api.js | 18 ++++++++++++++---- cash-ui/src/components/ChatPopup.jsx | 4 ++-- cash-ui/src/pages/Login.jsx | 6 +++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/cash-ui/src/api/api.js b/cash-ui/src/api/api.js index f5fafbe..c9fc443 100644 --- a/cash-ui/src/api/api.js +++ b/cash-ui/src/api/api.js @@ -31,10 +31,20 @@ apiClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { - // Token is invalid or expired - localStorage.removeItem('access_token'); - localStorage.removeItem('userId'); - window.location.href = '/login'; // Redirect to login + // Don't redirect if already on login/signup/auth pages or if it's a login attempt + const isAuthEndpoint = error.config?.url?.includes('/users/signin') || + error.config?.url?.includes('/users/signup') || + error.config?.url?.includes('/users/forgot-password'); + const isOnAuthPage = window.location.pathname === '/login' || + window.location.pathname === '/signup' || + window.location.pathname === '/forgot-password'; + + if (!isAuthEndpoint && !isOnAuthPage) { + // Token is invalid or expired + localStorage.removeItem('access_token'); + localStorage.removeItem('userId'); + window.location.href = '/login'; // Redirect to login + } } return Promise.reject(error); } diff --git a/cash-ui/src/components/ChatPopup.jsx b/cash-ui/src/components/ChatPopup.jsx index d5d5a15..5377e91 100644 --- a/cash-ui/src/components/ChatPopup.jsx +++ b/cash-ui/src/components/ChatPopup.jsx @@ -42,10 +42,10 @@ export default function ChatPopup() { bottom: 100, right: 24, bgcolor: 'background.paper', - border: 1, + // border: 1, borderColor: 'divider', boxShadow: 6, - borderRadius: 3, + borderRadius: 4, width: 500, height: 600, zIndex: 9999, diff --git a/cash-ui/src/pages/Login.jsx b/cash-ui/src/pages/Login.jsx index ab9b0c3..1c41544 100644 --- a/cash-ui/src/pages/Login.jsx +++ b/cash-ui/src/pages/Login.jsx @@ -50,7 +50,11 @@ export default function Login() { window.location.href = '/catalogue'; } catch (err) { console.error(err); - setError('Could not connect to the server. Please try again.'); + const message = + err.response?.data?.message || + err.response?.data || + 'Could not connect to the server. Please try again.'; + setError(message); } }; From 7f935274b557099c12a0687727c389271814c304 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 1 Dec 2025 01:31:41 -0500 Subject: [PATCH 17/21] formatting --- cash-ui/src/api/api.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cash-ui/src/api/api.js b/cash-ui/src/api/api.js index c9fc443..a2ba39b 100644 --- a/cash-ui/src/api/api.js +++ b/cash-ui/src/api/api.js @@ -32,13 +32,15 @@ apiClient.interceptors.response.use( (error) => { if (error.response?.status === 401) { // Don't redirect if already on login/signup/auth pages or if it's a login attempt - const isAuthEndpoint = error.config?.url?.includes('/users/signin') || + const isAuthEndpoint = + error.config?.url?.includes('/users/signin') || error.config?.url?.includes('/users/signup') || error.config?.url?.includes('/users/forgot-password'); - const isOnAuthPage = window.location.pathname === '/login' || + const isOnAuthPage = + window.location.pathname === '/login' || window.location.pathname === '/signup' || window.location.pathname === '/forgot-password'; - + if (!isAuthEndpoint && !isOnAuthPage) { // Token is invalid or expired localStorage.removeItem('access_token'); From a53cac92ee011fc6fe19d4e45c488d975717629a Mon Sep 17 00:00:00 2001 From: david Date: Mon, 1 Dec 2025 02:22:27 -0500 Subject: [PATCH 18/21] update navigation --- cash-ui/src/components/ChatMessages.jsx | 9 +++- cash-ui/src/components/ChatPopup.jsx | 60 ++++++++++++++++++++----- cash-ui/src/components/Chatbot.jsx | 13 +----- cash-ui/src/components/Navbar.jsx | 19 ++++---- cash-ui/src/pages/ForgotPassword.jsx | 1 + cash-ui/src/pages/Home.jsx | 10 +++-- cash-ui/src/pages/ItemUpload.jsx | 4 +- cash-ui/src/pages/Login.jsx | 15 +++++-- cash-ui/src/pages/Profile.jsx | 4 +- 9 files changed, 94 insertions(+), 41 deletions(-) diff --git a/cash-ui/src/components/ChatMessages.jsx b/cash-ui/src/components/ChatMessages.jsx index 5e02645..7033fc2 100644 --- a/cash-ui/src/components/ChatMessages.jsx +++ b/cash-ui/src/components/ChatMessages.jsx @@ -4,12 +4,17 @@ import Box from '@mui/material/Box'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import { useEffect, useRef } from 'react'; +import { useTheme } from '@mui/material/styles'; const USER_BUBBLE_COLOR = '#3c80daff'; -const ASSISTANT_BUBBLE_COLOR = '#E9E9EB'; function ChatMessages({ messages, isLoading }) { const scrollRef = useRef(null); + const theme = useTheme(); + + // Use theme-aware color for assistant bubble + const assistantBubbleColor = + theme.palette.mode === 'dark' ? theme.palette.grey[800] : '#E9E9EB'; // Auto-scroll to bottom when messages change useEffect(() => { @@ -48,7 +53,7 @@ function ChatMessages({ messages, isLoading }) { sx={{ px: 1.5, maxWidth: '85%', - bgcolor: isUser ? USER_BUBBLE_COLOR : ASSISTANT_BUBBLE_COLOR, + bgcolor: isUser ? USER_BUBBLE_COLOR : assistantBubbleColor, color: isUser ? 'common.white' : 'text.primary', borderRadius: isUser ? '18px 18px 2px 18px' diff --git a/cash-ui/src/components/ChatPopup.jsx b/cash-ui/src/components/ChatPopup.jsx index 5377e91..6accb24 100644 --- a/cash-ui/src/components/ChatPopup.jsx +++ b/cash-ui/src/components/ChatPopup.jsx @@ -1,12 +1,36 @@ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; +import { useImmer } from 'use-immer'; import Chatbot from './Chatbot'; import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; +import Tooltip from '@mui/material/Tooltip'; import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; +import RefreshIcon from '@mui/icons-material/Refresh'; + +function getInitialMessage() { + const userName = localStorage.getItem('firstName') || 'there'; + return [ + { + role: 'assistant', + content: `Hey, ${userName}! How can I assist you today?`, + }, + ]; +} export default function ChatPopup() { const [open, setOpen] = useState(false); + + // Lift chat state up so it persists across page navigations + const [chatId, setChatId] = useState(null); + const [messages, setMessages] = useImmer(getInitialMessage); + + // Reset chat to initial state + const handleResetChat = useCallback(() => { + setChatId(null); + setMessages(getInitialMessage()); + }, [setMessages]); + return ( <> - - CashBot - + {/* Spacer for centering */} + CashBot + + + + + + - + )} diff --git a/cash-ui/src/components/Chatbot.jsx b/cash-ui/src/components/Chatbot.jsx index eafc515..8ebe992 100644 --- a/cash-ui/src/components/Chatbot.jsx +++ b/cash-ui/src/components/Chatbot.jsx @@ -1,21 +1,10 @@ import { useState } from 'react'; -import { useImmer } from 'use-immer'; import { createChat, sendChatMessage } from '../api/api'; import ChatMessages from './ChatMessages'; import ChatInput from './ChatInput'; import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -function Chatbot() { - const userName = localStorage.getItem('firstName') || 'there'; - - const [chatId, setChatId] = useState(null); - const [messages, setMessages] = useImmer([ - { - role: 'assistant', - content: `Hey, ${userName}! How can I assist you today?`, - }, - ]); +function Chatbot({ chatId, setChatId, messages, setMessages }) { const [newMessage, setNewMessage] = useState(''); const lastMessage = messages[messages.length - 1]; diff --git a/cash-ui/src/components/Navbar.jsx b/cash-ui/src/components/Navbar.jsx index 0ba2d5d..6bfc00a 100644 --- a/cash-ui/src/components/Navbar.jsx +++ b/cash-ui/src/components/Navbar.jsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { useState } from 'react'; +import { Link as RouterLink, useNavigate } from 'react-router-dom'; import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; import Box from '@mui/material/Box'; @@ -19,13 +20,13 @@ import AddCircleIcon from '@mui/icons-material/AddCircle'; import SettingsBrightnessIcon from '@mui/icons-material/SettingsBrightness'; import Brightness7Icon from '@mui/icons-material/Brightness7'; import Brightness4Icon from '@mui/icons-material/Brightness4'; -import { logoutUser } from '../services/auth'; import { useAuth } from '../auth/AuthContext'; const pages = ['Catalogue', 'My Items', 'Past Auctions', 'Contact']; export default function Navbar({ themePreference, setThemePreference }) { const { isAuthenticated, logout } = useAuth(); + const navigate = useNavigate(); const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); @@ -33,17 +34,17 @@ export default function Navbar({ themePreference, setThemePreference }) { setAnchorEl(event.currentTarget); }; const handleItemUpload = () => { - window.location.href = '/upload-item'; + navigate('/upload-item'); }; const handleClose = () => { setAnchorEl(null); }; const handleMyAccount = () => { - window.location.href = '/profile'; + navigate('/profile'); }; const handleLogout = async () => { await logout(); - window.location.href = '/login'; + navigate('/login'); }; // Theme menu state const [themeAnchorEl, setThemeAnchorEl] = React.useState(null); @@ -73,8 +74,8 @@ export default function Navbar({ themePreference, setThemePreference }) { {page} @@ -217,10 +219,11 @@ export default function Navbar({ themePreference, setThemePreference }) { diff --git a/cash-ui/src/pages/ForgotPassword.jsx b/cash-ui/src/pages/ForgotPassword.jsx index b66a37d..78d9bbb 100644 --- a/cash-ui/src/pages/ForgotPassword.jsx +++ b/cash-ui/src/pages/ForgotPassword.jsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Avatar, Button, diff --git a/cash-ui/src/pages/Home.jsx b/cash-ui/src/pages/Home.jsx index 6ea953b..f503d38 100644 --- a/cash-ui/src/pages/Home.jsx +++ b/cash-ui/src/pages/Home.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Link as RouterLink } from 'react-router-dom'; import { motion } from 'framer-motion'; import { Button, @@ -82,10 +83,11 @@ export default function HomePage() { style={{ display: 'flex', justifyContent: 'center', gap: '1rem' }} > - diff --git a/cash-ui/src/pages/Profile.jsx b/cash-ui/src/pages/Profile.jsx index acb01d9..09c28e9 100644 --- a/cash-ui/src/pages/Profile.jsx +++ b/cash-ui/src/pages/Profile.jsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import Box from '@mui/material/Box'; import Avatar from '@mui/material/Avatar'; import Typography from '@mui/material/Typography'; @@ -15,6 +16,7 @@ const Profile = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [signoutLoading, setSignoutLoading] = useState(false); + const navigate = useNavigate(); // Get userId from localStorage (for fetching profile) const userId = localStorage.getItem('userId'); @@ -44,7 +46,7 @@ const Profile = () => { setSignoutLoading(true); setError(''); await logout(); - window.location.href = '/login'; + navigate('/login'); setSignoutLoading(false); }; From 3620085f0ab193d2c910997a077b4fe193e6b714 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 1 Dec 2025 02:23:55 -0500 Subject: [PATCH 19/21] fix routing --- cash-ui/src/pages/Login.jsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cash-ui/src/pages/Login.jsx b/cash-ui/src/pages/Login.jsx index 09256f1..49e9aa4 100644 --- a/cash-ui/src/pages/Login.jsx +++ b/cash-ui/src/pages/Login.jsx @@ -119,12 +119,7 @@ export default function Login() { Forgot password? - From f089dae597fe8dae0d0e7178478c88827f0aa279 Mon Sep 17 00:00:00 2001 From: David Zinkiv <120414753+dzinkiv@users.noreply.github.com> Date: Mon, 1 Dec 2025 08:23:19 -0500 Subject: [PATCH 20/21] Update publish-docker-image.yml --- .github/workflows/publish-docker-image.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml index db0d2ce..923d70d 100644 --- a/.github/workflows/publish-docker-image.yml +++ b/.github/workflows/publish-docker-image.yml @@ -4,7 +4,6 @@ on: push: branches: - main - pull_request: permissions: contents: read From 58d241c524e46b6375611018b6a7359c166db6e2 Mon Sep 17 00:00:00 2001 From: Mark Ngo Date: Mon, 1 Dec 2025 14:45:48 -0500 Subject: [PATCH 21/21] Bugfix: Remove activeBidItemId from local storage after user logs out. --- cash-ui/src/auth/AuthContext.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/cash-ui/src/auth/AuthContext.jsx b/cash-ui/src/auth/AuthContext.jsx index fb6a76c..b47817d 100644 --- a/cash-ui/src/auth/AuthContext.jsx +++ b/cash-ui/src/auth/AuthContext.jsx @@ -82,6 +82,7 @@ export function AuthProvider({ children }) { localStorage.removeItem('email'); localStorage.removeItem('firstName'); localStorage.removeItem('lastName'); + localStorage.removeItem('activeBidItemId'); setIsAuthenticated(false); setUser(null); window.dispatchEvent(new Event('auth-changed'));