diff --git a/frontend/biome.json b/frontend/biome.json index 86c7f071b..af0c2fafd 100644 --- a/frontend/biome.json +++ b/frontend/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.1/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.3/schema.json", "files": { "includes": [ "src/**/*.{ts,tsx,js,jsx}", diff --git a/frontend/bun.lock b/frontend/bun.lock index 89f672642..2943f36ea 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -4,29 +4,34 @@ "": { "name": "frontend", "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@react-router/node": "^7.9.4", + "@tanstack/react-form": "^1.23.8", "@tanstack/react-query": "^5.90.5", "@tauri-apps/api": "^2.9.0", "@tauri-apps/plugin-opener": "^2.5.1", "best-effort-json-parser": "^1.2.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "dayjs": "^1.11.18", "echarts": "^6.0.0", "isbot": "5.1.31", - "lucide-react": "^0.548.0", + "lucide-react": "^0.552.0", "mutative": "^1.3.0", "next-themes": "^0.4.6", "overlayscrollbars": "^2.12.0", @@ -38,10 +43,11 @@ "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", + "zod": "^4.1.12", "zustand": "^5.0.8", }, "devDependencies": { - "@biomejs/biome": "^2.3.1", + "@biomejs/biome": "^2.3.3", "@react-router/dev": "^7.9.4", "@react-router/serve": "^7.9.4", "@tailwindcss/typography": "^0.5.19", @@ -121,23 +127,23 @@ "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], - "@biomejs/biome": ["@biomejs/biome@2.3.1", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.1", "@biomejs/cli-darwin-x64": "2.3.1", "@biomejs/cli-linux-arm64": "2.3.1", "@biomejs/cli-linux-arm64-musl": "2.3.1", "@biomejs/cli-linux-x64": "2.3.1", "@biomejs/cli-linux-x64-musl": "2.3.1", "@biomejs/cli-win32-arm64": "2.3.1", "@biomejs/cli-win32-x64": "2.3.1" }, "bin": { "biome": "bin/biome" } }, "sha512-A29evf1R72V5bo4o2EPxYMm5mtyGvzp2g+biZvRFx29nWebGyyeOSsDWGx3tuNNMFRepGwxmA9ZQ15mzfabK2w=="], + "@biomejs/biome": ["@biomejs/biome@2.3.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.3", "@biomejs/cli-darwin-x64": "2.3.3", "@biomejs/cli-linux-arm64": "2.3.3", "@biomejs/cli-linux-arm64-musl": "2.3.3", "@biomejs/cli-linux-x64": "2.3.3", "@biomejs/cli-linux-x64-musl": "2.3.3", "@biomejs/cli-win32-arm64": "2.3.3", "@biomejs/cli-win32-x64": "2.3.3" }, "bin": { "biome": "bin/biome" } }, "sha512-zn/P1pRBCpDdhi+VNSMnpczOz9DnqzOA2c48K8xgxjDODvi5O8gs3a2H233rck/5HXpkFj6TmyoqVvxirZUnvg=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ombSf3MnTUueiYGN1SeI9tBCsDUhpWzOwS63Dove42osNh0PfE1cUtHFx6eZ1+MYCCLwXzlFlYFdrJ+U7h6LcA=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5+JtW6RKmjqL9un0UtHV0ezOslAyYBzyl5ZhYiu7GHesX2x8NCDl6tXYrenv9m7e1RLbkO5E5Kh04kseMtz6lw=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pcOfwyoQkrkbGvXxRvZNe5qgD797IowpJPovPX5biPk2FwMEV+INZqfCaz4G5bVq9hYnjwhRMamg11U4QsRXrQ=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-UPmKRalkHicvIpeccuKqq+/gA2HYV8FUnAEDJnqYBlGlycKqe6xrovWqvWTE4TTNpIFf4UQyuaDzLkN6Kz6tbA=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-td5O8pFIgLs8H1sAZsD6v+5quODihyEw4nv2R8z7swUfIK1FKk+15e4eiYVLcAE4jUqngvh4j3JCNgg0Y4o4IQ=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-zeiKwALNB/hax7+LLhCYqhqzlWdTfgE9BGkX2Z8S4VmCYnGFrf2fON/ec6KCos7mra5MDm6fYICsEWN2+HKZhw=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+DZYv8l7FlUtTrWs1Tdt1KcNCAmRO87PyOnxKGunbWm5HKg1oZBSbIIPkjrCtDZaeqSG1DiGx7qF+CPsquQRcg=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-KhCDMV+V7Yu72v40ssGJTHuv/j0n7JQ6l0s/c+EMcX5zPYLMLr4XpmI+WXhp4Vfkz0T5Xnh5wbrTBI3f2UTpjQ=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-PYWgEO7up7XYwSAArOpzsVCiqxBCXy53gsReAb1kKYIyXaoAlhBaBMvxR/k2Rm9aTuZ662locXUmPk/Aj+Xu+Q=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-05CjPLbvVVU8J6eaO6iSEoA0FXKy2l6ddL+1h/VpiosCmIp3HxRKLOa1hhC1n+D13Z8g9b1DtnglGtM5U3sTag=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Y3Ob4nqgv38Mh+6EGHltuN+Cq8aj/gyMTJYzkFZV2AEj+9XzoXB9VNljz9pjfFNHUxvLEV4b55VWyxozQTBaUQ=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-IyqQ+jYzU5MVy9CK5NV0U+NnUMPUAhYMrB/x4QgL/Dl1MqzBVc61bHeyhLnKM6DSEk73/TQYrk/8/QmVHudLdQ=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RHIG/zgo+69idUqVvV3n8+j58dKYABRpMyDmfWu2TITC+jwGPiEaT0Q3RKD+kQHiS80mpBrST0iUGeEXT0bU9A=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtlLs3pdFqFAQYZjlEHKOwJEn3GEaz7rtR2oCrzaLT2Xt3Cfd55/VvodQ5V+X+KepLa956QJagckJrNL+DmumQ=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-izl30JJ5Dp10mi90Eko47zhxE6pYyWPcnX1NQxKpL/yMhXxf95oLTzfpu4q+MDBh/gemNqyJEwjBpe0MT5iWPA=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-klJKPPQvUk9Rlp0Dd56gQw/+Wt6uUprHdHWtbDC93f3Iv+knA2tLWpcYoOZJgPV+9s+RBmYv0DGy4mUlr20esg=="], "@bundled-es-modules/cookie": ["@bundled-es-modules/cookie@2.0.1", "", { "dependencies": { "cookie": "^0.7.2" } }, "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw=="], @@ -229,10 +235,14 @@ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], @@ -255,10 +265,12 @@ "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], - "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], @@ -275,9 +287,9 @@ "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], - "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], @@ -381,10 +393,22 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.16", "", { "dependencies": { "@tailwindcss/node": "4.1.16", "@tailwindcss/oxide": "4.1.16", "tailwindcss": "4.1.16" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg=="], + "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.4", "", {}, "sha512-eq+PpuutUyubXu+ycC1GIiVwBs86NF/8yYJJAKSpPcJLWl6R/761F1H4F/9ziX6zKezltFUH1ah3Cz8Ah+KJrw=="], + + "@tanstack/form-core": ["@tanstack/form-core@1.24.4", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.3", "@tanstack/pacer": "^0.15.3", "@tanstack/store": "^0.7.7" } }, "sha512-+eIR7DiDamit1zvTVgaHxuIRA02YFgJaXMUGxsLRJoBpUjGl/g/nhUocQoNkRyfXqOlh8OCMTanjwDprWSRq6w=="], + + "@tanstack/pacer": ["@tanstack/pacer@0.15.4", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.2", "@tanstack/store": "^0.7.5" } }, "sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.5", "", {}, "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w=="], + "@tanstack/react-form": ["@tanstack/react-form@1.23.8", "", { "dependencies": { "@tanstack/form-core": "1.24.4", "@tanstack/react-store": "^0.7.7", "decode-formdata": "^0.9.0", "devalue": "^5.3.2" }, "peerDependencies": { "@tanstack/react-start": "^1.130.10", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@tanstack/react-start"] }, "sha512-ivfkiOHAI3aIWkCY4FnPWVAL6SkQWGWNVjtwIZpaoJE4ulukZWZ1KB8TQKs8f4STl+egjTsMHrWJuf2fv3Xh1w=="], + "@tanstack/react-query": ["@tanstack/react-query@5.90.5", "", { "dependencies": { "@tanstack/query-core": "5.90.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q=="], + "@tanstack/react-store": ["@tanstack/react-store@0.7.7", "", { "dependencies": { "@tanstack/store": "0.7.7", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg=="], + + "@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="], + "@tauri-apps/api": ["@tauri-apps/api@2.9.0", "", {}, "sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw=="], "@tauri-apps/cli": ["@tauri-apps/cli@2.9.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.1", "@tauri-apps/cli-darwin-x64": "2.9.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.1", "@tauri-apps/cli-linux-arm64-gnu": "2.9.1", "@tauri-apps/cli-linux-arm64-musl": "2.9.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.1", "@tauri-apps/cli-linux-x64-gnu": "2.9.1", "@tauri-apps/cli-linux-x64-musl": "2.9.1", "@tauri-apps/cli-win32-arm64-msvc": "2.9.1", "@tauri-apps/cli-win32-ia32-msvc": "2.9.1", "@tauri-apps/cli-win32-x64-msvc": "2.9.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-kKi2/WWsNXKoMdatBl4xrT7e1Ce27JvsetBVfWuIb6D3ep/Y0WO5SIr70yarXOSWam8NyDur4ipzjZkg6m7VDg=="], @@ -531,6 +555,8 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], + "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -579,6 +605,8 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decode-formdata": ["decode-formdata@0.9.0", "", {}, "sha512-q5uwOjR3Um5YD+ZWPOF/1sGHVW9A5rCrRwITQChRXlmPkxDFBqCm4jNTIVdGHNH9OnR+V9MoZVgRhsFb+ARbUw=="], + "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="], "dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="], @@ -595,6 +623,8 @@ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "devalue": ["devalue@5.4.2", "", {}, "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], @@ -873,7 +903,7 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@0.548.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA=="], + "lucide-react": ["lucide-react@0.552.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw=="], "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], @@ -1369,7 +1399,7 @@ "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="], @@ -1403,12 +1433,34 @@ "@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + "@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@npmcli/git/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], "@npmcli/git/which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], "@npmcli/promise-spawn/which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], + "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], @@ -1443,6 +1495,8 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -1493,6 +1547,8 @@ "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/frontend/package.json b/frontend/package.json index d48af3cf2..68f6145ba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,29 +17,34 @@ "tauri": "tauri" }, "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@react-router/node": "^7.9.4", + "@tanstack/react-form": "^1.23.8", "@tanstack/react-query": "^5.90.5", "@tauri-apps/api": "^2.9.0", "@tauri-apps/plugin-opener": "^2.5.1", "best-effort-json-parser": "^1.2.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "dayjs": "^1.11.18", "echarts": "^6.0.0", "isbot": "5.1.31", - "lucide-react": "^0.548.0", + "lucide-react": "^0.552.0", "mutative": "^1.3.0", "next-themes": "^0.4.6", "overlayscrollbars": "^2.12.0", @@ -51,10 +56,11 @@ "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", + "zod": "^4.1.12", "zustand": "^5.0.8" }, "devDependencies": { - "@biomejs/biome": "^2.3.1", + "@biomejs/biome": "^2.3.3", "@react-router/dev": "^7.9.4", "@react-router/serve": "^7.9.4", "@tailwindcss/typography": "^0.5.19", diff --git a/frontend/src/api/strategy.ts b/frontend/src/api/strategy.ts new file mode 100644 index 000000000..379ad8258 --- /dev/null +++ b/frontend/src/api/strategy.ts @@ -0,0 +1,108 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { API_QUERY_KEYS } from "@/constants/api"; +import { type ApiResponse, apiClient } from "@/lib/api-client"; +import type { + CreateStrategyRequest, + LlmConfig, + Position, + Strategy, + Trade, +} from "@/types/strategy"; + +export const useGetStrategyList = () => { + return useQuery({ + queryKey: API_QUERY_KEYS.STRATEGY.strategyList, + queryFn: () => + apiClient.get< + ApiResponse<{ + strategies: Strategy[]; + }> + >("/strategies"), + select: (data) => data.data.strategies, + refetchInterval: 15 * 1000, + }); +}; + +export const useGetStrategyTrades = (strategyId?: string) => { + return useQuery({ + queryKey: API_QUERY_KEYS.STRATEGY.strategyTrades([strategyId ?? ""]), + queryFn: () => + apiClient.get>( + `/strategies/detail?id=${strategyId}`, + ), + select: (data) => data.data, + refetchInterval: 15 * 1000, + enabled: !!strategyId, + }); +}; + +export const useGetStrategyHoldings = (strategyId?: string) => { + return useQuery({ + queryKey: API_QUERY_KEYS.STRATEGY.strategyHoldings([strategyId ?? ""]), + queryFn: () => + apiClient.get>( + `/strategies/holding?id=${strategyId}`, + ), + select: (data) => data.data, + refetchInterval: 15 * 1000, + enabled: !!strategyId, + }); +}; + +export const useGetStrategyPriceCurve = (strategyId?: string) => { + return useQuery({ + queryKey: API_QUERY_KEYS.STRATEGY.strategyPriceCurve([strategyId ?? ""]), + queryFn: () => + apiClient.get>>>( + `/strategies/holding_price_curve?id=${strategyId}`, + ), + select: (data) => data.data, + refetchInterval: 15 * 1000, + enabled: !!strategyId, + }); +}; + +export const useCreateStrategy = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: CreateStrategyRequest) => + apiClient.post>( + "/strategies/create", + data, + ), + onSuccess: () => { + // Invalidate strategy list to refetch + queryClient.invalidateQueries({ + queryKey: API_QUERY_KEYS.STRATEGY.strategyList, + }); + }, + }); +}; + +export const useGetStrategyApiKey = () => { + return useQuery({ + queryKey: API_QUERY_KEYS.STRATEGY.strategyApiKey, + queryFn: () => + apiClient.get>("/models/llm/config"), + select: (data) => data.data, + staleTime: 0, + }); +}; + +export const useStopStrategy = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (strategyId: string) => + apiClient.post>( + `/strategies/stop?id=${strategyId}`, + ), + onSuccess: () => { + // Invalidate strategy list to refetch + queryClient.invalidateQueries({ + queryKey: API_QUERY_KEYS.STRATEGY.strategyList, + }); + }, + }); +}; diff --git a/frontend/src/app/agent/chat.tsx b/frontend/src/app/agent/chat.tsx index 7c7c3d2ff..29d884abd 100644 --- a/frontend/src/app/agent/chat.tsx +++ b/frontend/src/app/agent/chat.tsx @@ -1,174 +1,23 @@ -import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect } from "react"; -import { - Navigate, - useLocation, - useNavigate, - useParams, - useSearchParams, -} from "react-router"; -import { toast } from "sonner"; -import { useGetAgentInfo } from "@/api/agent"; -import { useGetConversationHistory, usePollTaskList } from "@/api/conversation"; -import { API_QUERY_KEYS } from "@/constants/api"; -import { useSSE } from "@/hooks/use-sse"; -import { getServerUrl } from "@/lib/api-client"; -import { - useAgentStoreActions, - useCurrentConversation, -} from "@/store/agent-store"; -import type { AgentStreamRequest, SSEData } from "@/types/agent"; +import { Navigate, useParams } from "react-router"; import type { Route } from "./+types/chat"; -import ChatConversationArea from "./components/chat-conversation/chat-conversation-area"; +import CommonAgentArea from "./components/agent-view/common-agent-area"; +import StrategyAgentArea from "./components/agent-view/strategy-agent-area"; export default function AgentChat() { const { agentName } = useParams(); - const conversationId = useSearchParams()[0].get("id") ?? ""; - const navigate = useNavigate(); - const inputValue = useLocation().state?.inputValue; - // Use optimized hooks with built-in shallow comparison - const { curConversation, curConversationId } = useCurrentConversation(); - const { - dispatchAgentStore, - setCurConversationId, - dispatchAgentStoreHistory, - } = useAgentStoreActions(); - - const queryClient = useQueryClient(); - const { data: agent, isLoading: isLoadingAgent } = useGetAgentInfo({ - agentName: agentName ?? "", - }); - const { data: conversationHistory } = - useGetConversationHistory(conversationId); - const { data: taskList } = usePollTaskList(conversationId); - - // Load conversation history (only once when conversation changes) - useEffect(() => { - if ( - !conversationId || - !conversationHistory || - conversationHistory.length === 0 - ) - return; - - dispatchAgentStoreHistory(conversationId, conversationHistory, true); - }, [conversationId, conversationHistory, dispatchAgentStoreHistory]); - - // Update task list (polls every 30s) - useEffect(() => { - if (!conversationId || !taskList || taskList.length === 0) return; - - dispatchAgentStoreHistory(conversationId, taskList); - }, [conversationId, taskList, dispatchAgentStoreHistory]); - - // Initialize SSE connection using the useSSE hook - const { connect, close, isStreaming } = useSSE({ - url: getServerUrl("/agents/stream"), - handlers: { - onData: (sseData: SSEData) => { - // Update agent store using the reducer - dispatchAgentStore(sseData); - - // Handle specific UI state updates - const { event, data } = sseData; - switch (event) { - case "conversation_started": - navigate(`/agent/${agentName}?id=${data.conversation_id}`, { - replace: true, - }); - queryClient.invalidateQueries({ - queryKey: API_QUERY_KEYS.CONVERSATION.conversationList, - }); - break; - - case "component_generator": - if (data.payload.component_type === "subagent_conversation") { - queryClient.invalidateQueries({ - queryKey: API_QUERY_KEYS.CONVERSATION.conversationList, - }); - } - break; - - case "system_failed": - // Handle system errors in UI layer - toast.error(data.payload.content, { - closeButton: true, - duration: 30 * 1000, - }); - break; - - case "done": - close(); - break; - - // All message-related events are handled by the store - default: - break; - } - }, - onOpen: () => { - console.log("✅ SSE connection opened"); - }, - onError: (error: Error) => { - console.error("❌ SSE connection error:", error); - }, - onClose: () => { - console.log("🔌 SSE connection closed"); - }, - }, - }); - - // Send message to agent - // biome-ignore lint/correctness/useExhaustiveDependencies: connect is no need to be in dependencies - const sendMessage = useCallback( - async (message: string) => { - try { - const request: AgentStreamRequest = { - query: message, - agent_name: agentName ?? "", - conversation_id: conversationId, - }; - - // Connect SSE client with request body to receive streaming response - await connect(JSON.stringify(request)); - } catch (error) { - console.error("Failed to send message:", error); - } - }, - [agentName, conversationId], - ); - - useEffect(() => { - if (curConversationId !== conversationId) { - setCurConversationId(conversationId); - } - - if (inputValue) { - sendMessage(inputValue); - // Clear the state after using it once to prevent re-triggering on page refresh - navigate(".", { replace: true, state: {} }); - } - }, [ - conversationId, - inputValue, - sendMessage, - setCurConversationId, - curConversationId, - navigate, - ]); - - if (isLoadingAgent) return null; - if (!agent) return ; + if (!agentName) return ; return (
- + {(() => { + switch (agentName) { + case "StrategyAgent": + return ; + default: + return ; + } + })()}
); } diff --git a/frontend/src/app/agent/components/agent-view/common-agent-area.tsx b/frontend/src/app/agent/components/agent-view/common-agent-area.tsx new file mode 100644 index 000000000..b69b00983 --- /dev/null +++ b/frontend/src/app/agent/components/agent-view/common-agent-area.tsx @@ -0,0 +1,285 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { type FC, memo, useCallback, useEffect, useState } from "react"; +import { + Navigate, + useLocation, + useNavigate, + useSearchParams, +} from "react-router"; +import { toast } from "sonner"; +import { useGetAgentInfo } from "@/api/agent"; +import { useGetConversationHistory, usePollTaskList } from "@/api/conversation"; +import ScrollContainer from "@/components/valuecell/scroll/scroll-container"; +import { API_QUERY_KEYS } from "@/constants/api"; +import useSSE from "@/hooks/use-sse"; +import { getServerUrl } from "@/lib/api-client"; +import { + MultiSectionProvider, + useMultiSection, +} from "@/provider/multi-section-provider"; +import { + useAgentStoreActions, + useCurrentConversation, +} from "@/store/agent-store"; +import type { + AgentStreamRequest, + AgentViewProps, + MultiSectionComponentType, + SectionComponentType, + SSEData, +} from "@/types/agent"; +import { + ChatConversationHeader, + ChatInputArea, + ChatMultiSectionComponent, + ChatSectionComponent, + ChatThreadArea, + ChatWelcomeScreen, +} from "../chat-conversation"; + +interface CommonAgentAreaProps { + agentName: string; +} + +const CommonAgentAreaContent: FC = ({ agentName }) => { + const { data: agent, isLoading: isLoadingAgent } = useGetAgentInfo({ + agentName: agentName ?? "", + }); + + const conversationId = useSearchParams()[0].get("id") ?? ""; + const navigate = useNavigate(); + const inputValueFromLocation = useLocation().state?.inputValue; + + // Use optimized hooks with built-in shallow comparison + const { curConversation, curConversationId } = useCurrentConversation(); + const { + dispatchAgentStore, + setCurConversationId, + dispatchAgentStoreHistory, + } = useAgentStoreActions(); + + const queryClient = useQueryClient(); + + const { data: conversationHistory } = + useGetConversationHistory(conversationId); + const { data: taskList } = usePollTaskList(conversationId); + + // Load conversation history (only once when conversation changes) + useEffect(() => { + if ( + !conversationId || + !conversationHistory || + conversationHistory.length === 0 + ) + return; + + dispatchAgentStoreHistory(conversationId, conversationHistory, true); + }, [conversationId, conversationHistory, dispatchAgentStoreHistory]); + + // Update task list (polls every 30s) + useEffect(() => { + if (!conversationId || !taskList || taskList.length === 0) return; + + dispatchAgentStoreHistory(conversationId, taskList); + }, [conversationId, taskList, dispatchAgentStoreHistory]); + + // Initialize SSE connection using the useSSE hook + const { connect, close, isStreaming } = useSSE({ + url: getServerUrl("/agents/stream"), + handlers: { + onData: (sseData: SSEData) => { + // Update agent store using the reducer + dispatchAgentStore(sseData); + + // Handle specific UI state updates + const { event, data } = sseData; + switch (event) { + case "conversation_started": + navigate(`/agent/${agentName}?id=${data.conversation_id}`, { + replace: true, + }); + queryClient.invalidateQueries({ + queryKey: API_QUERY_KEYS.CONVERSATION.conversationList, + }); + break; + + case "component_generator": + if (data.payload.component_type === "subagent_conversation") { + queryClient.invalidateQueries({ + queryKey: API_QUERY_KEYS.CONVERSATION.conversationList, + }); + } + break; + + case "system_failed": + // Handle system errors in UI layer + toast.error(data.payload.content, { + closeButton: true, + duration: 30 * 1000, + }); + break; + + case "done": + close(); + break; + + // All message-related events are handled by the store + default: + break; + } + }, + onOpen: () => { + console.log("✅ SSE connection opened"); + }, + onError: (error: Error) => { + console.error("❌ SSE connection error:", error); + }, + onClose: () => { + console.log("🔌 SSE connection closed"); + }, + }, + }); + + // Send message to agent + // biome-ignore lint/correctness/useExhaustiveDependencies: connect is no need to be in dependencies + const sendMessage = useCallback( + async (message: string) => { + try { + const request: AgentStreamRequest = { + query: message, + agent_name: agentName, + conversation_id: conversationId, + }; + + // Connect SSE client with request body to receive streaming response + await connect(JSON.stringify(request)); + } catch (error) { + console.error("Failed to send message:", error); + } + }, + [agentName, conversationId], + ); + + useEffect(() => { + if (curConversationId !== conversationId) { + setCurConversationId(conversationId); + } + + if (inputValueFromLocation) { + sendMessage(inputValueFromLocation); + // Clear the state after using it once to prevent re-triggering on page refresh + navigate(".", { replace: true, state: {} }); + } + }, [ + sendMessage, + setCurConversationId, + curConversationId, + navigate, + conversationId, + inputValueFromLocation, + ]); + + const [inputValue, setInputValue] = useState(""); + const { currentSection } = useMultiSection(); + + const handleSendMessage = useCallback(async () => { + if (!inputValue.trim()) return; + try { + await sendMessage(inputValue); + setInputValue(""); + } catch (error) { + // Keep input value on error so user doesn't lose their text + console.error("Failed to send message:", error); + } + }, [inputValue, sendMessage]); + + const handleInputChange = useCallback((value: string) => { + setInputValue(value); + }, []); + + if (isLoadingAgent) return null; + if (!agent) return ; + + // Check if conversation has any messages + const hasMessages = + curConversation?.threads && Object.keys(curConversation.threads).length > 0; + + if (!hasMessages) { + return ( + <> + + + + ); + } + + return ( +
+ {/* main section */} +
+ + + + + {/* Input area now only in main section */} + +
+ + {/* Chat section components: one section per special component_type */} + {Object.entries(curConversation.sections).map( + ([componentType, threadView]) => { + return ( + + ); + }, + )} + + {/* Multi-section detail view */} + {currentSection && ( +
+ + + +
+ )} +
+ ); +}; + +const CommonAgentArea: FC = (props) => { + return ( + + + + ); +}; + +export default memo(CommonAgentArea); diff --git a/frontend/src/app/agent/components/agent-view/strategy-agent-area.tsx b/frontend/src/app/agent/components/agent-view/strategy-agent-area.tsx new file mode 100644 index 000000000..713103d32 --- /dev/null +++ b/frontend/src/app/agent/components/agent-view/strategy-agent-area.tsx @@ -0,0 +1,125 @@ +import { Plus } from "lucide-react"; +import { type FC, useEffect, useState } from "react"; +import { + useGetStrategyHoldings, + useGetStrategyList, + useGetStrategyPriceCurve, + useGetStrategyTrades, + useStopStrategy, +} from "@/api/strategy"; +import { Button } from "@/components/ui/button"; +import type { AgentViewProps } from "@/types/agent"; +import type { Strategy } from "@/types/strategy"; +import { + CreateStrategyModal, + PortfolioPositionsGroup, + TradeHistoryGroup, + TradeStrategyGroup, +} from "../strategy-items"; + +const EmptyIllustration = () => ( + + + + + + +); + +const StrategyAgentArea: FC = () => { + const { data: strategies = [], isLoading } = useGetStrategyList(); + const [selectedStrategy, setSelectedStrategy] = useState( + null, + ); + + const { data: trades = [] } = useGetStrategyTrades( + selectedStrategy?.strategy_id, + ); + + const { data: priceCurve = [] } = useGetStrategyPriceCurve( + selectedStrategy?.strategy_id, + ); + + const { data: positions = [] } = useGetStrategyHoldings( + selectedStrategy?.strategy_id, + ); + + const { mutateAsync: stopStrategy } = useStopStrategy(); + + useEffect(() => { + if (strategies && strategies.length > 0) { + setSelectedStrategy(strategies[0]); + } + }, [strategies]); + + if (isLoading) return null; + + return ( +
+ {/* Left section: Strategy list */} +
+

Trading Strategies

+ + {strategies && strategies.length > 0 ? ( + + await stopStrategy(strategyId) + } + /> + ) : ( +
+ + +
+

No trading strategies

+

Create your first trading strategy

+
+ + + + +
+ )} +
+ + {/* Right section: Trade History and Portfolio/Positions */} +
+ {selectedStrategy ? ( + <> + + + + ) : ( +
+ +

+ No running strategies +

+
+ )} +
+
+ ); +}; + +export default StrategyAgentArea; diff --git a/frontend/src/app/agent/components/chat-conversation/chat-conversation-area.tsx b/frontend/src/app/agent/components/chat-conversation/chat-conversation-area.tsx deleted file mode 100644 index 50f9abd17..000000000 --- a/frontend/src/app/agent/components/chat-conversation/chat-conversation-area.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { type FC, memo, useCallback, useState } from "react"; -import ScrollContainer from "@/components/valuecell/scroll/scroll-container"; -import { - MultiSectionProvider, - useMultiSection, -} from "@/provider/multi-section-provider"; -import type { - AgentInfo, - ConversationView, - MultiSectionComponentType, - SectionComponentType, -} from "@/types/agent"; -import ChatConversationHeader from "./chat-conversation-header"; -import ChatInputArea from "./chat-input-area"; -import ChatMultiSectionComponent from "./chat-multi-section-component"; -import ChatSectionComponent from "./chat-section-component"; -import ChatThreadArea from "./chat-thread-area"; -import ChatWelcomeScreen from "./chat-welcome-screen"; - -interface ChatConversationAreaProps { - agent: AgentInfo; - currentConversation: ConversationView | null; - isStreaming: boolean; - sendMessage: (message: string) => Promise; -} - -const ChatConversationAreaContent: FC = ({ - agent, - currentConversation, - isStreaming, - sendMessage, -}) => { - const [inputValue, setInputValue] = useState(""); - const { currentSection } = useMultiSection(); - - const handleSendMessage = useCallback(async () => { - if (!inputValue.trim()) return; - try { - await sendMessage(inputValue); - setInputValue(""); - } catch (error) { - // Keep input value on error so user doesn't lose their text - console.error("Failed to send message:", error); - } - }, [inputValue, sendMessage]); - - const handleInputChange = useCallback((value: string) => { - setInputValue(value); - }, []); - - // Check if conversation has any messages - const hasMessages = - currentConversation?.threads && - Object.keys(currentConversation.threads).length > 0; - - if (!hasMessages) { - return ( - <> - - - - ); - } - - return ( -
- {/* main section */} -
- - - - - {/* Input area now only in main section */} - -
- - {/* Chat section components: one section per special component_type */} - {Object.entries(currentConversation.sections).map( - ([componentType, threadView]) => { - return ( - - ); - }, - )} - - {/* Multi-section detail view */} - {currentSection && ( -
- - - -
- )} -
- ); -}; - -const ChatConversationArea: FC = (props) => { - return ( - - - - ); -}; - -export default memo(ChatConversationArea); diff --git a/frontend/src/app/agent/components/chat-conversation/index.tsx b/frontend/src/app/agent/components/chat-conversation/index.tsx new file mode 100644 index 000000000..5897190e9 --- /dev/null +++ b/frontend/src/app/agent/components/chat-conversation/index.tsx @@ -0,0 +1,6 @@ +export { default as ChatConversationHeader } from "./chat-conversation-header"; +export { default as ChatInputArea } from "./chat-input-area"; +export { default as ChatMultiSectionComponent } from "./chat-multi-section-component"; +export { default as ChatSectionComponent } from "./chat-section-component"; +export { default as ChatThreadArea } from "./chat-thread-area"; +export { default as ChatWelcomeScreen } from "./chat-welcome-screen"; diff --git a/frontend/src/app/agent/components/strategy-items/create-strategy-modal.tsx b/frontend/src/app/agent/components/strategy-items/create-strategy-modal.tsx new file mode 100644 index 000000000..3d521d986 --- /dev/null +++ b/frontend/src/app/agent/components/strategy-items/create-strategy-modal.tsx @@ -0,0 +1,762 @@ +import { useForm } from "@tanstack/react-form"; +import { MultiSelect } from "@valuecell/multi-select"; +import { Check, X } from "lucide-react"; +import type { FC } from "react"; +import { memo, useState } from "react"; +import { z } from "zod"; +import { useCreateStrategy, useGetStrategyApiKey } from "@/api/strategy"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Field, + FieldError, + FieldGroup, + FieldLabel, +} from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import ScrollContainer from "@/components/valuecell/scroll/scroll-container"; +import { + MODEL_PROVIDER_MAP, + MODEL_PROVIDERS, + TRADING_SYMBOLS, +} from "@/constants/agent"; + +interface CreateStrategyModalProps { + children?: React.ReactNode; +} + +type StepNumber = 1 | 2 | 3; + +// Step 1 Schema: AI Models +const step1Schema = z.object({ + provider: z.string().min(1, "Model platform is required"), + model_id: z.string().min(1, "Model selection is required"), + api_key: z.string().min(1, "API key is required"), +}); + +// Step 2 Schema: Exchanges (conditional validation with superRefine) +const step2Schema = z + .object({ + trading_mode: z.enum(["live", "virtual"]), + exchange_id: z.string(), + api_key: z.string(), + secret_key: z.string(), + }) + .superRefine((data, ctx) => { + // Only validate exchange credentials when live trading is selected + if (data.trading_mode === "live") { + const fields = [ + { + name: "exchange_id", + label: "Exchange", + value: data.exchange_id, + }, + { + name: "api_key", + label: "API key", + value: data.api_key, + }, + { + name: "secret_key", + label: "Secret key", + value: data.secret_key, + }, + ]; + + for (const field of fields) { + if (!field.value?.trim()) { + ctx.addIssue({ + code: "custom", + message: `${field.label} is required for live trading`, + path: [field.name], + }); + } + } + } + // Virtual trading mode: no validation needed for exchange fields + }); + +// Step 3 Schema: Trading Strategy +const step3Schema = z.object({ + strategy_name: z.string().min(1, "Strategy name is required"), + initial_capital: z.number().min(0, "Initial capital must be positive"), + max_leverage: z.number().min(1, "Leverage must be at least 1"), + symbols: z.array(z.string()).min(1, "At least one symbol is required"), + template_id: z.string().min(1, "Template selection is required"), + custom_prompt: z.string(), +}); + +const STEPS = [ + { number: 1 as const, title: "AI Models" }, + { number: 2 as const, title: "Exchanges" }, + { number: 3 as const, title: "Trading strategy" }, +]; + +const StepIndicator: FC<{ currentStep: StepNumber }> = ({ currentStep }) => { + const getStepState = (stepNumber: StepNumber) => ({ + isCompleted: stepNumber < currentStep, + isCurrent: stepNumber === currentStep, + isActive: stepNumber <= currentStep, + }); + + const renderStepNumber = ( + step: StepNumber, + isCurrent: boolean, + isCompleted: boolean, + ) => { + if (isCompleted) { + return ( +
+ +
+ ); + } + + return ( +
+
+ + {step} + +
+ ); + }; + + return ( +
+ {STEPS.map((step, index) => { + const { isCompleted, isCurrent, isActive } = getStepState(step.number); + const isLastStep = index === STEPS.length - 1; + + return ( +
+
+ {/* Step number/icon */} +
+ {renderStepNumber(step.number, isCurrent, isCompleted)} +
+ + {/* Step title and connector line */} +
+ + {step.title} + + + {!isLastStep && ( +
+ )} +
+
+
+ ); + })} +
+ ); +}; + +const CreateStrategyModal: FC = ({ children }) => { + const [open, setOpen] = useState(false); + const [currentStep, setCurrentStep] = useState(1); + const { mutateAsync: createStrategy } = useCreateStrategy(); + const { data: llmConfigs } = useGetStrategyApiKey(); + + // Step 1 Form: AI Models + const form1 = useForm({ + defaultValues: { + provider: "openrouter", + model_id: MODEL_PROVIDER_MAP.openrouter[0], + api_key: + llmConfigs?.find((config) => config.provider === "openrouter") + ?.api_key || "", + }, + validators: { + onSubmit: step1Schema, + }, + onSubmit: () => { + setCurrentStep(2); + }, + }); + + // Step 2 Form: Exchanges + const form2 = useForm({ + defaultValues: { + trading_mode: "live" as "live" | "virtual", + exchange_id: "okx", + api_key: "", + secret_key: "", + }, + validators: { + onSubmit: step2Schema, + }, + onSubmit: () => { + setCurrentStep(3); + }, + }); + + // Step 3 Form: Trading Strategy + const form3 = useForm({ + defaultValues: { + strategy_name: "", + initial_capital: 1000, + max_leverage: 8, + symbols: TRADING_SYMBOLS, + template_id: "default", + custom_prompt: "", + }, + validators: { + onSubmit: step3Schema, + }, + onSubmit: async ({ value }) => { + const payload = { + llm_model_config: form1.state.values, + exchange_config: form2.state.values, + trading_config: value, + }; + + await createStrategy(payload); + setOpen(false); + resetAll(); + }, + }); + + const resetAll = () => { + setCurrentStep(1); + form1.reset(); + form2.reset(); + form3.reset(); + }; + + const handleCancel = () => { + setOpen(false); + resetAll(); + }; + + const handleBack = () => { + if (currentStep > 1) { + setCurrentStep((prev) => (prev - 1) as StepNumber); + } + }; + + return ( + + + {children || ( + + )} + + + + +
+

Add trading strategy

+ +
+ + +
+ + {/* Form content with scroll */} + +
+ {/* Step 1: AI Models */} + {currentStep === 1 && ( +
{ + e.preventDefault(); + form1.handleSubmit(); + }} + > + + + {(field) => { + const isInvalid = + field.state.meta.isTouched && + field.state.meta.errors.length > 0; + return ( + + + Model Platform + + + {isInvalid && ( + + )} + + ); + }} + + + + {(field) => { + const isInvalid = + field.state.meta.isTouched && + field.state.meta.errors.length > 0; + const currentProvider = form1.state.values + .provider as keyof typeof MODEL_PROVIDER_MAP; + const availableModels = + MODEL_PROVIDER_MAP[currentProvider] || []; + + return ( + + + Select Model + + + {isInvalid && ( + + )} + + ); + }} + + + + {(field) => { + const isInvalid = + field.state.meta.isTouched && + field.state.meta.errors.length > 0; + return ( + + + API key + + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder="Enter API Key" + /> + {isInvalid && ( + + )} + + ); + }} + + +
+ )} + + {/* Step 2: Exchanges */} + {currentStep === 2 && ( +
{ + e.preventDefault(); + form2.handleSubmit(); + }} + > + + + {(field) => { + const isLiveTrading = field.state.value === "live"; + + return ( + <> + + + Transaction Type + + { + const newMode = value as "live" | "virtual"; + form2.reset(); + + field.handleChange(newMode); + }} + className="flex items-center gap-6" + > +
+ + +
+
+ + +
+
+
+ + {isLiveTrading && ( + <> + + {(field) => ( + + + Select Exchange + + + {field.state.meta.errors.length > 0 && ( + + )} + + )} + + + + {(field) => ( + + + API key + + + field.handleChange(e.target.value) + } + onBlur={field.handleBlur} + placeholder="Enter API Key" + /> + {field.state.meta.errors.length > 0 && ( + + )} + + )} + + + + {(field) => ( + + + Secret Key + + + field.handleChange(e.target.value) + } + onBlur={field.handleBlur} + placeholder="Enter Secret Key" + /> + {field.state.meta.errors.length > 0 && ( + + )} + + )} + + + )} + + ); + }} +
+
+
+ )} + + {/* Step 3: Trading Strategy */} + {currentStep === 3 && ( +
{ + e.preventDefault(); + form3.handleSubmit(); + }} + > + + + {(field) => ( + + + Strategy Name + + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder="Enter strategy name" + /> + {field.state.meta.errors.length > 0 && ( + + )} + + )} + + + {/* Transaction Configuration */} +
+
+
+

+ Transaction configuration +

+
+ +
+
+ + {(field) => ( + + + Initial Capital + + + field.handleChange(Number(e.target.value)) + } + onBlur={field.handleBlur} + /> + {field.state.meta.errors.length > 0 && ( + + )} + + )} + + + + {(field) => ( + + + Max Leverage + + + field.handleChange(Number(e.target.value)) + } + onBlur={field.handleBlur} + /> + {field.state.meta.errors.length > 0 && ( + + )} + + )} + +
+ + + {(field) => ( + + + Trading Symbols + + + field.handleChange(value) + } + placeholder="Select trading symbols..." + searchPlaceholder="Search symbols..." + emptyText="No symbols found." + maxDisplayed={5} + className="h-[58px] rounded-[10px] border-gray-200" + /> + {field.state.meta.errors.length > 0 && ( + + )} + + )} + +
+
+ + {/* Trading Strategy Prompt */} + {/*
+
+
+

+ Trading strategy prompt +

+
+ +
+ + {(field) => ( + + + System Prompt Template + + + {field.state.meta.errors.length > 0 && ( + + )} + + )} + + + + {(field) => ( + + + Custom Prompt + +