diff --git a/frontend/bun.lock b/frontend/bun.lock index 45be85ff4..b47eefdd2 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -9,7 +9,7 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", - "@react-router/node": "^7.8.2", + "@react-router/node": "^7.9.1", "@tanstack/react-query": "^5.89.0", "@tauri-apps/api": "^2.8.0", "@tauri-apps/plugin-opener": "^2.5.0", @@ -19,6 +19,7 @@ "echarts": "^6.0.0", "isbot": "^5", "lucide-react": "^0.544.0", + "mutative": "^1.3.0", "overlayscrollbars": "^2.12.0", "overlayscrollbars-react": "^0.5.6", "react": "^19.1.1", @@ -28,8 +29,8 @@ }, "devDependencies": { "@biomejs/biome": "^2.2.4", - "@react-router/dev": "^7.8.2", - "@react-router/serve": "^7.8.2", + "@react-router/dev": "^7.9.1", + "@react-router/serve": "^7.9.1", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.13", "@tauri-apps/cli": "^2.8.4", @@ -317,13 +318,13 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.1.tgz", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@react-router/dev": ["@react-router/dev@7.8.2", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@npmcli/package-json": "^4.0.1", "@react-router/node": "7.8.2", "@vitejs/plugin-rsc": "0.4.11", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "pathe": "^1.1.2", "picocolors": "^1.1.1", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "set-cookie-parser": "^2.6.0", "tinyglobby": "^0.2.14", "valibot": "^0.41.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.8.2", "react-router": "^7.8.2", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-9ilgQoNhvgvUyQKDapALt9qVO3GpSw9ng5X2BwIhLIwqh8CTyRM/jz5cK53p5yzGiVeyx9njXXfeuxUlvQgJuA=="], + "@react-router/dev": ["@react-router/dev@7.9.1", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@npmcli/package-json": "^4.0.1", "@react-router/node": "7.9.1", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "pathe": "^1.1.2", "picocolors": "^1.1.1", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "set-cookie-parser": "^2.6.0", "tinyglobby": "^0.2.14", "valibot": "^0.41.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.9.1", "@vitejs/plugin-rsc": "*", "react-router": "^7.9.1", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "@vitejs/plugin-rsc", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-fW/qubsdHq1nsufHPLpXa6hiNvXXV9JBtWqRlJ02OOhFeaWERZw4rGoHjG1DCg8/QTTadgbzplmP97ZnzWPkcA=="], - "@react-router/express": ["@react-router/express@7.8.2", "", { "dependencies": { "@react-router/node": "7.8.2" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.8.2", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-AJUNsE5Q+vD8TsNlKTw2MGUUnp/QJGlRV1jG2ItV30lwIx2wE7d4NHx/jWkGZIEblHQBTpodcp6MFirZXbisJw=="], + "@react-router/express": ["@react-router/express@7.9.1", "", { "dependencies": { "@react-router/node": "7.9.1" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.9.1", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-d1sfsD3AJXZj+C5k3jAmxAD3vJXGfoh3lNmtSwxp0NdZFHI54zPC5S9o80cy3P8p6Gc7XzSEQJYk9k7fAM/AIw=="], - "@react-router/node": ["@react-router/node@7.8.2", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.8.2", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-FNepNg4Aya6V0ZxD/+uObtqxtMXcsBGa0ax9PznUh5qr8g4M6Xo9IN+soLb1tghz6iS/F9djFyhJ/lDkF77dEw=="], + "@react-router/node": ["@react-router/node@7.9.1", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.9.1", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-XfyVLM+sDUDB1frGNr7iqaKNglrPwBiUp8+sFdL4///bngy49pUb2RuEtn2J2Cy5yjL+IlKbjJFrsmfimLBmeg=="], - "@react-router/serve": ["@react-router/serve@7.8.2", "", { "dependencies": { "@react-router/express": "7.8.2", "@react-router/node": "7.8.2", "compression": "^1.7.4", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.0", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.8.2" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-1AwKjBWmyWWA7dGCRjn2glWwO6cA7dDX7roP1tosFi5cu1EvqHaqelRH6K6MZSV10Tv6oPtFG7rgV+rCafJvyw=="], + "@react-router/serve": ["@react-router/serve@7.9.1", "", { "dependencies": { "@react-router/express": "7.9.1", "@react-router/node": "7.9.1", "compression": "^1.7.4", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.0", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.9.1" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-yVBSb5KsNCdkSoOk186/M5GJtcIvKE32Ax9LhXySVpM+suCSjucI+p2TXDOJIYsBqr2aKcBl/bNBm5sIJxG/HA=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.38", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.38.tgz", { "os": "android", "cpu": "arm64" }, "sha512-AE3HFQrjWCKLFZD1Vpiy+qsqTRwwoil1oM5WsKPSmfQ5fif/A+ZtOZetF32erZdsR7qyvns6qHEteEsF6g6rsQ=="], @@ -455,8 +456,6 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@vitejs/plugin-rsc": ["@vitejs/plugin-rsc@0.4.11", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.7.0", "es-module-lexer": "^1.7.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.17", "periscopic": "^4.0.2", "turbo-stream": "^3.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*", "vite": "*" } }, "sha512-+4H4wLi+Y9yF58znBfKgGfX8zcqUGt8ngnmNgzrdGdF1SVz7EO0sg7WnhK5fFVHt6fUxsVEjmEabsCWHKPL1Tw=="], - "@xmldom/xmldom": ["@xmldom/xmldom@0.9.8", "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.8.tgz", {}, "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A=="], "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], @@ -665,8 +664,6 @@ "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "https://registry.npmmirror.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "eventsource": ["eventsource@3.0.7", "https://registry.npmmirror.com/eventsource/-/eventsource-3.0.7.tgz", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], @@ -811,8 +808,6 @@ "is-promise": ["is-promise@4.0.0", "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], - "is-regexp": ["is-regexp@3.1.0", "https://registry.npmmirror.com/is-regexp/-/is-regexp-3.1.0.tgz", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="], "is-stream": ["is-stream@4.0.1", "https://registry.npmmirror.com/is-stream/-/is-stream-4.0.1.tgz", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], @@ -985,6 +980,8 @@ "msw": ["msw@2.11.2", "https://registry.npmmirror.com/msw/-/msw-2.11.2.tgz", { "dependencies": { "@bundled-es-modules/cookie": "^2.0.1", "@bundled-es-modules/statuses": "^1.0.1", "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.39.1", "@open-draft/deferred-promise": "^2.2.0", "@open-draft/until": "^2.1.0", "@types/cookie": "^0.6.0", "@types/statuses": "^2.0.4", "graphql": "^16.8.1", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.7.0", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^4.26.1", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-MI54hLCsrMwiflkcqlgYYNJJddY5/+S0SnONvhv1owOplvqohKSQyGejpNdUGyCwgs4IH7PqaNbPw/sKOEze9Q=="], + "mutative": ["mutative@1.3.0", "", {}, "sha512-8MJj6URmOZAV70dpFe1YnSppRTKC4DsMkXQiBDFayLcDI4ljGokHxmpqaBQuDWa4iAxWaJJ1PS8vAmbntjjKmQ=="], + "mute-stream": ["mute-stream@2.0.0", "https://registry.npmmirror.com/mute-stream/-/mute-stream-2.0.0.tgz", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], "nanoid": ["nanoid@3.3.11", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -1057,8 +1054,6 @@ "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], - "periscopic": ["periscopic@4.0.2", "", { "dependencies": { "@types/estree": "*", "is-reference": "^3.0.2", "zimmerframe": "^1.0.0" } }, "sha512-sqpQDUy8vgB7ycLkendSKS6HnVz1Rneoc3Rc+ZBUCe2pbqlVuCC5vF52l0NJ1aiMg/r1qfYF9/myz8CZeI2rjA=="], - "picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], @@ -1251,8 +1246,6 @@ "tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="], - "turbo-stream": ["turbo-stream@3.1.0", "", {}, "sha512-tVI25WEXl4fckNEmrq70xU1XumxUwEx/FZD5AgEcV8ri7Wvrg2o7GEq8U7htrNx3CajciGm+kDyhRf5JB6t7/A=="], - "tw-animate-css": ["tw-animate-css@1.3.8", "https://registry.npmmirror.com/tw-animate-css/-/tw-animate-css-1.3.8.tgz", {}, "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw=="], "type-fest": ["type-fest@4.41.0", "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -1315,8 +1308,6 @@ "vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "https://registry.npmmirror.com/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="], - "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "which": ["which@4.0.0", "https://registry.npmmirror.com/which/-/which-4.0.0.tgz", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], @@ -1341,8 +1332,6 @@ "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "https://registry.npmmirror.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], - "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], - "zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.6", "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="], @@ -1397,8 +1386,6 @@ "@tybys/wasm-util/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@vitejs/plugin-rsc/@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.7.0", "", {}, "sha512-un8diyEBKU3BTVj3GzlTPA1kIjCkGdD+AMYQy31Gf9JCkfoZzwgJ79GUtHrF2BN3XPNMLpubbzPcxys+a3uZEw=="], - "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "ansi-escapes/type-fest": ["type-fest@0.21.3", "https://registry.npmmirror.com/type-fest/-/type-fest-0.21.3.tgz", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], diff --git a/frontend/package.json b/frontend/package.json index 758e543dd..1074c9de6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,7 +15,7 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", - "@react-router/node": "^7.8.2", + "@react-router/node": "^7.9.1", "@tanstack/react-query": "^5.89.0", "@tauri-apps/api": "^2.8.0", "@tauri-apps/plugin-opener": "^2.5.0", @@ -25,6 +25,7 @@ "echarts": "^6.0.0", "isbot": "^5", "lucide-react": "^0.544.0", + "mutative": "^1.3.0", "overlayscrollbars": "^2.12.0", "overlayscrollbars-react": "^0.5.6", "react": "^19.1.1", @@ -34,8 +35,8 @@ }, "devDependencies": { "@biomejs/biome": "^2.2.4", - "@react-router/dev": "^7.8.2", - "@react-router/serve": "^7.8.2", + "@react-router/dev": "^7.9.1", + "@react-router/serve": "^7.9.1", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.13", "@tauri-apps/cli": "^2.8.4", diff --git a/frontend/src/app/agent/chat.tsx b/frontend/src/app/agent/chat.tsx index 015e6b84f..aa940b6b4 100644 --- a/frontend/src/app/agent/chat.tsx +++ b/frontend/src/app/agent/chat.tsx @@ -1,23 +1,457 @@ -import { useParams } from "react-router"; +import { ArrowUp, MessageCircle, Settings } from "lucide-react"; +import { + useCallback, + useEffect, + useMemo, + useReducer, + useRef, + useState, +} from "react"; +import { Navigate, useParams } from "react-router"; +import { Button } from "@/components/ui/button"; +import ScrollTextarea, { + type ScrollTextareaRef, +} from "@/components/valuecell/scroll/scroll-textarea"; +import { useSSE } from "@/hooks/use-sse"; +import { updateAgentConversationsStore } from "@/lib/agent-store"; +import { getServerUrl } from "@/lib/api-client"; +import { SSEReadyState } from "@/lib/sse-client"; +import { cn } from "@/lib/utils"; +import { agentData } from "@/mock/agent-data"; +import type { + AgentConversationsStore, + AgentStreamRequest, + SSEData, + ThreadView, +} from "@/types/agent"; +import type { Route } from "./+types/chat"; +import { ChatBackground } from "./components"; +import { ChatMessage as ChatMessageComponent } from "./components/chat-message"; + +// Optimized reducer for agent store management +function agentStoreReducer( + state: AgentConversationsStore, + action: SSEData, +): AgentConversationsStore { + return updateAgentConversationsStore(state, action); +} export default function AgentChat() { - const { agentId } = useParams(); + const { agentId } = useParams(); + const textareaRef = useRef(null); + const [inputValue, setInputValue] = useState(""); + + // Use optimized reducer for state management + const [agentStore, dispatchAgentStore] = useReducer(agentStoreReducer, {}); + const curConversationId = useRef(""); + const curThreadId = useRef(""); + const [isSending, setIsSending] = useState(false); + const [shouldClose, setShouldClose] = useState(false); + + // Get current conversation + const threadEntries = useMemo(() => { + if (!curConversationId.current || !agentStore[curConversationId.current]) { + return null; + } + const threads = agentStore[curConversationId.current].threads; + return threads + ? (Object.entries(threads) as Array<[string, ThreadView]>) + : []; + }, [agentStore]); + + // Handle SSE data events using agent store + const handleSSEData = useCallback( + (sseData: SSEData) => { + console.log("🚀 ~ AgentChat ~ sseData:", sseData); + // Update agent store using the reducer + dispatchAgentStore(sseData); + + // Handle specific UI state updates + const { event, data } = sseData; + switch (event) { + case "conversation_started": { + curConversationId.current = data.conversation_id; + break; + } + + case "thread_started": { + curThreadId.current = data.thread_id; + dispatchAgentStore({ + event: "message_chunk", + data: { + conversation_id: curConversationId.current, + thread_id: data.thread_id, + task_id: "", + subtask_id: "", + payload: { content: inputValue.trim() }, + role: "user", + }, + }); + break; + } + + case "done": { + curThreadId.current = data.thread_id; + setShouldClose(true); + break; + } + + // All message-related events are handled by the store + default: + // Update current thread ID for message events + if ("thread_id" in data) { + curThreadId.current = data.thread_id; + } + break; + } + }, + [inputValue], + ); + + // Initialize SSE connection using the useSSE hook + const { + connect, + close, + state, + error: sseError, + } = useSSE({ + url: getServerUrl("/agents/stream"), + handlers: { + onData: handleSSEData, + onOpen: () => { + console.log("✅ SSE connection opened"); + setIsSending(false); // Reset sending state on open + }, + onError: (error: Error) => { + console.error("❌ SSE connection error:", error); + setIsSending(false); // Reset sending state on error + }, + onClose: () => { + console.log("🔌 SSE connection closed"); + setIsSending(false); // Reset sending state on close + }, + }, + }); + + useEffect(() => { + if (shouldClose) { + close(); + setShouldClose(false); + } + }, [shouldClose]); + + // Derived state - compute from existing state instead of maintaining separately + const isConnected = state === SSEReadyState.OPEN; + const isConnecting = state === SSEReadyState.CONNECTING; + // isStreaming represents when AI is processing/responding, not related to user input requirements + const isStreaming = isConnected && isSending; + + // Send message to agent + const sendMessage = useCallback( + async (message: string) => { + // Prevent duplicate sends + if (isSending || isConnecting) { + console.log( + "Already sending or connecting, ignoring duplicate request", + ); + return; + } + + setIsSending(true); + + try { + const request: AgentStreamRequest = { + query: message, + agent_name: "WarrenBuffettAgent", + conversation_id: curConversationId.current, + }; + + // Connect SSE client with request body to receive streaming response + await connect(JSON.stringify(request)); + } catch (error) { + console.error("Failed to send message:", error); + setIsSending(false); // Reset immediately on error + } + }, + [connect, isSending, isConnecting], + ); + + const handleSendMessage = useCallback(() => { + const trimmedInput = inputValue.trim(); + // Prevent sending while connecting/sending or when input is empty + if (!trimmedInput || isConnecting || isSending) { + console.log("Cannot send: empty input, connecting, or already sending"); + return; + } + + // Always use sendMessage - user input for plan_require_user_input is just normal conversation + sendMessage(trimmedInput); + setInputValue(""); + }, [inputValue, isConnecting, isSending, sendMessage]); + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + const agent = agentData[agentId ?? ""]; + if (!agent) return ; + + // Agent skills/tags + const agentSkills = [ + "Hong Kong stocks", + "US stocks", + "Predictive analysis", + "Stock selection", + ]; return ( -
-
-
-

- Chat with Agent: {agentId} -

-
- {/* Chat interface will be implemented here */} -
- Chat interface coming soon... +
+ {/* Header with agent info and actions */} +
+
+ {/* Agent Avatar */} +
+
+
+ AI +
+
+ + {/* Agent Info */} +
+

+ AI Hedge Fund Agent +

+
+ {agentSkills.slice(0, 2).map((skill) => ( + + {skill} + + ))} + {agentSkills.length > 2 && ( + + +{agentSkills.length - 2} more + + )}
-
+ + {/* Action buttons */} +
+ + +
+
+ + {/* Main content area */} +
+ {!threadEntries?.length ? ( + <> + {/* Background blur effects for welcome screen */} + + + {/* Welcome content */} +
+

+ Welcome to AI hedge fund agent! +

+ + {/* Input card */} +
+ + +
+ + {/* Connection status */} + {sseError && ( +
+ ⚠️ Connection error: {sseError.message} +
+ )} +
+ + ) : ( + <> + {/* Chat messages with optimized rendering */} +
+ {threadEntries.map(([threadId, thread], threadIndex) => { + if (!thread.messages.length) { + return threadEntries.length > 1 ? ( +
+ Thread {threadIndex + 1} 暂无消息 +
+ ) : null; + } + + return ( +
+ {threadEntries.length > 1 && ( +
+ + Thread {threadIndex + 1} + +
+ )} + + {thread.messages.map((message, index) => ( + + ))} +
+ ); + })} + + {/* Streaming indicator */} + {isStreaming && ( +
+
+
+
+
+
+ AI is thinking... +
+ )} +
+ + {/* Input area at bottom */} +
+
+ + +
+ + {/* Status indicators */} +
+
+ +
+ + {isStreaming + ? "Streaming" + : isConnecting + ? "Connecting" + : isConnected + ? "Ready" + : "Disconnected"} + + + {curConversationId.current && ( + Session: {curConversationId.current} + )} + {curThreadId.current && ( + Thread: {curThreadId.current} + )} +
+ Press Enter to send, Shift+Enter for new line +
+ + {sseError && ( +
+ Error: {sseError.message} +
+ )} +
+ + )} +
); } diff --git a/frontend/src/app/agent/components/chat-background.tsx b/frontend/src/app/agent/components/chat-background.tsx new file mode 100644 index 000000000..12ea8761e --- /dev/null +++ b/frontend/src/app/agent/components/chat-background.tsx @@ -0,0 +1,44 @@ +function ChatBackground() { + return ( +
+ {[ + { + position: "left-0", + size: "h-80 w-96", + colors: "from-orange-200 to-orange-300", + }, + { + position: "left-56", + size: "h-80 w-96", + colors: "from-yellow-200 to-yellow-300", + }, + { + position: "left-96", + size: "h-72 w-72", + colors: "from-green-200 to-green-300", + }, + { + position: "right-56", + size: "h-80 w-96", + colors: "from-blue-200 to-blue-300", + }, + { + position: "right-0", + size: "h-72 w-72", + colors: "from-purple-200 to-purple-300", + }, + ].map((blur, index) => ( +
+
+
+ ))} +
+ ); +} + +export default ChatBackground; diff --git a/frontend/src/app/agent/components/chat-message.tsx b/frontend/src/app/agent/components/chat-message.tsx new file mode 100644 index 000000000..031cda017 --- /dev/null +++ b/frontend/src/app/agent/components/chat-message.tsx @@ -0,0 +1,115 @@ +import { Bot, CheckCircle, Clock, FileText, User } from "lucide-react"; +import { memo } from "react"; +import { cn } from "@/lib/utils"; +import type { ChatMessage as ChatMessageType } from "@/types/agent"; + +export interface ChatMessageProps { + message: ChatMessageType; + index: number; + conversationId: string; + threadId: string; +} + +export const ChatMessage = memo(({ message }) => { + return ( +
+ {message.role !== "user" && ( +
+
+ +
+
+ )} + +
+ {/* Render different message types based on payload structure */} + {(() => { + const payload = message.payload; + if (!payload) return null; + + // Component generator message + if ("component_type" in payload && "content" in payload) { + return ( +
+
+
+ + + {payload.component_type} Generated + +
+
+
+                      {payload.content}
+                    
+
+
+
+ ); + } + + // Tool call message + if ("tool_call_id" in payload && "tool_name" in payload) { + const hasResult = + "tool_call_result" in payload && payload.tool_call_result; + return ( +
+
+ {hasResult ? ( + + ) : ( + + )} + + {payload.tool_name} + + {hasResult ? ( + + {String(payload.tool_call_result).substring(0, 50)} + ... + + ) : ( + Running... + )} +
+
+ ); + } + + // Regular content message + if ("content" in payload) { + return ( +
+ {payload.content} +
+ ); + } + + return null; + })()} +
+ + {message.role === "user" && ( +
+
+ +
+
+ )} +
+ ); +}); + +ChatMessage.displayName = "ChatMessage"; diff --git a/frontend/src/app/agent/components/index.tsx b/frontend/src/app/agent/components/index.tsx new file mode 100644 index 000000000..741e24e95 --- /dev/null +++ b/frontend/src/app/agent/components/index.tsx @@ -0,0 +1 @@ +export { default as ChatBackground } from "./chat-background"; \ No newline at end of file diff --git a/frontend/src/app/agent/config.tsx b/frontend/src/app/agent/config.tsx index 4318e814d..8c7a17057 100644 --- a/frontend/src/app/agent/config.tsx +++ b/frontend/src/app/agent/config.tsx @@ -1,14 +1,17 @@ import { ArrowRight } from "lucide-react"; -import { Link, useParams } from "react-router"; +import { Link, Navigate, useParams } from "react-router"; import BackButton from "@/components/valuecell/button/back-button"; import PreviewMarkdown from "@/components/valuecell/markdown/preview-markdown"; -import ScrollContainer from "@/components/valuecell/scroll-container"; +import ScrollContainer from "@/components/valuecell/scroll/scroll-container"; import { agentData } from "@/mock/agent-data"; +import type { Route } from "./+types/config"; export default function AgentConfig() { - const { agentId } = useParams(); + const { agentId } = useParams(); - const agent = agentData[agentId as keyof typeof agentData]; + if (!agentId) return ; + + const agent = agentData[agentId]; return (
diff --git a/frontend/src/app/home/_layout.tsx b/frontend/src/app/home/_layout.tsx index 0ab5596de..33e11076b 100644 --- a/frontend/src/app/home/_layout.tsx +++ b/frontend/src/app/home/_layout.tsx @@ -2,7 +2,7 @@ import { Plus } from "lucide-react"; import { Outlet } from "react-router"; import StockSearchModal from "@/app/home/components/stock-search-modal"; import { Button } from "@/components/ui/button"; -import ScrollContainer from "@/components/valuecell/scroll-container"; +import ScrollContainer from "@/components/valuecell/scroll/scroll-container"; import StockList from "./components/stock-list"; export default function HomeLayout() { diff --git a/frontend/src/app/home/components/stock-list.tsx b/frontend/src/app/home/components/stock-list.tsx index 621b857ab..37ee57a2d 100644 --- a/frontend/src/app/home/components/stock-list.tsx +++ b/frontend/src/app/home/components/stock-list.tsx @@ -7,7 +7,7 @@ import { StockMenuHeader, StockMenuListItem, } from "@/components/valuecell/menus/stock-menus"; -import ScrollContainer from "@/components/valuecell/scroll-container"; +import ScrollContainer from "@/components/valuecell/scroll/scroll-container"; import { stockData } from "@/mock/stock-data"; function StockList() { diff --git a/frontend/src/app/home/components/stock-search-modal.tsx b/frontend/src/app/home/components/stock-search-modal.tsx index 04013e1fd..9fe9cc91e 100644 --- a/frontend/src/app/home/components/stock-search-modal.tsx +++ b/frontend/src/app/home/components/stock-search-modal.tsx @@ -10,7 +10,7 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import ScrollContainer from "@/components/valuecell/scroll-container"; +import ScrollContainer from "@/components/valuecell/scroll/scroll-container"; import { useDebounce } from "@/hooks/use-debounce"; interface StockSearchModalProps { diff --git a/frontend/src/app/home/stock.tsx b/frontend/src/app/home/stock.tsx index b1a4fb6b0..af26fb5fe 100644 --- a/frontend/src/app/home/stock.tsx +++ b/frontend/src/app/home/stock.tsx @@ -9,6 +9,7 @@ import { STOCK_BADGE_COLORS } from "@/constants/stock"; import { formatChange, formatPrice, getChangeType } from "@/lib/utils"; import { stockData } from "@/mock/stock-data"; import type { SparklineData } from "@/types/chart"; +import type { Route } from "./+types/stock"; // Generate historical price data in [timestamp, value] format function generateHistoricalData( @@ -37,7 +38,7 @@ function generateHistoricalData( } const Stock = memo(function Stock() { - const { stockId } = useParams(); + const { stockId } = useParams(); // Find stock information from mock data const stockInfo = useMemo(() => { diff --git a/frontend/src/components/valuecell/scroll-container.tsx b/frontend/src/components/valuecell/scroll/scroll-container.tsx similarity index 100% rename from frontend/src/components/valuecell/scroll-container.tsx rename to frontend/src/components/valuecell/scroll/scroll-container.tsx diff --git a/frontend/src/components/valuecell/scroll/scroll-textarea.tsx b/frontend/src/components/valuecell/scroll/scroll-textarea.tsx new file mode 100644 index 000000000..f33e80299 --- /dev/null +++ b/frontend/src/components/valuecell/scroll/scroll-textarea.tsx @@ -0,0 +1,106 @@ +import { useCallback, useImperativeHandle, useRef } from "react"; +import { cn } from "@/lib/utils"; +import ScrollContainer from "./scroll-container"; + +export interface ScrollTextareaProps + extends Omit< + React.TextareaHTMLAttributes, + "style" | "onChange" + > { + /** + * Maximum height in pixels for the textarea container + * @default 120 + */ + maxHeight?: number; + /** + * Minimum height in pixels for the textarea + * @default 24 + */ + minHeight?: number; + /** + * Additional className for the ScrollContainer wrapper + */ + containerClassName?: string; + /** + * Whether to auto-resize the textarea based on content + * @default true + */ + autoResize?: boolean; + /** + * Ref object to access textarea methods + */ + ref?: React.Ref; +} + +export interface ScrollTextareaRef { + /** + * Focus the textarea + */ + focus: () => void; + /** + * Get the current textarea value + */ + getValue: () => string; +} + +function ScrollTextarea({ + className, + containerClassName, + maxHeight = 120, + minHeight = 24, + autoResize = true, + onInput, + ref, + ...props +}: ScrollTextareaProps) { + const textareaRef = useRef(null); + + // Auto-resize textarea function + const adjustTextareaHeight = useCallback(() => { + const textarea = textareaRef.current; + if (!textarea || !autoResize) return; + + // Reset height to auto to get the correct scrollHeight + textarea.style.height = "auto"; + textarea.style.height = `${Math.max(textarea.scrollHeight, minHeight)}px`; + }, [autoResize, minHeight]); + + // Handle input events (for compatibility) + const handleInput = useCallback( + (e: React.FormEvent) => { + onInput?.(e); + // Note: onChange will handle the height adjustment + adjustTextareaHeight(); + }, + [onInput, adjustTextareaHeight], + ); + + // Expose methods through ref + useImperativeHandle(ref, () => ({ + focus: () => { + textareaRef.current?.focus(); + }, + getValue: () => { + return textareaRef.current?.value || ""; + }, + })); + + return ( + +