Sfoglia il codice sorgente

add web (#165)

* add admin pane
limbo 8 mesi fa
parent
commit
cea81b4e3c
100 ha cambiato i file con 13986 aggiunte e 6 eliminazioni
  1. 15 0
      Dockerfile
  2. 1 1
      README.md
  3. 2 1
      core/router/main.go
  4. 32 0
      core/router/static.go
  5. 5 4
      docker-compose.yaml
  6. 59 0
      go.work.sum
  7. 2 0
      web/.env.template
  8. 28 0
      web/.gitignore
  9. 54 0
      web/README.md
  10. 21 0
      web/components.json
  11. 125 0
      web/context.md
  12. 28 0
      web/eslint.config.js
  13. 309 0
      web/index.html
  14. 100 0
      web/openapi.txt
  15. 68 0
      web/package.json
  16. 4013 0
      web/pnpm-lock.yaml
  17. 84 0
      web/prompt.txt
  18. 173 0
      web/public/locales/en/translation.json
  19. 173 0
      web/public/locales/zh/translation.json
  20. 9 0
      web/public/logo.svg
  21. 12 0
      web/src/App.tsx
  22. 21 0
      web/src/api/auth.ts
  23. 46 0
      web/src/api/channel.ts
  24. 267 0
      web/src/api/index.ts
  25. 26 0
      web/src/api/model.ts
  26. 4 0
      web/src/api/services.ts
  27. 33 0
      web/src/api/token.ts
  28. 0 0
      web/src/assets/react.svg
  29. 64 0
      web/src/components/common/LanguageSelector.tsx
  30. 98 0
      web/src/components/common/LoadingFallBack.tsx
  31. 31 0
      web/src/components/common/ThemeToggle.tsx
  32. 63 0
      web/src/components/common/error/errorConfig.tsx
  33. 168 0
      web/src/components/common/error/errorDisplay.tsx
  34. 58 0
      web/src/components/common/error/errorTypes.ts
  35. 62 0
      web/src/components/layout/AnimatedRoute.tsx
  36. 32 0
      web/src/components/layout/RootLayOut.tsx
  37. 274 0
      web/src/components/layout/SideBar.tsx
  38. 223 0
      web/src/components/select/ConstructMappingComponent.tsx
  39. 200 0
      web/src/components/select/MultiSelectCombobox.tsx
  40. 102 0
      web/src/components/select/Select.tsx
  41. 128 0
      web/src/components/select/SingleSelectCombobox.tsx
  42. 66 0
      web/src/components/table/column-header.tsx
  43. 59 0
      web/src/components/table/column-toggle.tsx
  44. 194 0
      web/src/components/table/data-table.tsx
  45. 272 0
      web/src/components/table/motion-data-table.tsx
  46. 103 0
      web/src/components/table/pagination.tsx
  47. 155 0
      web/src/components/ui/alert-dialog.tsx
  48. 66 0
      web/src/components/ui/alert.tsx
  49. 150 0
      web/src/components/ui/animation/button-animation.ts
  50. 255 0
      web/src/components/ui/animation/collapse-animation.ts
  51. 55 0
      web/src/components/ui/animation/components/animated-button.tsx
  52. 57 0
      web/src/components/ui/animation/components/animated-container.tsx
  53. 69 0
      web/src/components/ui/animation/components/animated-icon.tsx
  54. 46 0
      web/src/components/ui/animation/components/collapse.tsx
  55. 48 0
      web/src/components/ui/animation/components/display.tsx
  56. 139 0
      web/src/components/ui/animation/components/particles-background.tsx
  57. 39 0
      web/src/components/ui/animation/components/tab-animation.tsx
  58. 82 0
      web/src/components/ui/animation/components/table-scroll.tsx
  59. 50 0
      web/src/components/ui/animation/container-animation.ts
  60. 119 0
      web/src/components/ui/animation/dialog-animation.ts
  61. 122 0
      web/src/components/ui/animation/display-animation.ts
  62. 44 0
      web/src/components/ui/animation/grid-animation.ts
  63. 170 0
      web/src/components/ui/animation/icon-animation.ts
  64. 145 0
      web/src/components/ui/animation/route-animation.ts
  65. 67 0
      web/src/components/ui/animation/tab-animation.ts
  66. 51 0
      web/src/components/ui/avatar.tsx
  67. 46 0
      web/src/components/ui/badge.tsx
  68. 59 0
      web/src/components/ui/button.tsx
  69. 92 0
      web/src/components/ui/card.tsx
  70. 31 0
      web/src/components/ui/collapsible.tsx
  71. 133 0
      web/src/components/ui/dialog.tsx
  72. 130 0
      web/src/components/ui/drawer.tsx
  73. 255 0
      web/src/components/ui/dropdown-menu.tsx
  74. 165 0
      web/src/components/ui/form.tsx
  75. 21 0
      web/src/components/ui/input.tsx
  76. 24 0
      web/src/components/ui/label.tsx
  77. 183 0
      web/src/components/ui/select.tsx
  78. 26 0
      web/src/components/ui/separator.tsx
  79. 137 0
      web/src/components/ui/sheet.tsx
  80. 13 0
      web/src/components/ui/skeleton.tsx
  81. 29 0
      web/src/components/ui/sonner.tsx
  82. 29 0
      web/src/components/ui/switch.tsx
  83. 114 0
      web/src/components/ui/table.tsx
  84. 59 0
      web/src/components/ui/tooltip.tsx
  85. 152 0
      web/src/constant/index.ts
  86. 19 0
      web/src/feature/auth/components/ProtectedRoute.tsx
  87. 41 0
      web/src/feature/auth/hooks.ts
  88. 93 0
      web/src/feature/channel/components/ChannelDialog.tsx
  89. 439 0
      web/src/feature/channel/components/ChannelForm.tsx
  90. 317 0
      web/src/feature/channel/components/ChannelTable.tsx
  91. 93 0
      web/src/feature/channel/components/DeleteChannelDialog.tsx
  92. 163 0
      web/src/feature/channel/hooks.ts
  93. 93 0
      web/src/feature/model/components/DeleteModelDialog.tsx
  94. 84 0
      web/src/feature/model/components/ModelDialog.tsx
  95. 158 0
      web/src/feature/model/components/ModelForm.tsx
  96. 235 0
      web/src/feature/model/components/ModelTable.tsx
  97. 480 0
      web/src/feature/model/components/api-doc/ApiDoc.tsx
  98. 59 0
      web/src/feature/model/components/api-doc/CodeHight.tsx
  99. 85 0
      web/src/feature/model/hooks.ts
  100. 93 0
      web/src/feature/token/components/DeleteTokenDialog.tsx

+ 15 - 0
Dockerfile

@@ -14,6 +14,19 @@ RUN sh scripts/swag.sh
 
 RUN go build -trimpath -tags "jsoniter" -ldflags "-s -w" -o aiproxy
 
+# Frontend build stage
+FROM node:23-alpine AS frontend-builder
+
+WORKDIR /aiproxy/web
+
+COPY ./web/ ./
+
+# Install pnpm globally
+RUN npm install -g pnpm
+
+# Install dependencies and build with pnpm
+RUN pnpm install && pnpm run build
+
 FROM alpine:latest
 
 RUN mkdir -p /aiproxy
@@ -26,6 +39,8 @@ RUN apk add --no-cache ca-certificates tzdata ffmpeg curl && \
     rm -rf /var/cache/apk/*
 
 COPY --from=builder /aiproxy/core/aiproxy /usr/local/bin/aiproxy
+# Copy frontend dist files
+COPY --from=frontend-builder /aiproxy/web/dist/ ./web/dist/
 
 ENV PUID=0 PGID=0 UMASK=022
 

+ 1 - 1
README.md

@@ -55,7 +55,7 @@ docker run -d --name aiproxy -p 3000:3000 -v $(pwd)/aiproxy:/aiproxy ghcr.io/lab
 
 ### Use Docker Compose
 
-Copy [docker-compose.yaml](./docker-compose.yaml) to directory.
+Copy [docker-compose.yaml](./docker-compose.yaml) to directory. default access key is `aiproxy`. default listen port is `3000`.
 
 ```bash
 docker-compose up -d

+ 2 - 1
core/router/main.go

@@ -7,11 +7,12 @@ import (
 )
 
 func SetRouter(router *gin.Engine) {
-	router.GET("/", func(c *gin.Context) {
+	router.GET("/ok", func(c *gin.Context) {
 		c.String(http.StatusOK, "AI Proxy is running!")
 	})
 	SetAPIRouter(router)
 	SetRelayRouter(router)
 	SetMCPRouter(router)
+	SetStaticFileRouter(router)
 	SetSwaggerRouter(router)
 }

+ 32 - 0
core/router/static.go

@@ -0,0 +1,32 @@
+package router
+
+import (
+	"net/http"
+	"strings"
+
+	"github.com/gin-gonic/gin"
+)
+
+// SetStaticFileRouter configures routes to serve frontend static files
+func SetStaticFileRouter(router *gin.Engine) {
+	// Serve static assets
+	router.Static("/assets", "./web/dist/assets")
+
+	// Serve localization files
+	router.Static("/locales", "./web/dist/locales")
+
+	// Serve other static files
+	router.StaticFile("/logo.svg", "./web/dist/logo.svg")
+
+	// Handle non-API routes, returning the frontend entry point
+	router.NoRoute(func(c *gin.Context) {
+		// Return 404 for API requests
+		if strings.HasPrefix(c.Request.URL.Path, "/api") {
+			c.JSON(http.StatusNotFound, gin.H{"error": "API route not found"})
+			return
+		}
+
+		// Return the frontend entry file for all other routes
+		c.File("./web/dist/index.html")
+	})
+}

+ 5 - 4
core/docker-compose.yaml → docker-compose.yaml

@@ -1,7 +1,9 @@
-version: '3.3'
+version: "3.3"
 services:
   aiproxy:
-    image: 'ghcr.io/labring/aiproxy:latest'
+    build:
+      context: .
+      dockerfile: Dockerfile
     container_name: aiproxy
     restart: unless-stopped
     depends_on:
@@ -10,14 +12,13 @@ services:
       redis:
         condition: service_healthy
     ports:
-      - '3000:3000/tcp'
+      - "3000:3000/tcp"
     environment:
       - ADMIN_KEY=aiproxy
       - LOG_DETAIL_STORAGE_HOURS=1
       - TZ=Asia/Shanghai
       - SQL_DSN=postgres://postgres:aiproxy@pgsql:5432/aiproxy
       - REDIS_CONN_STRING=redis://redis
-      - DISABLE_MODEL_CONFIG=true
     healthcheck:
       test: ["CMD", "curl", "-f", "http://localhost:3000/api/status"]
       interval: 5s

+ 59 - 0
go.work.sum

@@ -1,70 +1,129 @@
 cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI=
 cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
 cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
 cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
+cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw=
 cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
+cloud.google.com/go/translate v1.10.3 h1:g+B29z4gtRGsiKDoTF+bNeH25bLRokAaElygX2FcZkE=
 cloud.google.com/go/translate v1.10.3/go.mod h1:GW0vC1qvPtd3pgtypCv4k4U8B7EdgK9/QEF2aJEUovs=
 github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw=
 github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A=
+github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
 github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
 github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
 github.com/aws/aws-sdk-go-v2/service/sts v1.33.18/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
 github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
 github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
+github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
 github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
 github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
+github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
 github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
+github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
 github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
+github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
 github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
 github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
+github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
 github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8=
 github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY=
+github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk=
 github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
+github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c h1:bY6ktFuJkt+ZXkX0RChQch2FtHpWQLVS8Qo1YasiIVk=
 github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
 github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISeOcal5irEhJd1M+IrApc0PdcN7e7Aj4yuEnOrfQ=
 github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
+github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk=
 github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
+github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 h1:pXY9qYc/MP5zdvqWEUH6SjNiu7VhSjuVFTFiTcphaLU=
 github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
+github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
 github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
 github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
+github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
+github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
 github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao=
 go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo=
+golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
 golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
 golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
+golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
 google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
 google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE=
 google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
 google.golang.org/genproto/googleapis/bytestream v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:WkJpQl6Ujj3ElX4qZaNm5t6cT95ffI4K+HKQ0+1NyMw=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20250414145226-207652e42e2e h1:OK8bKvRgTGs7U871RdjtCiRcQJLice8/rZkeoaZgnlc=
 google.golang.org/genproto/googleapis/bytestream v0.0.0-20250414145226-207652e42e2e/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
 lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
 modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
+modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
 modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
+nullprogram.com/x/optparse v1.0.0 h1:xGFgVi5ZaWOnYdac2foDT3vg0ZZC9ErXFV57mr4OHrI=
+rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
 sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

+ 2 - 0
web/.env.template

@@ -0,0 +1,2 @@
+VITE_API_BASE_URL=http://localhost:3000/api
+VITE_API_TIMEOUT=15000

+ 28 - 0
web/.gitignore

@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.env.local
+.env.development
+.env.production

+ 54 - 0
web/README.md

@@ -0,0 +1,54 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default tseslint.config({
+  extends: [
+    // Remove ...tseslint.configs.recommended and replace with this
+    ...tseslint.configs.recommendedTypeChecked,
+    // Alternatively, use this for stricter rules
+    ...tseslint.configs.strictTypeChecked,
+    // Optionally, add this for stylistic rules
+    ...tseslint.configs.stylisticTypeChecked,
+  ],
+  languageOptions: {
+    // other options...
+    parserOptions: {
+      project: ['./tsconfig.node.json', './tsconfig.app.json'],
+      tsconfigRootDir: import.meta.dirname,
+    },
+  },
+})
+```
+
+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default tseslint.config({
+  plugins: {
+    // Add the react-x and react-dom plugins
+    'react-x': reactX,
+    'react-dom': reactDom,
+  },
+  rules: {
+    // other rules...
+    // Enable its recommended typescript rules
+    ...reactX.configs['recommended-typescript'].rules,
+    ...reactDom.configs.recommended.rules,
+  },
+})
+```

+ 21 - 0
web/components.json

@@ -0,0 +1,21 @@
+{
+  "$schema": "https://ui.shadcn.com/schema.json",
+  "style": "new-york",
+  "rsc": false,
+  "tsx": true,
+  "tailwind": {
+    "config": "",
+    "css": "src/index.css",
+    "baseColor": "neutral",
+    "cssVariables": true,
+    "prefix": ""
+  },
+  "aliases": {
+    "components": "@/components",
+    "utils": "@/lib/utils",
+    "ui": "@/components/ui",
+    "lib": "@/lib",
+    "hooks": "@/hooks"
+  },
+  "iconLibrary": "lucide"
+}

+ 125 - 0
web/context.md

@@ -0,0 +1,125 @@
+重点信息如下
+项目采用的框架是 react + vite + tailwindcss + typescript
+路由使用的是 "react-router": "^7.5.1"
+包管理器使用的是 pnpm
+ui 使用的是 shadcn
+主要分为左右布局,左边是 sidebar 负责主导航,右边是内容区。
+项目 icon 使用 lucide-react
+
+---
+
+项目要求
+
+1. 结构完整,功能完整,代码规范,可维护性强
+2. 目录组装,文件命名符合规范,关键代码需要有友好注释
+3. 使用 vite 进行开发,使用 vite 进行打包
+4. 使用 tailwindcss 进行样式开发,tailwindcss 使用的是 v4 版本
+5. 使用 shadcn 进行 ui 开发
+6. 使用 react-router "react-router": "^7.5.1", 进行路由开发
+7. 使用 tanstack query @tanstack/[email protected] 进行数据请求
+8. 使用 tanstack table 进行表格开发
+9. 使用 zustand 进行状态管理
+10. 使用 react-hook-form 进行表单开发
+11. 使用 zod 进行数据验证
+12. 使用 react-i18next 进行国际化
+13. 支持中英文切换,默认使用中文
+14. 使用 i18next-http-backend 加载翻译文件
+15. 使用 i18next-browser-languagedetector 检测用户语言
+16. 项目 icon 使用 lucide-react
+
+---
+
+项目主要目录结构介绍
+静态资源
+public
+public/locales 是国际化文件夹,负责项目的国际化配置
+public/locales/en/translation.json 是英文翻译文件
+public/locales/zh/translation.json 是中文翻译文件
+src/assets 是项目静态资源文件夹,负责项目的静态资源管理
+
+入口与配置
+index.html 单应用挂载点,入口文件
+src/main.tsx 是项目入口文件,负责项目的初始化
+src/App.tsx 是项目主组件,负责项目的根组件
+src/i18n.ts 是国际化 i18n 配置文件,负责项目的国际化配置
+eslint.config.js 是 eslint 的配置文件,负责项目的 eslint 配置
+package.json 包文件
+vite.config.ts 是 vite 的配置文件,负责项目的 vite 配置
+components.json 是 shadcn 的配置文件,负责项目的 shadcn 配置
+
+样式
+src/index.css 是项目样式文件,负责项目的样式,包括 shadcn 的样式,tailwind css 的导入
+
+API 请求封装
+src/api/index.ts: 使用 axios 创建了基础 API 客户端,包含请求拦截器和响应拦截器
+src/api/auth.ts: 实现了认证相关的 API 服务,包括登录
+src/api/services.ts: 统一导出 API 服务
+
+项目内容
+路由配置
+src/routes 下是路由配置文件,负责路由的配置和面包屑的配置,
+src/routes/config.tsx 是路由配置文件,负责路由的配置和面包屑的配置
+src/routes/constants.ts 定义路由路径常量,例如 ROUTES
+
+布局
+src/components/layout 下是布局文件,负责项目的布局管理
+src/components/layout/RootLayOut.tsx 是根布局文件,负责项目的根布局管理
+src/components/layout/Sidebar.tsx 是侧边栏文件,负责项目的侧边栏管理
+
+路由端点 页面
+src/pages 下是页面文件,负责项目的页面管理
+src/pages/auth 下是认证页面文件,负责项目的认证页面管理
+src/pages/auth/login.tsx: 登录页面组件实现
+
+src/lib 公共库
+src/lib 下是项目公共库文件,负责项目的公共库管理
+
+src/hooks hook
+src/hooks 下是项目 hooks 文件,负责项目的 hooks 管理
+
+src/types 类型
+src/types 下是项目 types 文件,负责项目的 types 管理
+src/types/i18next.d.ts 是 i18next 的类型文件,它扩展了 i18next 模块的类型定义,目的是提供更好的类型检查和代码补全功能。
+
+src/components 项目组件
+src/components/common 下是项目公共组件文件,负责项目的公共组件管理
+src/components/layout 下是项目布局组件文件,负责项目的布局组件管理
+src/components/table 下是项目表格组件文件,负责项目的表格组件管理,将 tanstack table 进行封装
+src/components/ui shadcn ui 基础组件,由 shadcn 命令行生成
+
+src/store 状态管理
+src/store 下是项目状态管理文件,负责项目的状态管理
+src/store/auth.ts 是认证状态管理文件,负责项目的认证状态管理
+
+src/validation 表单验证
+src/validation 下是项目表单验证文件,负责项目的表单验证管理
+src/validation/auth.ts: 包含登录表单的 zod 验证 schema
+
+src/feature 功能模块 一些页面的功能,组件可以单独抽离出来
+src/feature/auth 下是认证功能模块文件,负责项目的认证功能模块管理
+src/feature/auth/hook 下是认证功能模块的 tanstack query 数据管理封装
+src/feature/auth/hooks.ts: 包含登录相关的 tanstack query hooks
+src/feature/auth/components 下是认证功能模块组件文件,负责项目的认证功能模块组件管理
+src/feature/auth/components/ProtectedRoute.tsx 是路由保护组件文件,负责项目的路由保护组件管理
+
+src/utils 工具函数
+src/utils 下是项目 utils 文件,负责项目的 utils 管理
+
+全局状态管理:
+
+- 认证状态: token、是否已认证等
+- 状态持久化: 使用 Zustand persist 中间件实现
+
+## 渠道功能模块
+
+src/types/channel.ts - 定义渠道相关的类型
+src/api/channel.ts - 封装渠道相关的 API 调用
+src/validation/channel.ts - 渠道表单验证逻辑
+src/feature/channel/hooks.ts - 渠道相关的数据请求和状态管理 hooks
+src/feature/channel/components/ - 渠道相关的组件
+
+- ChannelTable.tsx - 渠道列表表格组件,支持无限滚动
+- ChannelDialog.tsx - 渠道创建/编辑对话框组件
+- ChannelForm.tsx - 渠道表单组件
+- DeleteChannelDialog.tsx - 渠道删除确认对话框组件
+  src/pages/channel/page.tsx - 渠道页面组件

+ 28 - 0
web/eslint.config.js

@@ -0,0 +1,28 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+
+export default tseslint.config(
+  { ignores: ['dist'] },
+  {
+    extends: [js.configs.recommended, ...tseslint.configs.recommended],
+    files: ['**/*.{ts,tsx}'],
+    languageOptions: {
+      ecmaVersion: 2020,
+      globals: globals.browser,
+    },
+    plugins: {
+      'react-hooks': reactHooks,
+      'react-refresh': reactRefresh,
+    },
+    rules: {
+      ...reactHooks.configs.recommended.rules,
+      'react-refresh/only-export-components': [
+        'warn',
+        { allowConstantExport: true },
+      ],
+    },
+  },
+)

+ 309 - 0
web/index.html

@@ -0,0 +1,309 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="UTF-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
+        <link rel="icon" type="image/svg+xml" href="/logo.svg" />
+
+        <!-- 主题色定义 -->
+        <meta name="theme-color" content="#6a6de6" />
+
+        <!-- SEO元标签 -->
+        <meta
+            name="description"
+            content="AI Proxy"
+        />
+        <meta
+            name="keywords"
+            content="AI, proxy, security, protection"
+        />
+
+        <!-- 社交媒体分享信息 -->
+        <meta property="og:title" content="AI Proxy" />
+        <meta property="og:description" content="AI Proxy" />
+        <meta property="og:image" content="/og-image.png" />
+
+
+
+        <!-- 添加页面加载样式,避免初始白屏 -->
+        <style>
+            /* 基础样式重置 */
+            body,
+            html {
+                margin: 0;
+                padding: 0;
+                height: 100%;
+                width: 100%;
+                overflow: hidden;
+                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
+                    Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
+                    "Helvetica Neue", sans-serif;
+            }
+
+            #root {
+                height: 100%;
+                width: 100%;
+            }
+
+            /* 加载动画样式 */
+            .app-loading {
+                position: fixed;
+                top: 0;
+                left: 0;
+                width: 100%;
+                height: 100%;
+                display: flex;
+                flex-direction: column;
+                align-items: center;
+                justify-content: center;
+                z-index: 9999;
+            }
+
+            /* 使用蓝紫色渐变背景 */
+            .app-loading-background {
+                position: absolute;
+                inset: 0;
+                background: linear-gradient(
+                    135deg,
+                    rgba(106, 109, 230, 0.95) 0%,
+                    rgba(123, 127, 246, 0.9) 50%,
+                    rgba(138, 141, 247, 0.95) 100%
+                );
+                background-size: 200% 200%;
+            }
+
+            .app-loading-blur-elements {
+                position: absolute;
+                inset: 0;
+                overflow: hidden;
+            }
+
+            .app-loading-blur-1 {
+                position: absolute;
+                width: 80%;
+                height: 80%;
+                top: 10%;
+                left: 10%;
+                background-color: rgba(255, 255, 255, 0.1); /* white/10 */
+                border-radius: 9999px;
+                filter: blur(24px);
+                animation: preload-float 8s ease-in-out infinite;
+            }
+
+            .app-loading-blur-2 {
+                position: absolute;
+                width: 40%;
+                height: 40%;
+                top: 5%;
+                right: 15%;
+                background-color: rgba(255, 255, 255, 0.15); /* 调整为更接近原AI-Proxy文件 */
+                border-radius: 9999px;
+                filter: blur(24px);
+                animation: preload-float-reverse 9s ease-in-out infinite;
+            }
+
+            .app-loading-blur-3 {
+                position: absolute;
+                width: 50%;
+                height: 50%;
+                bottom: 5%;
+                left: 15%;
+                background-color: rgba(255, 255, 255, 0.1); /* 调整为更接近原AI-Proxy文件 */
+                border-radius: 9999px;
+                filter: blur(24px);
+                animation: preload-pulse-glow 4s ease-in-out infinite;
+            }
+
+            /* 进度条容器 */
+            .app-loading-content {
+                position: relative;
+                z-index: 10;
+                display: flex;
+                flex-direction: column;
+                align-items: center;
+                gap: 32px; /* 保持原间距 */
+            }
+
+            .app-loading-text {
+                color: white;
+                font-size: 24px;
+                font-weight: 500;
+                animation: preload-fade-in 0.5s ease-out;
+            }
+
+            .app-loading-progress-container {
+                width: 256px;
+                height: 8px;
+                background-color: rgba(255, 255, 255, 0.2);
+                border-radius: 9999px;
+                overflow: hidden;
+            }
+
+            .app-loading-progress-bar {
+                height: 100%;
+                width: 0%;
+                border-radius: 9999px;
+                background: linear-gradient(
+                    90deg,
+                    rgba(255, 255, 255, 0.9) 0%,
+                    rgba(255, 255, 255, 0.7) 100%
+                );
+                box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
+                transition: width 0.3s ease;
+            }
+
+            .app-loading-percentage {
+                color: rgba(255, 255, 255, 0.8);
+                font-size: 14px;
+                animation: preload-pulse 2s infinite;
+            }
+
+            /* 添加粒子效果 */
+            .app-loading-particle {
+                position: absolute;
+                border-radius: 9999px;
+                background-color: rgba(255, 255, 255, 0.1);
+                animation: preload-float 15s ease-in-out infinite;
+            }
+
+            /* 定义动画关键帧 */
+            @keyframes preload-float {
+                0%,
+                100% {
+                    transform: translateY(0) scale(1);
+                }
+                50% {
+                    transform: translateY(-20px) scale(1.05);
+                }
+            }
+
+            @keyframes preload-float-reverse {
+                0%,
+                100% {
+                    transform: translateY(0) scale(1);
+                }
+                50% {
+                    transform: translateY(20px) scale(1.05);
+                }
+            }
+
+            @keyframes preload-pulse-glow {
+                0%,
+                100% {
+                    opacity: 0.6;
+                    transform: scale(1);
+                }
+                50% {
+                    opacity: 0.8;
+                    transform: scale(1.1);
+                }
+            }
+
+            @keyframes preload-pulse {
+                0%,
+                100% {
+                    opacity: 0.5;
+                }
+                50% {
+                    opacity: 1;
+                }
+            }
+
+            @keyframes preload-fade-in {
+                from {
+                    opacity: 0;
+                    transform: translateY(-20px);
+                }
+                to {
+                    opacity: 1;
+                    transform: translateY(0);
+                }
+            }
+
+            /* 隐藏加载动画,当应用加载完成时使用 */
+            .app-loading-hidden {
+                opacity: 0;
+                visibility: hidden;
+                transition: opacity 0.5s, visibility 0.5s;
+            }
+        </style>
+
+        <title>AI Proxy</title>
+    </head>
+    <body>
+        <div id="root">
+            <!-- 应用加载前显示的加载指示器 -->
+            <div id="app-loading-screen" class="app-loading">
+                <div class="app-loading-background"></div>
+                <div class="app-loading-blur-elements">
+                    <div class="app-loading-blur-1"></div>
+                    <div class="app-loading-blur-2"></div>
+                    <div class="app-loading-blur-3"></div>
+                    
+                    <!-- 粒子效果 - 动态生成 -->
+                    <script>
+                        // 动态创建25个粒子元素
+                        for (let i = 0; i < 25; i++) {
+                            const particle = document.createElement("div");
+                            particle.className = "app-loading-particle";
+                            const size = Math.random() * 6 + 2;
+
+                            particle.style.width = `${size}px`;
+                            particle.style.height = `${size}px`;
+                            particle.style.top = `${Math.random() * 100}%`;
+                            particle.style.left = `${Math.random() * 100}%`;
+                            particle.style.animationDelay = `${
+                                Math.random() * 5
+                            }s`;
+
+                            document.currentScript.parentNode.appendChild(
+                                particle
+                            );
+                        }
+                    </script>
+                </div>
+                <div class="app-loading-content">
+                    <div class="app-loading-text">Loading...</div>
+                    <div class="app-loading-progress-container">
+                        <div
+                            class="app-loading-progress-bar"
+                            id="progressBar"
+                        ></div>
+                    </div>
+                    <div class="app-loading-percentage" id="progressText">
+                        0% Complete
+                    </div>
+                </div>
+            </div>
+        </div>
+        <script>
+            // Match the React component's progress calculation logic exactly
+            (function () {
+                const progressBar = document.getElementById("progressBar");
+                const progressText = document.getElementById("progressText");
+                let progress = 0;
+
+                const timer = setInterval(() => {
+                    // Slow down as it approaches 100%
+                    // 使用Math.floor确保结果是整数
+                    const increment = Math.floor(
+                        Math.max(1, 10 * (1 - progress / 100))
+                    );
+                    progress = Math.min(99, progress + increment);
+                    // 确保最终结果也是整数
+                    progress = Math.floor(progress);
+
+                    // Update the DOM
+                    progressBar.style.width = `${progress}%`;
+                    progressText.textContent = `${progress}% Complete`;
+
+                    if (progress >= 99) {
+                        clearInterval(timer);
+                    }
+                }, 200);
+            })();
+        </script>
+        <script type="module" src="/src/main.tsx"></script>
+    </body>
+</html>

+ 100 - 0
web/openapi.txt

@@ -0,0 +1,100 @@
+/api/token/${group}?auto_create_group=true
+描述信息:创建 token
+方法:post
+参数:
+path group: 组名
+body
+{
+    "name": "token1"
+}
+
+响应结构:
+{
+    "data":{
+        "key": "xE7Lz",
+        "name": "liangfen",
+        "group": "ns-ovore4kv",
+        "subnets": null,
+        "models": null,
+        "status": 1,
+        "id": 1241,
+        "quota": 0,
+        "used_amount": 0,
+        "request_count": 0,
+        "created_at": 1744798413078,
+        "expired_at": -62135596800000,
+        "accessed_at": -62135596800000
+     },
+    "message": "",
+    "success": true
+}
+
+---
+
+/api/tokens/search
+描述信息:获取 token
+方法:get
+参数:
+query
+p 页码
+per_page 每页数量
+
+响应结构:
+{
+	"data": {
+		"tokens": [
+      {
+        "key": "xE7Lz",
+        "name": "liangfen",
+        "group": "ns-ovore4kv",
+        "subnets": null,
+        "models": null,
+        "status": 1,
+        "id": 1241,
+        "quota": 0,
+        "used_amount": 0,
+        "request_count": 0,
+        "created_at": 1744798413078,
+        "expired_at": -62135596800000,
+        "accessed_at": -62135596800000
+     }
+    ],
+		"total": 0
+	},
+	"success": true
+}
+
+---
+
+/api/tokens/:id
+描述信息:删除 token
+方法:delete
+参数:
+id: token id
+
+响应结构:
+{
+	"data": null,
+	"message": "",
+	"success": true
+}
+
+---
+
+/api/tokens/:id/status
+描述信息:更新 token 状态
+方法:post
+参数:
+path id: token id
+body
+{
+    "status": 1
+}
+status 状态 1 启用 2 禁用
+
+响应结构:
+{
+	"data": null,
+	"message": "",
+	"success": true
+}

+ 68 - 0
web/package.json

@@ -0,0 +1,68 @@
+{
+  "name": "admin-pane",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc -b && vite build",
+    "lint": "eslint .",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@hookform/resolvers": "^5.0.1",
+    "@radix-ui/react-alert-dialog": "^1.1.11",
+    "@radix-ui/react-avatar": "^1.1.7",
+    "@radix-ui/react-collapsible": "^1.1.8",
+    "@radix-ui/react-dialog": "^1.1.11",
+    "@radix-ui/react-dropdown-menu": "^2.1.12",
+    "@radix-ui/react-label": "^2.1.4",
+    "@radix-ui/react-select": "^2.2.2",
+    "@radix-ui/react-separator": "^1.1.4",
+    "@radix-ui/react-slot": "^1.2.0",
+    "@radix-ui/react-switch": "^1.2.2",
+    "@radix-ui/react-tooltip": "^1.2.4",
+    "@tailwindcss/vite": "^4.1.4",
+    "@tanstack/react-query": "^5.74.4",
+    "@tanstack/react-query-devtools": "^5.74.6",
+    "@tanstack/react-table": "^8.21.3",
+    "axios": "^1.9.0",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "downshift": "^9.0.9",
+    "i18next": "^25.0.1",
+    "i18next-browser-languagedetector": "^8.0.5",
+    "i18next-http-backend": "^3.0.2",
+    "lucide-react": "^0.503.0",
+    "motion": "^12.9.1",
+    "next-themes": "^0.4.6",
+    "react": "^19.0.0",
+    "react-dom": "^19.0.0",
+    "react-hook-form": "^7.56.1",
+    "react-i18next": "^15.5.1",
+    "react-router": "^7.5.1",
+    "react-syntax-highlighter": "^15.6.1",
+    "sonner": "^2.0.3",
+    "tailwind-merge": "^3.2.0",
+    "tailwindcss": "^4.1.4",
+    "vaul": "^1.1.2",
+    "zod": "^3.24.3",
+    "zustand": "^5.0.3"
+  },
+  "devDependencies": {
+    "@eslint/js": "^9.22.0",
+    "@types/node": "^22.14.1",
+    "@types/react": "^19.0.10",
+    "@types/react-dom": "^19.0.4",
+    "@types/react-syntax-highlighter": "^15.5.13",
+    "@vitejs/plugin-react-swc": "^3.8.0",
+    "eslint": "^9.22.0",
+    "eslint-plugin-react-hooks": "^5.2.0",
+    "eslint-plugin-react-refresh": "^0.4.19",
+    "globals": "^16.0.0",
+    "tw-animate-css": "^1.2.8",
+    "typescript": "~5.7.2",
+    "typescript-eslint": "^8.26.1",
+    "vite": "^6.3.1"
+  }
+}

+ 4013 - 0
web/pnpm-lock.yaml

@@ -0,0 +1,4013 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    dependencies:
+      '@hookform/resolvers':
+        specifier: ^5.0.1
+        version: 5.0.1([email protected]([email protected]))
+      '@radix-ui/react-alert-dialog':
+        specifier: ^1.1.11
+        version: 1.1.11(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-avatar':
+        specifier: ^1.1.7
+        version: 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-collapsible':
+        specifier: ^1.1.8
+        version: 1.1.8(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-dialog':
+        specifier: ^1.1.11
+        version: 1.1.11(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-dropdown-menu':
+        specifier: ^2.1.12
+        version: 2.1.12(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-label':
+        specifier: ^2.1.4
+        version: 2.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-select':
+        specifier: ^2.2.2
+        version: 2.2.2(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-separator':
+        specifier: ^1.1.4
+        version: 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot':
+        specifier: ^1.2.0
+        version: 1.2.0(@types/[email protected])([email protected])
+      '@radix-ui/react-switch':
+        specifier: ^1.2.2
+        version: 1.2.2(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-tooltip':
+        specifier: ^1.2.4
+        version: 1.2.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@tailwindcss/vite':
+        specifier: ^4.1.4
+        version: 4.1.4([email protected](@types/[email protected])([email protected])([email protected]))
+      '@tanstack/react-query':
+        specifier: ^5.74.4
+        version: 5.74.4([email protected])
+      '@tanstack/react-query-devtools':
+        specifier: ^5.74.6
+        version: 5.74.6(@tanstack/[email protected]([email protected]))([email protected])
+      '@tanstack/react-table':
+        specifier: ^8.21.3
+        version: 8.21.3([email protected]([email protected]))([email protected])
+      axios:
+        specifier: ^1.9.0
+        version: 1.9.0
+      class-variance-authority:
+        specifier: ^0.7.1
+        version: 0.7.1
+      clsx:
+        specifier: ^2.1.1
+        version: 2.1.1
+      downshift:
+        specifier: ^9.0.9
+        version: 9.0.9([email protected])
+      i18next:
+        specifier: ^25.0.1
+        version: 25.0.1([email protected])
+      i18next-browser-languagedetector:
+        specifier: ^8.0.5
+        version: 8.0.5
+      i18next-http-backend:
+        specifier: ^3.0.2
+        version: 3.0.2
+      lucide-react:
+        specifier: ^0.503.0
+        version: 0.503.0([email protected])
+      motion:
+        specifier: ^12.9.1
+        version: 12.9.1([email protected]([email protected]))([email protected])
+      next-themes:
+        specifier: ^0.4.6
+        version: 0.4.6([email protected]([email protected]))([email protected])
+      react:
+        specifier: ^19.0.0
+        version: 19.1.0
+      react-dom:
+        specifier: ^19.0.0
+        version: 19.1.0([email protected])
+      react-hook-form:
+        specifier: ^7.56.1
+        version: 7.56.1([email protected])
+      react-i18next:
+        specifier: ^15.5.1
+        version: 15.5.1([email protected]([email protected]))([email protected]([email protected]))([email protected])([email protected])
+      react-router:
+        specifier: ^7.5.1
+        version: 7.5.1([email protected]([email protected]))([email protected])
+      react-syntax-highlighter:
+        specifier: ^15.6.1
+        version: 15.6.1([email protected])
+      sonner:
+        specifier: ^2.0.3
+        version: 2.0.3([email protected]([email protected]))([email protected])
+      tailwind-merge:
+        specifier: ^3.2.0
+        version: 3.2.0
+      tailwindcss:
+        specifier: ^4.1.4
+        version: 4.1.4
+      vaul:
+        specifier: ^1.1.2
+        version: 1.1.2(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      zod:
+        specifier: ^3.24.3
+        version: 3.24.3
+      zustand:
+        specifier: ^5.0.3
+        version: 5.0.3(@types/[email protected])([email protected])([email protected]([email protected]))
+    devDependencies:
+      '@eslint/js':
+        specifier: ^9.22.0
+        version: 9.25.1
+      '@types/node':
+        specifier: ^22.14.1
+        version: 22.14.1
+      '@types/react':
+        specifier: ^19.0.10
+        version: 19.1.2
+      '@types/react-dom':
+        specifier: ^19.0.4
+        version: 19.1.2(@types/[email protected])
+      '@types/react-syntax-highlighter':
+        specifier: ^15.5.13
+        version: 15.5.13
+      '@vitejs/plugin-react-swc':
+        specifier: ^3.8.0
+        version: 3.9.0([email protected](@types/[email protected])([email protected])([email protected]))
+      eslint:
+        specifier: ^9.22.0
+        version: 9.25.1([email protected])
+      eslint-plugin-react-hooks:
+        specifier: ^5.2.0
+        version: 5.2.0([email protected]([email protected]))
+      eslint-plugin-react-refresh:
+        specifier: ^0.4.19
+        version: 0.4.20([email protected]([email protected]))
+      globals:
+        specifier: ^16.0.0
+        version: 16.0.0
+      tw-animate-css:
+        specifier: ^1.2.8
+        version: 1.2.8
+      typescript:
+        specifier: ~5.7.2
+        version: 5.7.3
+      typescript-eslint:
+        specifier: ^8.26.1
+        version: 8.31.0([email protected]([email protected]))([email protected])
+      vite:
+        specifier: ^6.3.1
+        version: 6.3.3(@types/[email protected])([email protected])([email protected])
+
+packages:
+
+  '@babel/[email protected]':
+    resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==}
+    engines: {node: '>=6.9.0'}
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [netbsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
+  '@eslint-community/[email protected]':
+    resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+    peerDependencies:
+      eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+  '@eslint-community/[email protected]':
+    resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
+    engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@floating-ui/[email protected]':
+    resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==}
+
+  '@floating-ui/[email protected]':
+    resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
+
+  '@floating-ui/[email protected]':
+    resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==}
+    peerDependencies:
+      react: '>=16.8.0'
+      react-dom: '>=16.8.0'
+
+  '@floating-ui/[email protected]':
+    resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
+
+  '@hookform/[email protected]':
+    resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==}
+    peerDependencies:
+      react-hook-form: ^7.55.0
+
+  '@humanfs/[email protected]':
+    resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
+    engines: {node: '>=18.18.0'}
+
+  '@humanfs/[email protected]':
+    resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
+    engines: {node: '>=18.18.0'}
+
+  '@humanwhocodes/[email protected]':
+    resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+    engines: {node: '>=12.22'}
+
+  '@humanwhocodes/[email protected]':
+    resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
+    engines: {node: '>=18.18'}
+
+  '@humanwhocodes/[email protected]':
+    resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==}
+    engines: {node: '>=18.18'}
+
+  '@nodelib/[email protected]':
+    resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+    engines: {node: '>= 8'}
+
+  '@nodelib/[email protected]':
+    resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+    engines: {node: '>= 8'}
+
+  '@nodelib/[email protected]':
+    resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+    engines: {node: '>= 8'}
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-4KfkwrFnAw3Y5Jeoq6G+JYSKW0JfIS3uDdFC/79Jw9AsMayZMizSSMxk1gkrolYXsa/WzbbDfOA7/D8N5D+l1g==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-V7ODUt4mUoJTe3VUxZw6nfURxaPALVqmDQh501YmaQsk3D8AZQrOPRnfKn4H7JGDLBc0KqLhT94H79nV88ppNg==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-hxEsLvK9WxIAPyxdDRULL4hcaSjMZCfP7fHB0Z1uUnDoDBat1Zh46hwYfa69DeZAbJrPckjf0AGAtEZyvDyJbw==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-yI7S1ipkP5/+99qhSI6nthfo/tR6bL6Zgxi/+1UO6qPa6UeM6nlafWcQ65vB4rU2XjgjMfMhI3k9Y5MztA62VQ==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-VJoMs+BWWE7YhzEQyVwvF9n22Eiyr83HotCVrMQzla/OwRovXCgah7AcaEr4hMNj4gJxSdtIbcHGvmJXOoJVHA==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-wy3dqizZnZVV4ja0FNnUhIWNwWdoldXrneEyUcVtLYDAt8ovGS4ridtMAOGgXBBIfggL4BOveVWsjXDORdGEQg==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-+qYq6LfbiGo97Zz9fioX83HCiIYYFNs8zAsVCMQrIakoNYylIzWuoD/anAD3UzvvR6cnswmfRFJFq/zYYq/k7Q==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-HjkVHtBkuq+r3zUAZ/CvNWUGKPfuicGDbgtZgiQuFmNcV5F+Tgy24ep2nsAW2nFgvhGPJVqeBZa6KyVN0EyrBA==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-2fTm6PSiUm8YPq9W0E4reYuv01EE3aFSzt8edBiXqPHshF8N9+Kymt/k0/R+F3dkY5lQyB/zPtrP82phskLi7w==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-7Z8n6L+ifMIIYZ83f28qWSceUpkXuslI2FJ34+kDMTiyj91ENdpdQ7VCidrzj5JfwfZTeano/BnGBbu/jqa5rQ==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-DyW8VVeeMSSLFvAmnVnCwvI3H+1tpJFHT50r+tdOoMse9XqYDBCcyux8u3G2y+LOpt7fPQ6KKH0mhs+ce1+Z5w==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-rQj0aAWOpCdCMRbI6pLQm8r7S2BM3YhTa0SzOYD55k+hJA8oo9J+H+9wLM9oMlZWOX/wJWPTzfDfmZkf7LvCfg==}
+    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
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==}
+    cpu: [arm]
+    os: [android]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==}
+    cpu: [arm64]
+    os: [android]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==}
+    cpu: [x64]
+    os: [darwin]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==}
+    cpu: [arm]
+    os: [linux]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==}
+    cpu: [arm]
+    os: [linux]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==}
+    cpu: [arm64]
+    os: [linux]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==}
+    cpu: [arm64]
+    os: [linux]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==}
+    cpu: [loong64]
+    os: [linux]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==}
+    cpu: [s390x]
+    os: [linux]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==}
+    cpu: [x64]
+    os: [linux]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==}
+    cpu: [x64]
+    os: [linux]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==}
+    cpu: [arm64]
+    os: [win32]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==}
+    cpu: [ia32]
+    os: [win32]
+
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==}
+    cpu: [x64]
+    os: [win32]
+
+  '@standard-schema/[email protected]':
+    resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-upSiFQfo1TE2QM3+KpBcp5SrOdKKjoc+oUoD1mmBDU2Wv4Bjjv16Z2I5ADvIqMV+b87AhYW+4Qu6iVrQD7j96Q==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-8PEuF/gxIMJVK21DjuCOtzdqstn2DqnxVhpAYfXEtm3WmMqLIOIZBypF/xafAozyaHws4aB/5xmz8/7rPsjavw==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-NIPTXvqtn9e7oQHgdaxM9Z/anHoXC3Fg4ZAgw5rSGa1OlnKKupt5sdfJamNggSi+eAtyoFcyfkgqHnfe2u63HA==}
+    engines: {node: '>=10'}
+    cpu: [arm]
+    os: [linux]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-xZ+bgS60c5r8kAeYsLNjJJhhQNkXdidQ277pUabSlu5GjR0CkQUPQ+L9hFeHf8DITEqpPBPRiAiiJsWq5eqMBg==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-JhrP/q5VqQl2eJR0xKYIkKTPjgf8CRsAmRnjJA2PtZhfQ543YbYvUqxyXSRyBOxdyX8JwzuAxIPEAlKlT7PPuQ==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-htmAVL+U01gk9GyziVUP0UWYaUQBgrsiP7Ytf6uDffrySyn/FclUS3MDPocNydqYsOpj3OpNKPxkaHK+F+X5fg==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [linux]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-PL0VHbduWPX+ANoyOzr58jBiL2VnD0xGSFwPy7NRZ1Pr6SNWm4jw3x2u6RjLArGhS5EcWp64BSk9ZxqmTV3FEg==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [linux]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-moJvFhhTVGoMeEThtdF7hQog80Q00CS06v5uB+32VRuv+I31+4WPRyGlTWHO+oY4rReNcXut/mlDHPH7p0LdFg==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-/jnsPJJz89F1aKHIb5ScHkwyzBciz2AjEq2m9tDvQdIdVufdJ4SpEDEN9FqsRNRLcBHjtbLs6bnboA+B+pRFXw==}
+    engines: {node: '>=10'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-lc93Y8Mku7LCFGqIxJ91coXZp2HeoDcFZSHCL90Wttg5xhk5xVM9uUCP+OdQsSsEixLF34h5DbT9ObzP8rAdRw==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [win32]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-mjPYbqq8XjwqSE0hEPT9CzaJDyxql97LgK4iyvYlwVSQhdN1uK0DBG4eP9PxYzCS2MUGAXB34WFLegdUj5HGpg==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@swc/helpers': '>=0.5.17'
+    peerDependenciesMeta:
+      '@swc/helpers':
+        optional: true
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==}
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==}
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [android]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg==}
+    engines: {node: '>= 10'}
+    cpu: [arm]
+    os: [linux]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==}
+    engines: {node: '>=14.0.0'}
+    cpu: [wasm32]
+    bundledDependencies:
+      - '@napi-rs/wasm-runtime'
+      - '@emnapi/core'
+      - '@emnapi/runtime'
+      - '@tybys/wasm-util'
+      - '@emnapi/wasi-threads'
+      - tslib
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [win32]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ==}
+    engines: {node: '>= 10'}
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-4UQeMrONbvrsXKXXp/uxmdEN5JIJ9RkH7YVzs6AMxC/KC1+Np7WZBaNIco7TEjlkthqxZbt8pU/ipD+hKjm80A==}
+    peerDependencies:
+      vite: ^5.2.0 || ^6
+
+  '@tanstack/[email protected]':
+    resolution: {integrity: sha512-YuG0A0+3i9b2Gfo9fkmNnkUWh5+5cFhWBN0pJAHkHilTx6A0nv8kepkk4T4GRt4e5ahbtFj2eTtkiPcVU1xO4A==}
+
+  '@tanstack/[email protected]':
+    resolution: {integrity: sha512-djaFT11mVCOW3e0Ezfyiq7T6OoHy2LRI1fUFQvj+G6+/4A1FkuRMNUhQkdP1GXlx8id0f1/zd5fgDpIy5SU/Iw==}
+
+  '@tanstack/[email protected]':
+    resolution: {integrity: sha512-vlsDwz4/FsblK0h7VAlXUdJ+9OV+i1n8OLb8CLLAZqu0M9GCnbajytZwsRmns33PXBZ6wQBJ859kg6aajx+e9Q==}
+    peerDependencies:
+      '@tanstack/react-query': ^5.74.4
+      react: ^18 || ^19
+
+  '@tanstack/[email protected]':
+    resolution: {integrity: sha512-mAbxw60d4ffQ4qmRYfkO1xzRBPUEf/72Dgo3qqea0J66nIKuDTLEqQt0ku++SDFlMGMnB6uKDnEG1xD/TDse4Q==}
+    peerDependencies:
+      react: ^18 || ^19
+
+  '@tanstack/[email protected]':
+    resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
+    engines: {node: '>=12'}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@tanstack/[email protected]':
+    resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
+    engines: {node: '>=12'}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==}
+    peerDependencies:
+      '@types/react': ^19.0.0
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <5.9.0'
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <5.9.0'
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <5.9.0'
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <5.9.0'
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <5.9.0'
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@vitejs/[email protected]':
+    resolution: {integrity: sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==}
+    peerDependencies:
+      vite: ^4 || ^5 || ^6
+
+  [email protected]:
+    resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+    peerDependencies:
+      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+  [email protected]:
+    resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+  [email protected]:
+    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+  [email protected]:
+    resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+  [email protected]:
+    resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+    engines: {node: '>=7.0.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+    engines: {node: '>= 0.8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
+    engines: {node: '>=18'}
+
+  [email protected]:
+    resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
+
+  [email protected]:
+    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+    engines: {node: '>= 8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+    engines: {node: '>=0.4.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-ygOT8blgiz5liDuEFAIaPeU4dDEa+w9p6PHVUisPIjrkF5wfR59a52HpGWAVVMoWnoFO8po2mZSScKZueihS7g==}
+    peerDependencies:
+      react: '>=16.12.0'
+
+  [email protected]:
+    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
+    engines: {node: '>=10.13.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==}
+    engines: {node: '>=18'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
+
+  [email protected]:
+    resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==}
+    peerDependencies:
+      eslint: '>=8.40'
+
+  [email protected]:
+    resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  [email protected]:
+    resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  [email protected]:
+    resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  [email protected]:
+    resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    hasBin: true
+    peerDependencies:
+      jiti: '*'
+    peerDependenciesMeta:
+      jiti:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  [email protected]:
+    resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+    engines: {node: '>=0.10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+    engines: {node: '>=4.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+    engines: {node: '>=4.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+  [email protected]:
+    resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+    engines: {node: '>=8.6.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==}
+    peerDependencies:
+      picomatch: ^3 || ^4
+    peerDependenciesMeta:
+      picomatch:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+    engines: {node: '>=16.0.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+    engines: {node: '>=16'}
+
+  [email protected]:
+    resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
+    engines: {node: '>= 6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
+    engines: {node: '>=0.4.x'}
+
+  [email protected]:
+    resolution: {integrity: sha512-dZBp2TO0a39Cc24opshlLoM0/OdTZVKzcXWuhntfwy2Qgz3t9+N4sTyUqNANyHaRFiJUWbwwsXeDvQkEBPky+g==}
+    peerDependencies:
+      '@emotion/is-prop-valid': '*'
+      react: ^18.0.0 || ^19.0.0
+      react-dom: ^18.0.0 || ^19.0.0
+    peerDependenciesMeta:
+      '@emotion/is-prop-valid':
+        optional: true
+      react:
+        optional: true
+      react-dom:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+
+  [email protected]:
+    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+    engines: {node: '>= 6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+    engines: {node: '>=10.13.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+    engines: {node: '>=18'}
+
+  [email protected]:
+    resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==}
+    engines: {node: '>=18'}
+
+  [email protected]:
+    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+
+  [email protected]:
+    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==}
+
+  [email protected]:
+    resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
+
+  [email protected]:
+    resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-OstebRKqKiQw8xEvQF5aRyUujsCatanj7Q9eo5iiH2gJpoXGZ7483ol3sVBwfqbobTQPNH1J+NAyJ1aCQoEC+w==}
+
+  [email protected]:
+    resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==}
+
+  [email protected]:
+    resolution: {integrity: sha512-8S8PyZbrymJZn3DaN70/34JYWNhsqrU6yA4MuzcygJBv+41dgNMocEA8h+kV1P7MCc1ll03lOTOIXE7mpNCicw==}
+    peerDependencies:
+      typescript: ^5
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+    engines: {node: '>= 4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+    engines: {node: '>=0.8.19'}
+
+  [email protected]:
+    resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==}
+
+  [email protected]:
+    resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+    engines: {node: '>=0.12.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+    engines: {node: '>= 0.8.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm64]
+    os: [darwin]
+
+  [email protected]:
+    resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [darwin]
+
+  [email protected]:
+    resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [freebsd]
+
+  [email protected]:
+    resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm]
+    os: [linux]
+
+  [email protected]:
+    resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm64]
+    os: [linux]
+
+  [email protected]:
+    resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm64]
+    os: [linux]
+
+  [email protected]:
+    resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [linux]
+
+  [email protected]:
+    resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [linux]
+
+  [email protected]:
+    resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm64]
+    os: [win32]
+
+  [email protected]:
+    resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [win32]
+
+  [email protected]:
+    resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==}
+    engines: {node: '>= 12.0.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w==}
+    peerDependencies:
+      react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+  [email protected]:
+    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+    engines: {node: '>= 8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+    engines: {node: '>=8.6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+    engines: {node: '>= 0.6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+    engines: {node: '>= 0.6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+    engines: {node: '>=16 || 14 >=14.17'}
+
+  [email protected]:
+    resolution: {integrity: sha512-xqXEwRLDYDTzOgXobSoWtytRtGlf7zdkRfFbrrdP7eojaGQZ5Go4OOKtgnx7uF8sAkfr1ZjMvbCJSCIT2h6fkQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-amdtlwafU+XLPcrfSrOQ/S2sqiSw+UTywH+X/Yoqaz0qYEocqJKh8bs6M09CdRmkjZuKx2YM+BHodXjsqoTEag==}
+    peerDependencies:
+      '@emotion/is-prop-valid': '*'
+      react: ^18.0.0 || ^19.0.0
+      react-dom: ^18.0.0 || ^19.0.0
+    peerDependenciesMeta:
+      '@emotion/is-prop-valid':
+        optional: true
+      react:
+        optional: true
+      react-dom:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
+    peerDependencies:
+      react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+
+  [email protected]:
+    resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+    engines: {node: 4.x || >=6.0.0}
+    peerDependencies:
+      encoding: ^0.1.0
+    peerDependenciesMeta:
+      encoding:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+    engines: {node: '>= 0.8.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+    engines: {node: '>=8.6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
+    engines: {node: ^10 || ^12 || >=14}
+
+  [email protected]:
+    resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+    engines: {node: '>= 0.8.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+  [email protected]:
+    resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
+    peerDependencies:
+      react: ^19.1.0
+
+  [email protected]:
+    resolution: {integrity: sha512-qWAVokhSpshhcEuQDSANHx3jiAEFzu2HAaaQIzi/r9FNPm1ioAvuJSD4EuZzWd7Al7nTRKcKPnBKO7sRn+zavQ==}
+    engines: {node: '>=18.0.0'}
+    peerDependencies:
+      react: ^16.8.0 || ^17 || ^18 || ^19
+
+  [email protected]:
+    resolution: {integrity: sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==}
+    peerDependencies:
+      i18next: '>= 23.2.3'
+      react: '>= 16.8.0'
+      react-dom: '*'
+      react-native: '*'
+      typescript: ^5
+    peerDependenciesMeta:
+      react-dom:
+        optional: true
+      react-native:
+        optional: true
+      typescript:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
+
+  [email protected]:
+    resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-/jjU3fcYNd2bwz9Q0xt5TwyiyoO8XjSEFXJY4O/lMAlkGTHWuHRAbR9Etik+lSDqMC7A7mz3UlXzgYT6Vl58sA==}
+    engines: {node: '>=20.0.0'}
+    peerDependencies:
+      react: '>=18'
+      react-dom: '>=18'
+    peerDependenciesMeta:
+      react-dom:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==}
+    peerDependencies:
+      react: '>= 0.14.0'
+
+  [email protected]:
+    resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+    engines: {node: '>=4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+    engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==}
+    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
+    engines: {node: '>=10'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==}
+    peerDependencies:
+      react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+      react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
+  [email protected]:
+    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==}
+
+  [email protected]:
+    resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
+    engines: {node: '>=12.0.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+    engines: {node: '>=8.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
+    engines: {node: '>=18.12'}
+    peerDependencies:
+      typescript: '>=4.8.4'
+
+  [email protected]:
+    resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+  [email protected]:
+    resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
+
+  [email protected]:
+    resolution: {integrity: sha512-AxSnYRvyFnAiZCUndS3zQZhNfV/B77ZhJ+O7d3K6wfg/jKJY+yv6ahuyXwnyaYA9UdLqnpCwhTRv9pPTBnPR2g==}
+
+  [email protected]:
+    resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+    engines: {node: '>= 0.8.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-u+93F0sB0An8WEAPtwxVhFby573E8ckdjwUUQUj9QA4v8JAvgtoDdIyYR3XFwFHq2W1KJ1AurwJCO+w+Y1ixyQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <5.9.0'
+
+  [email protected]:
+    resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+  [email protected]:
+    resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+
+  [email protected]:
+    resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==}
+    engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+    hasBin: true
+    peerDependencies:
+      '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+      jiti: '>=1.21.0'
+      less: '*'
+      lightningcss: ^1.21.0
+      sass: '*'
+      sass-embedded: '*'
+      stylus: '*'
+      sugarss: '*'
+      terser: ^5.16.0
+      tsx: ^4.8.1
+      yaml: ^2.4.2
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      jiti:
+        optional: true
+      less:
+        optional: true
+      lightningcss:
+        optional: true
+      sass:
+        optional: true
+      sass-embedded:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+      tsx:
+        optional: true
+      yaml:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+    engines: {node: '>= 8'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
+    engines: {node: '>=0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
+    engines: {node: '>=12.20.0'}
+    peerDependencies:
+      '@types/react': '>=18.0.0'
+      immer: '>=9.0.6'
+      react: '>=18.0.0'
+      use-sync-external-store: '>=1.2.0'
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      immer:
+        optional: true
+      react:
+        optional: true
+      use-sync-external-store:
+        optional: true
+
+snapshots:
+
+  '@babel/[email protected]':
+    dependencies:
+      regenerator-runtime: 0.14.1
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@eslint-community/[email protected]([email protected]([email protected]))':
+    dependencies:
+      eslint: 9.25.1([email protected])
+      eslint-visitor-keys: 3.4.3
+
+  '@eslint-community/[email protected]': {}
+
+  '@eslint/[email protected]':
+    dependencies:
+      '@eslint/object-schema': 2.1.6
+      debug: 4.4.0
+      minimatch: 3.1.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/[email protected]': {}
+
+  '@eslint/[email protected]':
+    dependencies:
+      '@types/json-schema': 7.0.15
+
+  '@eslint/[email protected]':
+    dependencies:
+      ajv: 6.12.6
+      debug: 4.4.0
+      espree: 10.3.0
+      globals: 14.0.0
+      ignore: 5.3.2
+      import-fresh: 3.3.1
+      js-yaml: 4.1.0
+      minimatch: 3.1.2
+      strip-json-comments: 3.1.1
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/[email protected]': {}
+
+  '@eslint/[email protected]': {}
+
+  '@eslint/[email protected]':
+    dependencies:
+      '@eslint/core': 0.13.0
+      levn: 0.4.1
+
+  '@floating-ui/[email protected]':
+    dependencies:
+      '@floating-ui/utils': 0.2.9
+
+  '@floating-ui/[email protected]':
+    dependencies:
+      '@floating-ui/core': 1.6.9
+      '@floating-ui/utils': 0.2.9
+
+  '@floating-ui/[email protected]([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@floating-ui/dom': 1.6.13
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+
+  '@floating-ui/[email protected]': {}
+
+  '@hookform/[email protected]([email protected]([email protected]))':
+    dependencies:
+      '@standard-schema/utils': 0.3.0
+      react-hook-form: 7.56.1([email protected])
+
+  '@humanfs/[email protected]': {}
+
+  '@humanfs/[email protected]':
+    dependencies:
+      '@humanfs/core': 0.19.1
+      '@humanwhocodes/retry': 0.3.1
+
+  '@humanwhocodes/[email protected]': {}
+
+  '@humanwhocodes/[email protected]': {}
+
+  '@humanwhocodes/[email protected]': {}
+
+  '@nodelib/[email protected]':
+    dependencies:
+      '@nodelib/fs.stat': 2.0.5
+      run-parallel: 1.2.0
+
+  '@nodelib/[email protected]': {}
+
+  '@nodelib/[email protected]':
+    dependencies:
+      '@nodelib/fs.scandir': 2.1.5
+      fastq: 1.19.1
+
+  '@radix-ui/[email protected]': {}
+
+  '@radix-ui/[email protected]': {}
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-dialog': 1.1.11(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot': 1.2.0(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-is-hydrated': 0.1.0(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-presence': 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot': 1.2.0(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-dismissable-layer': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-focus-guards': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-focus-scope': 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-portal': 1.1.6(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-presence': 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot': 1.2.0(@types/[email protected])([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      aria-hidden: 1.2.4
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+      react-remove-scroll: 2.6.3(@types/[email protected])([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-escape-keydown': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-menu': 2.1.12(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-collection': 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-direction': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-dismissable-layer': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-focus-guards': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-focus-scope': 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-popper': 1.2.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-portal': 1.1.6(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-presence': 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-roving-focus': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot': 1.2.0(@types/[email protected])([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      aria-hidden: 1.2.4
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+      react-remove-scroll: 2.6.3(@types/[email protected])([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@floating-ui/react-dom': 2.1.2([email protected]([email protected]))([email protected])
+      '@radix-ui/react-arrow': 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-rect': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-size': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/rect': 1.1.1
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-slot': 1.2.0(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-collection': 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-direction': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/number': 1.1.1
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-collection': 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-direction': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-dismissable-layer': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-focus-guards': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-focus-scope': 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-popper': 1.2.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-portal': 1.1.6(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot': 1.2.0(@types/[email protected])([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-previous': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-visually-hidden': 1.2.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      aria-hidden: 1.2.4
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+      react-remove-scroll: 2.6.3(@types/[email protected])([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      '@radix-ui/react-use-previous': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-size': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-dismissable-layer': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-popper': 1.2.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-portal': 1.1.6(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-presence': 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot': 1.2.0(@types/[email protected])([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      '@radix-ui/react-visually-hidden': 1.2.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/react-use-effect-event': 0.0.2(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+      use-sync-external-store: 1.5.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/rect': 1.1.1
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
+  '@radix-ui/[email protected]': {}
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@rollup/[email protected]':
+    optional: true
+
+  '@standard-schema/[email protected]': {}
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    dependencies:
+      '@swc/counter': 0.1.3
+      '@swc/types': 0.1.21
+    optionalDependencies:
+      '@swc/core-darwin-arm64': 1.11.22
+      '@swc/core-darwin-x64': 1.11.22
+      '@swc/core-linux-arm-gnueabihf': 1.11.22
+      '@swc/core-linux-arm64-gnu': 1.11.22
+      '@swc/core-linux-arm64-musl': 1.11.22
+      '@swc/core-linux-x64-gnu': 1.11.22
+      '@swc/core-linux-x64-musl': 1.11.22
+      '@swc/core-win32-arm64-msvc': 1.11.22
+      '@swc/core-win32-ia32-msvc': 1.11.22
+      '@swc/core-win32-x64-msvc': 1.11.22
+
+  '@swc/[email protected]': {}
+
+  '@swc/[email protected]':
+    dependencies:
+      '@swc/counter': 0.1.3
+
+  '@tailwindcss/[email protected]':
+    dependencies:
+      enhanced-resolve: 5.18.1
+      jiti: 2.4.2
+      lightningcss: 1.29.2
+      tailwindcss: 4.1.4
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optionalDependencies:
+      '@tailwindcss/oxide-android-arm64': 4.1.4
+      '@tailwindcss/oxide-darwin-arm64': 4.1.4
+      '@tailwindcss/oxide-darwin-x64': 4.1.4
+      '@tailwindcss/oxide-freebsd-x64': 4.1.4
+      '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.4
+      '@tailwindcss/oxide-linux-arm64-gnu': 4.1.4
+      '@tailwindcss/oxide-linux-arm64-musl': 4.1.4
+      '@tailwindcss/oxide-linux-x64-gnu': 4.1.4
+      '@tailwindcss/oxide-linux-x64-musl': 4.1.4
+      '@tailwindcss/oxide-wasm32-wasi': 4.1.4
+      '@tailwindcss/oxide-win32-arm64-msvc': 4.1.4
+      '@tailwindcss/oxide-win32-x64-msvc': 4.1.4
+
+  '@tailwindcss/[email protected]([email protected](@types/[email protected])([email protected])([email protected]))':
+    dependencies:
+      '@tailwindcss/node': 4.1.4
+      '@tailwindcss/oxide': 4.1.4
+      tailwindcss: 4.1.4
+      vite: 6.3.3(@types/[email protected])([email protected])([email protected])
+
+  '@tanstack/[email protected]': {}
+
+  '@tanstack/[email protected]': {}
+
+  '@tanstack/[email protected](@tanstack/[email protected]([email protected]))([email protected])':
+    dependencies:
+      '@tanstack/query-devtools': 5.74.6
+      '@tanstack/react-query': 5.74.4([email protected])
+      react: 19.1.0
+
+  '@tanstack/[email protected]([email protected])':
+    dependencies:
+      '@tanstack/query-core': 5.74.4
+      react: 19.1.0
+
+  '@tanstack/[email protected]([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@tanstack/table-core': 8.21.3
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+
+  '@tanstack/[email protected]': {}
+
+  '@types/[email protected]': {}
+
+  '@types/[email protected]':
+    dependencies:
+      '@types/unist': 2.0.11
+
+  '@types/[email protected]': {}
+
+  '@types/[email protected]':
+    dependencies:
+      undici-types: 6.21.0
+
+  '@types/[email protected](@types/[email protected])':
+    dependencies:
+      '@types/react': 19.1.2
+
+  '@types/[email protected]':
+    dependencies:
+      '@types/react': 19.1.2
+
+  '@types/[email protected]':
+    dependencies:
+      csstype: 3.1.3
+
+  '@types/[email protected]': {}
+
+  '@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@eslint-community/regexpp': 4.12.1
+      '@typescript-eslint/parser': 8.31.0([email protected]([email protected]))([email protected])
+      '@typescript-eslint/scope-manager': 8.31.0
+      '@typescript-eslint/type-utils': 8.31.0([email protected]([email protected]))([email protected])
+      '@typescript-eslint/utils': 8.31.0([email protected]([email protected]))([email protected])
+      '@typescript-eslint/visitor-keys': 8.31.0
+      eslint: 9.25.1([email protected])
+      graphemer: 1.4.0
+      ignore: 5.3.2
+      natural-compare: 1.4.0
+      ts-api-utils: 2.1.0([email protected])
+      typescript: 5.7.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@typescript-eslint/scope-manager': 8.31.0
+      '@typescript-eslint/types': 8.31.0
+      '@typescript-eslint/typescript-estree': 8.31.0([email protected])
+      '@typescript-eslint/visitor-keys': 8.31.0
+      debug: 4.4.0
+      eslint: 9.25.1([email protected])
+      typescript: 5.7.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/[email protected]':
+    dependencies:
+      '@typescript-eslint/types': 8.31.0
+      '@typescript-eslint/visitor-keys': 8.31.0
+
+  '@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@typescript-eslint/typescript-estree': 8.31.0([email protected])
+      '@typescript-eslint/utils': 8.31.0([email protected]([email protected]))([email protected])
+      debug: 4.4.0
+      eslint: 9.25.1([email protected])
+      ts-api-utils: 2.1.0([email protected])
+      typescript: 5.7.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/[email protected]': {}
+
+  '@typescript-eslint/[email protected]([email protected])':
+    dependencies:
+      '@typescript-eslint/types': 8.31.0
+      '@typescript-eslint/visitor-keys': 8.31.0
+      debug: 4.4.0
+      fast-glob: 3.3.3
+      is-glob: 4.0.3
+      minimatch: 9.0.5
+      semver: 7.7.1
+      ts-api-utils: 2.1.0([email protected])
+      typescript: 5.7.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@eslint-community/eslint-utils': 4.6.1([email protected]([email protected]))
+      '@typescript-eslint/scope-manager': 8.31.0
+      '@typescript-eslint/types': 8.31.0
+      '@typescript-eslint/typescript-estree': 8.31.0([email protected])
+      eslint: 9.25.1([email protected])
+      typescript: 5.7.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/[email protected]':
+    dependencies:
+      '@typescript-eslint/types': 8.31.0
+      eslint-visitor-keys: 4.2.0
+
+  '@vitejs/[email protected]([email protected](@types/[email protected])([email protected])([email protected]))':
+    dependencies:
+      '@swc/core': 1.11.22
+      vite: 6.3.3(@types/[email protected])([email protected])([email protected])
+    transitivePeerDependencies:
+      - '@swc/helpers'
+
+  [email protected]([email protected]):
+    dependencies:
+      acorn: 8.14.1
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      fast-deep-equal: 3.1.3
+      fast-json-stable-stringify: 2.1.0
+      json-schema-traverse: 0.4.1
+      uri-js: 4.4.1
+
+  [email protected]:
+    dependencies:
+      color-convert: 2.0.1
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      tslib: 2.8.1
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      follow-redirects: 1.15.9
+      form-data: 4.0.2
+      proxy-from-env: 1.1.0
+    transitivePeerDependencies:
+      - debug
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      balanced-match: 1.0.2
+      concat-map: 0.0.1
+
+  [email protected]:
+    dependencies:
+      balanced-match: 1.0.2
+
+  [email protected]:
+    dependencies:
+      fill-range: 7.1.1
+
+  [email protected]:
+    dependencies:
+      es-errors: 1.3.0
+      function-bind: 1.1.2
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      ansi-styles: 4.3.0
+      supports-color: 7.2.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      clsx: 2.1.1
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      color-name: 1.1.4
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      delayed-stream: 1.0.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      node-fetch: 2.7.0
+    transitivePeerDependencies:
+      - encoding
+
+  [email protected]:
+    dependencies:
+      path-key: 3.1.1
+      shebang-command: 2.0.0
+      which: 2.0.2
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      ms: 2.1.3
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]([email protected]):
+    dependencies:
+      '@babel/runtime': 7.27.0
+      compute-scroll-into-view: 3.1.1
+      prop-types: 15.8.1
+      react: 19.1.0
+      react-is: 18.2.0
+      tslib: 2.8.1
+
+  [email protected]:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-errors: 1.3.0
+      gopd: 1.2.0
+
+  [email protected]:
+    dependencies:
+      graceful-fs: 4.2.11
+      tapable: 2.2.1
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      es-errors: 1.3.0
+
+  [email protected]:
+    dependencies:
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      has-tostringtag: 1.0.2
+      hasown: 2.0.2
+
+  [email protected]:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.25.3
+      '@esbuild/android-arm': 0.25.3
+      '@esbuild/android-arm64': 0.25.3
+      '@esbuild/android-x64': 0.25.3
+      '@esbuild/darwin-arm64': 0.25.3
+      '@esbuild/darwin-x64': 0.25.3
+      '@esbuild/freebsd-arm64': 0.25.3
+      '@esbuild/freebsd-x64': 0.25.3
+      '@esbuild/linux-arm': 0.25.3
+      '@esbuild/linux-arm64': 0.25.3
+      '@esbuild/linux-ia32': 0.25.3
+      '@esbuild/linux-loong64': 0.25.3
+      '@esbuild/linux-mips64el': 0.25.3
+      '@esbuild/linux-ppc64': 0.25.3
+      '@esbuild/linux-riscv64': 0.25.3
+      '@esbuild/linux-s390x': 0.25.3
+      '@esbuild/linux-x64': 0.25.3
+      '@esbuild/netbsd-arm64': 0.25.3
+      '@esbuild/netbsd-x64': 0.25.3
+      '@esbuild/openbsd-arm64': 0.25.3
+      '@esbuild/openbsd-x64': 0.25.3
+      '@esbuild/sunos-x64': 0.25.3
+      '@esbuild/win32-arm64': 0.25.3
+      '@esbuild/win32-ia32': 0.25.3
+      '@esbuild/win32-x64': 0.25.3
+
+  [email protected]: {}
+
+  [email protected]([email protected]([email protected])):
+    dependencies:
+      eslint: 9.25.1([email protected])
+
+  [email protected]([email protected]([email protected])):
+    dependencies:
+      eslint: 9.25.1([email protected])
+
+  [email protected]:
+    dependencies:
+      esrecurse: 4.3.0
+      estraverse: 5.3.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]([email protected]):
+    dependencies:
+      '@eslint-community/eslint-utils': 4.6.1([email protected]([email protected]))
+      '@eslint-community/regexpp': 4.12.1
+      '@eslint/config-array': 0.20.0
+      '@eslint/config-helpers': 0.2.1
+      '@eslint/core': 0.13.0
+      '@eslint/eslintrc': 3.3.1
+      '@eslint/js': 9.25.1
+      '@eslint/plugin-kit': 0.2.8
+      '@humanfs/node': 0.16.6
+      '@humanwhocodes/module-importer': 1.0.1
+      '@humanwhocodes/retry': 0.4.2
+      '@types/estree': 1.0.7
+      '@types/json-schema': 7.0.15
+      ajv: 6.12.6
+      chalk: 4.1.2
+      cross-spawn: 7.0.6
+      debug: 4.4.0
+      escape-string-regexp: 4.0.0
+      eslint-scope: 8.3.0
+      eslint-visitor-keys: 4.2.0
+      espree: 10.3.0
+      esquery: 1.6.0
+      esutils: 2.0.3
+      fast-deep-equal: 3.1.3
+      file-entry-cache: 8.0.0
+      find-up: 5.0.0
+      glob-parent: 6.0.2
+      ignore: 5.3.2
+      imurmurhash: 0.1.4
+      is-glob: 4.0.3
+      json-stable-stringify-without-jsonify: 1.0.1
+      lodash.merge: 4.6.2
+      minimatch: 3.1.2
+      natural-compare: 1.4.0
+      optionator: 0.9.4
+    optionalDependencies:
+      jiti: 2.4.2
+    transitivePeerDependencies:
+      - supports-color
+
+  [email protected]:
+    dependencies:
+      acorn: 8.14.1
+      acorn-jsx: 5.3.2([email protected])
+      eslint-visitor-keys: 4.2.0
+
+  [email protected]:
+    dependencies:
+      estraverse: 5.3.0
+
+  [email protected]:
+    dependencies:
+      estraverse: 5.3.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      '@nodelib/fs.stat': 2.0.5
+      '@nodelib/fs.walk': 1.2.8
+      glob-parent: 5.1.2
+      merge2: 1.4.1
+      micromatch: 4.0.8
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      reusify: 1.1.0
+
+  [email protected]:
+    dependencies:
+      format: 0.2.2
+
+  [email protected]([email protected]):
+    optionalDependencies:
+      picomatch: 4.0.2
+
+  [email protected]:
+    dependencies:
+      flat-cache: 4.0.1
+
+  [email protected]:
+    dependencies:
+      to-regex-range: 5.0.1
+
+  [email protected]:
+    dependencies:
+      locate-path: 6.0.0
+      path-exists: 4.0.0
+
+  [email protected]:
+    dependencies:
+      flatted: 3.3.3
+      keyv: 4.5.4
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      es-set-tostringtag: 2.1.0
+      mime-types: 2.1.35
+
+  [email protected]: {}
+
+  [email protected]([email protected]([email protected]))([email protected]):
+    dependencies:
+      motion-dom: 12.9.1
+      motion-utils: 12.8.3
+      tslib: 2.8.1
+    optionalDependencies:
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+
+  [email protected]:
+    optional: true
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-define-property: 1.0.1
+      es-errors: 1.3.0
+      es-object-atoms: 1.1.1
+      function-bind: 1.1.2
+      get-proto: 1.0.1
+      gopd: 1.2.0
+      has-symbols: 1.1.0
+      hasown: 2.0.2
+      math-intrinsics: 1.1.0
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      dunder-proto: 1.0.1
+      es-object-atoms: 1.1.1
+
+  [email protected]:
+    dependencies:
+      is-glob: 4.0.3
+
+  [email protected]:
+    dependencies:
+      is-glob: 4.0.3
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      has-symbols: 1.1.0
+
+  [email protected]:
+    dependencies:
+      function-bind: 1.1.2
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      '@types/hast': 2.3.10
+      comma-separated-tokens: 1.0.8
+      hast-util-parse-selector: 2.2.5
+      property-information: 5.6.0
+      space-separated-tokens: 1.1.5
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      void-elements: 3.1.0
+
+  [email protected]:
+    dependencies:
+      '@babel/runtime': 7.27.0
+
+  [email protected]:
+    dependencies:
+      cross-fetch: 4.0.0
+    transitivePeerDependencies:
+      - encoding
+
+  [email protected]([email protected]):
+    dependencies:
+      '@babel/runtime': 7.27.0
+    optionalDependencies:
+      typescript: 5.7.3
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      parent-module: 1.0.1
+      resolve-from: 4.0.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      is-alphabetical: 1.0.4
+      is-decimal: 1.0.4
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      is-extglob: 2.1.1
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      argparse: 2.0.1
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      json-buffer: 3.0.1
+
+  [email protected]:
+    dependencies:
+      prelude-ls: 1.2.1
+      type-check: 0.4.0
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    dependencies:
+      detect-libc: 2.0.4
+    optionalDependencies:
+      lightningcss-darwin-arm64: 1.29.2
+      lightningcss-darwin-x64: 1.29.2
+      lightningcss-freebsd-x64: 1.29.2
+      lightningcss-linux-arm-gnueabihf: 1.29.2
+      lightningcss-linux-arm64-gnu: 1.29.2
+      lightningcss-linux-arm64-musl: 1.29.2
+      lightningcss-linux-x64-gnu: 1.29.2
+      lightningcss-linux-x64-musl: 1.29.2
+      lightningcss-win32-arm64-msvc: 1.29.2
+      lightningcss-win32-x64-msvc: 1.29.2
+
+  [email protected]:
+    dependencies:
+      p-locate: 5.0.0
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      js-tokens: 4.0.0
+
+  [email protected]:
+    dependencies:
+      fault: 1.0.4
+      highlight.js: 10.7.3
+
+  [email protected]([email protected]):
+    dependencies:
+      react: 19.1.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      braces: 3.0.3
+      picomatch: 2.3.1
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      mime-db: 1.52.0
+
+  [email protected]:
+    dependencies:
+      brace-expansion: 1.1.11
+
+  [email protected]:
+    dependencies:
+      brace-expansion: 2.0.1
+
+  [email protected]:
+    dependencies:
+      motion-utils: 12.8.3
+
+  [email protected]: {}
+
+  [email protected]([email protected]([email protected]))([email protected]):
+    dependencies:
+      framer-motion: 12.9.1([email protected]([email protected]))([email protected])
+      tslib: 2.8.1
+    optionalDependencies:
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]([email protected]([email protected]))([email protected]):
+    dependencies:
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+
+  [email protected]:
+    dependencies:
+      whatwg-url: 5.0.0
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      deep-is: 0.1.4
+      fast-levenshtein: 2.0.6
+      levn: 0.4.1
+      prelude-ls: 1.2.1
+      type-check: 0.4.0
+      word-wrap: 1.2.5
+
+  [email protected]:
+    dependencies:
+      yocto-queue: 0.1.0
+
+  [email protected]:
+    dependencies:
+      p-limit: 3.1.0
+
+  [email protected]:
+    dependencies:
+      callsites: 3.1.0
+
+  [email protected]:
+    dependencies:
+      character-entities: 1.2.4
+      character-entities-legacy: 1.1.4
+      character-reference-invalid: 1.1.4
+      is-alphanumerical: 1.0.4
+      is-decimal: 1.0.4
+      is-hexadecimal: 1.0.4
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      nanoid: 3.3.11
+      picocolors: 1.1.1
+      source-map-js: 1.2.1
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      loose-envify: 1.4.0
+      object-assign: 4.1.1
+      react-is: 16.13.1
+
+  [email protected]:
+    dependencies:
+      xtend: 4.0.2
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]([email protected]):
+    dependencies:
+      react: 19.1.0
+      scheduler: 0.26.0
+
+  [email protected]([email protected]):
+    dependencies:
+      react: 19.1.0
+
+  [email protected]([email protected]([email protected]))([email protected]([email protected]))([email protected])([email protected]):
+    dependencies:
+      '@babel/runtime': 7.27.0
+      html-parse-stringify: 3.0.1
+      i18next: 25.0.1([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      react-dom: 19.1.0([email protected])
+      typescript: 5.7.3
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      react: 19.1.0
+      react-style-singleton: 2.2.3(@types/[email protected])([email protected])
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      react: 19.1.0
+      react-remove-scroll-bar: 2.3.8(@types/[email protected])([email protected])
+      react-style-singleton: 2.2.3(@types/[email protected])([email protected])
+      tslib: 2.8.1
+      use-callback-ref: 1.3.3(@types/[email protected])([email protected])
+      use-sidecar: 1.1.3(@types/[email protected])([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  [email protected]([email protected]([email protected]))([email protected]):
+    dependencies:
+      cookie: 1.0.2
+      react: 19.1.0
+      set-cookie-parser: 2.7.1
+      turbo-stream: 2.4.0
+    optionalDependencies:
+      react-dom: 19.1.0([email protected])
+
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      get-nonce: 1.0.1
+      react: 19.1.0
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  [email protected]([email protected]):
+    dependencies:
+      '@babel/runtime': 7.27.0
+      highlight.js: 10.7.3
+      highlightjs-vue: 1.0.0
+      lowlight: 1.20.0
+      prismjs: 1.30.0
+      react: 19.1.0
+      refractor: 3.6.0
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      hastscript: 6.0.0
+      parse-entities: 2.0.0
+      prismjs: 1.27.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      '@types/estree': 1.0.7
+    optionalDependencies:
+      '@rollup/rollup-android-arm-eabi': 4.40.0
+      '@rollup/rollup-android-arm64': 4.40.0
+      '@rollup/rollup-darwin-arm64': 4.40.0
+      '@rollup/rollup-darwin-x64': 4.40.0
+      '@rollup/rollup-freebsd-arm64': 4.40.0
+      '@rollup/rollup-freebsd-x64': 4.40.0
+      '@rollup/rollup-linux-arm-gnueabihf': 4.40.0
+      '@rollup/rollup-linux-arm-musleabihf': 4.40.0
+      '@rollup/rollup-linux-arm64-gnu': 4.40.0
+      '@rollup/rollup-linux-arm64-musl': 4.40.0
+      '@rollup/rollup-linux-loongarch64-gnu': 4.40.0
+      '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0
+      '@rollup/rollup-linux-riscv64-gnu': 4.40.0
+      '@rollup/rollup-linux-riscv64-musl': 4.40.0
+      '@rollup/rollup-linux-s390x-gnu': 4.40.0
+      '@rollup/rollup-linux-x64-gnu': 4.40.0
+      '@rollup/rollup-linux-x64-musl': 4.40.0
+      '@rollup/rollup-win32-arm64-msvc': 4.40.0
+      '@rollup/rollup-win32-ia32-msvc': 4.40.0
+      '@rollup/rollup-win32-x64-msvc': 4.40.0
+      fsevents: 2.3.3
+
+  [email protected]:
+    dependencies:
+      queue-microtask: 1.2.3
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      shebang-regex: 3.0.0
+
+  [email protected]: {}
+
+  [email protected]([email protected]([email protected]))([email protected]):
+    dependencies:
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      has-flag: 4.0.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      fdir: 6.4.4([email protected])
+      picomatch: 4.0.2
+
+  [email protected]:
+    dependencies:
+      is-number: 7.0.0
+
+  [email protected]: {}
+
+  [email protected]([email protected]):
+    dependencies:
+      typescript: 5.7.3
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      prelude-ls: 1.2.1
+
+  [email protected]([email protected]([email protected]))([email protected]):
+    dependencies:
+      '@typescript-eslint/eslint-plugin': 8.31.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected]))([email protected])
+      '@typescript-eslint/parser': 8.31.0([email protected]([email protected]))([email protected])
+      '@typescript-eslint/utils': 8.31.0([email protected]([email protected]))([email protected])
+      eslint: 9.25.1([email protected])
+      typescript: 5.7.3
+    transitivePeerDependencies:
+      - supports-color
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      punycode: 2.3.1
+
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      react: 19.1.0
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      detect-node-es: 1.1.0
+      react: 19.1.0
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  [email protected]([email protected]):
+    dependencies:
+      react: 19.1.0
+
+  [email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected]):
+    dependencies:
+      '@radix-ui/react-dialog': 1.1.11(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    transitivePeerDependencies:
+      - '@types/react'
+      - '@types/react-dom'
+
+  [email protected](@types/[email protected])([email protected])([email protected]):
+    dependencies:
+      esbuild: 0.25.3
+      fdir: 6.4.4([email protected])
+      picomatch: 4.0.2
+      postcss: 8.5.3
+      rollup: 4.40.0
+      tinyglobby: 0.2.13
+    optionalDependencies:
+      '@types/node': 22.14.1
+      fsevents: 2.3.3
+      jiti: 2.4.2
+      lightningcss: 1.29.2
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      tr46: 0.0.3
+      webidl-conversions: 3.0.1
+
+  [email protected]:
+    dependencies:
+      isexe: 2.0.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected](@types/[email protected])([email protected])([email protected]([email protected])):
+    optionalDependencies:
+      '@types/react': 19.1.2
+      react: 19.1.0
+      use-sync-external-store: 1.5.0([email protected])

+ 84 - 0
web/prompt.txt

@@ -0,0 +1,84 @@
+使用中文回答
+@src @openapi.txt 
+
+版本信息
+react-router: "^7.5.1"
+tanstack query: "@tanstack/[email protected]"
+
+完成 API Keys 页面 @page.tsx  要求如下:
+1. 使用 shadcn 的组件库
+2. 使用 tanstack query 进行数据请求
+3. 使用 react-hook-form 和 zod 进行表单验证
+4. 页面简洁大方,好看
+5. 按照项目原本的目录结构进行组织
+6. 代码不出错,可以运行
+
+api 代码风格 参考  @model.ts 
+validation 代码风格参考 @model.ts 
+hoook 代码风格参考 @hooks.ts  @hooks.ts 
+dialog 代码风格参考 @ModelDialog.tsx 
+
+详细要求如下:
+1. API Keys 页面的 key 对应后端的数据结构 token,完成 API Keys 页面的 crud 操作
+2. API Keys 页面的 table 展示字段 name key (API Key) accessed_at (时间戳,最近使用时间,当时间是负值时表明该 key 没被使用) request_count (请求次数) status (1 启用 2 禁用) ,操作有删除 启用 禁用
+3. table 参考 @ChannelTable.tsx  @hooks.ts  给的示例代码 用无限滚动实现 
+4. 页面代码要求现代化,视觉好看,符合 shadcn ui 风格
+5. 项目的国际化翻译文件 @translation.json  @translation.json 项目需要完成国际化
+
+重要要求:group 概念对用户隐藏,创建key 时,group 的值和 name 保持一致
+/api/token/${group}?auto_create_group=true
+描述信息:创建 token
+方法:post
+参数:
+path group: 组名
+body
+{
+    "name": "token1"
+}
+且 name 的命名只能是字母数字下划线的组合
+
+
+非常重要的一点
[email protected]  是项目的上下文信息,在设计的时候可以参考这个文件获取目前项目已有的信息,在完成本次变更后,所做的变更需要同步到 context.txt 上下文文件中,方便下次提问
+
+项目核心结构如下
+src/
+├── api/                   # API相关文件
+│   ├── index.ts           # 基础配置和拦截器
+│   ├── auth.ts            # 认证相关API
+│   └── services.ts        # 统一导出所有API服务
+├── feature/              # 功能模块(包含特定功能的组件和hooks)
+│   ├── auth/              # 认证相关功能
+│   │   ├── components/    # 认证相关组件
+│   │   └── hooks.ts       # 认证相关自定义hook,例如 tanstack query 数据管理的封装
+│   └── ... 
+├── store/                 # 全局状态管理
+│   ├── auth.ts
+│   └── index.ts
+├── validation/           # 表单验证逻辑
+│   ├── auth.ts
+│   └── ...
+├── components/            # 通用组件
+│   ├── table/             # 表格组件
+│   ├── layout/            # 布局组件
+│   │   ├── sidebar.tsx    # 侧边栏组件
+│   │   └── root-layout.tsx # 根布局组件
+│   ├── ui/                # 通用UI组件
+├── router/                # 路由配置
+│   ├── config.ts          # 路由配置
+│   └── ...
+├── utils/                 # 工具库
+│   ├── env.ts
+│   └── ...
+├── hooks/                 # 自定义hooks
+├── lib/                   # 工具库
+│   ├── utils.ts
+│   └── ...
+├── types/                 # 类型定义
+│   ├── auth.ts
+│   └── ...
+├── pages/                 # 页面组件
+│   ├── auth/              # 认证相关页面
+│   │   ├── login.tsx      # 登录页面
+│   └── ...
+

+ 173 - 0
web/public/locales/en/translation.json

@@ -0,0 +1,173 @@
+{
+  "sidebar": {
+    "monitor": "Dashboard",
+    "key": "API Keys",
+    "channel": "Channel",
+    "model": "Model",
+    "log": "Logs",
+    "doc": "Document",
+    "github": "GitHub",
+    "logout": "Logout"
+  },
+  "common": {
+    "noResult": "No Data",
+    "copied": "Copied to clipboard",
+    "copyFailed": "Copy failed"
+  },
+  "table": {
+    "selected": "{{selected}} of {{total}} row(s) selected",
+    "rowsPerPage": "Rows per page",
+    "pageInfo": "Page {{current}} of {{total}}",
+    "firstPage": "Go to first page",
+    "previousPage": "Go to previous page",
+    "nextPage": "Go to next page",
+    "lastPage": "Go to last page"
+  },
+  "auth": {
+    "login": {
+      "title": "Login to Admin Panel",
+      "description": "Please enter your access token to login",
+      "token": "Access Token",
+      "tokenPlaceholder": "Enter your API Token",
+      "loading": "Logging in...",
+      "submit": "Login",
+      "keepSafe": "Please keep your access token safe",
+      "allRightsReserved": "All rights reserved"
+    }
+  },
+  "token": {
+    "management": "Key Management",
+    "name": "Name",
+    "key": "API Key",
+    "lastUsed": "Last Used",
+    "requestCount": "Request Count",
+    "status": "Status",
+    "enabled": "Enabled",
+    "disabled": "Disabled",
+    "add": "Add API Key",
+    "edit": "Edit",
+    "delete": "Delete",
+    "enable": "Enable",
+    "disable": "Disable",
+    "refresh": "Refresh",
+    "copyKey": "Copy Key",
+    "never": "Never used",
+    "invalidDate": "Invalid date",
+    "noResults": "No API Keys found",
+    "dialog": {
+      "createTitle": "Create API Key",
+      "createDescription": "Create a new API access key",
+      "name": "Name",
+      "namePlaceholder": "Enter API Key name",
+      "create": "Create",
+      "submitting": "Submitting..."
+    },
+    "deleteDialog": {
+      "confirmTitle": "Delete API Key",
+      "confirmDescription": "Are you sure you want to delete this API Key? This action cannot be undone.",
+      "cancel": "Cancel",
+      "delete": "Delete",
+      "deleting": "Deleting..."
+    }
+  },
+  "model": {
+    "management": "Model Management",
+    "modelName": "Model Name",
+    "modelType": "Model Type",
+    "owner": "Owner",
+    "rpm": "RPM",
+    "add": "Add Model",
+    "edit": "Edit",
+    "delete": "Delete",
+    "refresh": "Refresh",
+    "noResults": "No models found",
+    "apiDetails": "API Detail",
+    "dialog": {
+      "createTitle": "Create Model",
+      "updateTitle": "Update Model",
+      "createDescription": "Add a new model to the system",
+      "updateDescription": "Update the model information",
+      "modelName": "Model Name",
+      "modelNamePlaceholder": "Enter model name",
+      "modelType": "Model Type",
+      "selectType": "Select model type",
+      "create": "Create",
+      "update": "Update",
+      "submitting": "Submitting..."
+    },
+    "deleteDialog": {
+      "confirmTitle": "Delete Model",
+      "confirmDescription": "Are you sure you want to delete this model? This action cannot be undone.",
+      "cancel": "Cancel",
+      "delete": "Delete",
+      "deleting": "Deleting..."
+    }
+  },
+  "apiDoc": {
+    "requestExample": "Request Example",
+    "voice": "required",
+    "voiceValues": "Available options:",
+    "responseFormatValues": "The audio output supports for the following formats:",
+    "responseExample": "Response Example"
+  },
+  "channel": {
+    "management": "Channel Management",
+    "id": "ID",
+    "name": "Name",
+    "type": "Provider",
+    "requestCount": "Request Count",
+    "status": "Status",
+    "enabled": "Enabled",
+    "disabled": "Disabled",
+    "add": "Add Channel",
+    "edit": "Edit",
+    "delete": "Delete",
+    "enable": "Enable",
+    "disable": "Disable",
+    "refresh": "Refresh",
+    "noResults": "No channels found",
+    "dialog": {
+      "createTitle": "Create Channel",
+      "updateTitle": "Update Channel",
+      "createDescription": "Add a new channel to the system",
+      "updateDescription": "Update the channel information",
+      "type": "Provider",
+      "selectType": "Select provider",
+      "name": "Custom Name",
+      "namePlaceholder": "Enter custom name",
+      "key": "API Key",
+      "keyPlaceholder": "Enter API key",
+      "baseUrl": "Base URL",
+      "baseUrlPlaceholder": "Enter base URL",
+      "models": "Models",
+      "selectModels": "Select models",
+      "createModel": "Create Model",
+      "modelMapping": "Model Mapping",
+      "mappedName": "Mapped name",
+      "create": "Create",
+      "update": "Update",
+      "submitting": "Submitting..."
+    },
+    "deleteDialog": {
+      "confirmTitle": "Delete Channel",
+      "confirmDescription": "Are you sure you want to delete this channel? This action cannot be undone.",
+      "cancel": "Cancel",
+      "delete": "Delete",
+      "deleting": "Deleting..."
+    }
+  },
+  "modeType": {
+    "0": "Unknown",
+    "1": "Chat",
+    "2": "Text",
+    "3": "Embed",
+    "4": "Moderate",
+    "5": "Image",
+    "6": "Edit",
+    "7": "TTS",
+    "8": "STT",
+    "9": "Audio",
+    "10": "Rerank",
+    "11": "PDF Parsing"
+  }
+}

+ 173 - 0
web/public/locales/zh/translation.json

@@ -0,0 +1,173 @@
+{
+  "sidebar": {
+    "monitor": "仪表盘",
+    "key": "API Keys",
+    "channel": "渠道",
+    "model": "模型",
+    "log": "日志",
+    "doc": "文档",
+    "github": "GitHub",
+    "logout": "登出"
+  },
+  "common": {
+    "noResult": "没有数据",
+    "copied": "已复制到剪贴板",
+    "copyFailed": "复制失败"
+  },
+  "table": {
+    "selected": "已选择 {{selected}} 行,共 {{total}} 行",
+    "rowsPerPage": "每页行数",
+    "pageInfo": "第 {{current}} 页,共 {{total}} 页",
+    "firstPage": "首页",
+    "previousPage": "上一页",
+    "nextPage": "下一页",
+    "lastPage": "尾页"
+  },
+  "auth": {
+    "login": {
+      "title": "登录到管理面板",
+      "description": "请输入您的访问令牌以登录",
+      "token": "访问令牌",
+      "tokenPlaceholder": "请输入您的API Token",
+      "loading": "登录中...",
+      "submit": "登录",
+      "keepSafe": "请妥善保管您的访问令牌",
+      "allRightsReserved": "保留所有权利"
+    }
+  },
+  "token": {
+    "management": "API Keys 管理",
+    "name": "名称",
+    "key": "API Key",
+    "lastUsed": "最近使用时间",
+    "requestCount": "请求次数",
+    "status": "状态",
+    "enabled": "已启用",
+    "disabled": "已禁用",
+    "add": "添加API Key",
+    "edit": "编辑",
+    "delete": "删除",
+    "enable": "启用",
+    "disable": "禁用",
+    "refresh": "刷新",
+    "copyKey": "复制密钥",
+    "never": "从未使用",
+    "invalidDate": "无效日期",
+    "noResults": "未找到API Keys",
+    "dialog": {
+      "createTitle": "创建API Key",
+      "createDescription": "创建新的API访问密钥",
+      "name": "名称",
+      "namePlaceholder": "输入API Key名称",
+      "create": "创建",
+      "submitting": "提交中..."
+    },
+    "deleteDialog": {
+      "confirmTitle": "删除API Key",
+      "confirmDescription": "确定要删除这个API Key吗?此操作无法撤消。",
+      "cancel": "取消",
+      "delete": "删除",
+      "deleting": "删除中..."
+    }
+  },
+  "model": {
+    "management": "模型管理",
+    "modelName": "模型名称",
+    "modelType": "模型类型",
+    "owner": "所有者",
+    "rpm": "每分钟请求数",
+    "add": "添加模型",
+    "edit": "编辑",
+    "delete": "删除",
+    "refresh": "刷新",
+    "noResults": "未找到模型",
+    "apiDetails": "API 详情",
+    "dialog": {
+      "createTitle": "创建模型",
+      "updateTitle": "更新模型",
+      "createDescription": "向系统添加新模型",
+      "updateDescription": "更新模型信息",
+      "modelName": "模型名称",
+      "modelNamePlaceholder": "输入模型名称",
+      "modelType": "模型类型",
+      "selectType": "选择模型类型",
+      "create": "创建",
+      "update": "更新",
+      "submitting": "提交中..."
+    },
+    "deleteDialog": {
+      "confirmTitle": "删除模型",
+      "confirmDescription": "确定要删除这个模型吗?此操作无法撤消。",
+      "cancel": "取消",
+      "delete": "删除",
+      "deleting": "删除中..."
+    }
+  },
+  "apiDoc": {
+    "requestExample": "请求示例",
+    "voice": "必填",
+    "voiceValues": "可用的选项有:",
+    "responseFormatValues": "音频输出支持的格式有:",
+    "responseExample": "响应示例"
+  },
+  "channel": {
+    "management": "渠道管理",
+    "id": "ID",
+    "name": "名称",
+    "type": "厂商",
+    "requestCount": "调用次数",
+    "status": "状态",
+    "enabled": "已启用",
+    "disabled": "已禁用",
+    "add": "添加渠道",
+    "edit": "编辑",
+    "delete": "删除",
+    "enable": "启用",
+    "disable": "禁用",
+    "refresh": "刷新",
+    "noResults": "未找到渠道",
+    "dialog": {
+      "createTitle": "创建渠道",
+      "updateTitle": "更新渠道",
+      "createDescription": "向系统添加新渠道",
+      "updateDescription": "更新渠道信息",
+      "type": "厂商",
+      "selectType": "选择厂商",
+      "name": "自定义名称",
+      "namePlaceholder": "输入自定义名称",
+      "key": "密钥",
+      "keyPlaceholder": "输入密钥",
+      "baseUrl": "代理地址",
+      "baseUrlPlaceholder": "输入代理地址",
+      "models": "模型",
+      "selectModels": "选择模型",
+      "createModel": "创建模型",
+      "modelMapping": "模型映射",
+      "mappedName": "映射名称",
+      "create": "创建",
+      "update": "更新",
+      "submitting": "提交中..."
+    },
+    "deleteDialog": {
+      "confirmTitle": "删除渠道",
+      "confirmDescription": "确定要删除这个渠道吗?此操作无法撤消。",
+      "cancel": "取消",
+      "delete": "删除",
+      "deleting": "删除中..."
+    }
+  },
+  "modeType": {
+    "0": "未知",
+    "1": "聊天补全",
+    "2": "文本补全",
+    "3": "文本嵌入",
+    "4": "内容审核",
+    "5": "图像生成",
+    "6": "文本编辑",
+    "7": "语音合成",
+    "8": "语音转录",
+    "9": "音频翻译",
+    "10": "重排序",
+    "11": "pdf解析"
+  }
+}

+ 9 - 0
web/public/logo.svg

@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
+  <path d="M5.22391 14.8305V12.8644H10.1392C9.90979 12.5859 9.72564 12.2828 9.5867 11.9551C9.4471 11.6274 9.33635 11.2751 9.25443 10.8983H3.25781V8.93221H9.25443C9.33635 8.55537 9.4471 8.20311 9.5867 7.87543C9.72564 7.54774 9.90979 7.24464 10.1392 6.9661H5.22391V5H14.0714C15.4313 5 16.5906 5.47907 17.5494 6.43722C18.5076 7.39603 18.9866 8.55537 18.9866 9.91526C18.9866 11.2751 18.5076 12.4342 17.5494 13.3923C16.5906 14.3511 15.4313 14.8305 14.0714 14.8305H5.22391ZM1.29171 14.8305V12.8644H4.24086V14.8305H1.29171Z" fill="url(#paint0_linear_327_1801)"/>
+  <defs>
+    <linearGradient id="paint0_linear_327_1801" x1="4.14072" y1="14.8305" x2="11.4508" y2="2.02067" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#1A63D4"/>
+      <stop offset="1" stop-color="#A9B1F6"/>
+    </linearGradient>
+  </defs>
+</svg>

+ 12 - 0
web/src/App.tsx

@@ -0,0 +1,12 @@
+import { AppRouter } from "@/routes"
+import { ThemeProvider } from "./handler/ThemeProvider"
+
+function App() {
+  return (
+    <ThemeProvider defaultTheme="system" storageKey="aiproxy-theme">
+      <AppRouter />
+    </ThemeProvider>
+  )
+}
+
+export default App

+ 21 - 0
web/src/api/auth.ts

@@ -0,0 +1,21 @@
+import { get } from './index'
+import { AxiosRequestConfig } from 'axios'
+import { ChannelTypeMeta } from '@/types/channel'
+
+// Auth API endpoints
+export const authApi = {
+
+    // Get channel type metas
+    getChannelTypeMetas: (token?: string): Promise<ChannelTypeMeta[]> => {
+        const config: AxiosRequestConfig = {}
+
+        if (token) {
+            config.headers = {
+                Authorization: `${token}`
+            }
+        }
+
+        return get<ChannelTypeMeta[]>('/channels/type_metas', config)
+    },
+
+} 

+ 46 - 0
web/src/api/channel.ts

@@ -0,0 +1,46 @@
+// src/api/channel.ts
+import { get, post, put, del } from './index'
+import {
+    ChannelTypeMetaMap,
+    ChannelsResponse,
+    ChannelCreateRequest,
+    ChannelUpdateRequest,
+    ChannelStatusRequest
+} from '@/types/channel'
+
+export const channelApi = {
+    getTypeMetas: async (): Promise<ChannelTypeMetaMap> => {
+        const response = await get<ChannelTypeMetaMap>('channels/type_metas')
+        return response
+    },
+
+    getChannels: async (page: number, perPage: number): Promise<ChannelsResponse> => {
+        const response = await get<ChannelsResponse>('channels/search', {
+            params: {
+                p: page,
+                per_page: perPage
+            }
+        })
+        return response
+    },
+
+    createChannel: async (data: ChannelCreateRequest): Promise<void> => {
+        await post('channel/', data)
+        return
+    },
+
+    updateChannel: async (id: number, data: ChannelUpdateRequest): Promise<void> => {
+        await put(`channel/${id}`, data)
+        return
+    },
+
+    deleteChannel: async (id: number): Promise<void> => {
+        await del(`channel/${id}`)
+        return
+    },
+
+    updateChannelStatus: async (id: number, status: ChannelStatusRequest): Promise<void> => {
+        await post(`channel/${id}/status`, status)
+        return
+    }
+}

+ 267 - 0
web/src/api/index.ts

@@ -0,0 +1,267 @@
+import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
+import { useAuthStore } from '@/store/auth'
+import { ENV } from '@/utils/env'
+
+// ======================
+// API 响应类型定义
+// ======================
+
+/**
+ * 通用API响应接口
+ */
+export interface APIResponse<T = unknown> {
+    message: string
+    success: boolean
+    data?: T
+}
+
+/**
+ * Custom API error class
+ */
+export class ApiError extends Error {
+    code: number
+
+    constructor(message: string, code: number) {
+        super(message)
+        this.name = 'ApiError'
+        this.code = code
+    }
+}
+
+// 定义基础请求和响应接口
+export type ApiRequestData = Record<string, unknown>
+export type ApiResponseData = Record<string, unknown>
+
+// ======================
+// API 客户端配置
+// ======================
+
+const API_BASE_URL = ENV.API_BASE_URL || '/api'
+const API_TIMEOUT = Number(ENV.API_TIMEOUT || 10000)
+
+// 创建axios实例
+const apiClient = axios.create({
+    baseURL: API_BASE_URL,
+    timeout: API_TIMEOUT,
+    headers: {
+        'Content-Type': 'application/json',
+    },
+})
+
+// 请求拦截器
+apiClient.interceptors.request.use(
+    (config) => {
+        const token = useAuthStore.getState().token
+
+        if (token && config.headers) {
+            config.headers.Authorization = `${token}`
+        }
+
+        return config
+    },
+    (error) => {
+        return Promise.reject(error)
+    }
+)
+
+// 响应拦截器 - 统一处理错误和响应格式
+apiClient.interceptors.response.use(
+    (response) => {
+        // Check if response format matches API standard format
+        const data = response.data as APIResponse
+
+        // If response has success field and it's false, consider it a business logic error
+        if (data && data.success === false) {
+            throw new ApiError(
+                data.message || 'Request failed',
+                response.status
+            )
+        }
+
+        return response
+    },
+    (error: AxiosError<APIResponse>) => {
+        const status = error.response?.status
+        const errorData = error.response?.data
+
+        // Handle 401 unauthorized error
+        if (status === 401) {
+            useAuthStore.getState().logout()
+            window.location.href = '/login'
+        }
+
+        // Convert to custom API error object
+        if (errorData) {
+            throw new ApiError(
+                errorData.message || 'Request failed',
+                status || 500
+            )
+        } else {
+            // Network error or other non-standard error
+            throw new ApiError(
+                error.message || 'Network request failed',
+                status || 500
+            )
+        }
+    }
+)
+
+// 增加请求重试功能的封装方法
+const withRetry = async <T>(
+    requestFn: () => Promise<T>,
+    maxRetries = 3,
+    delay = 1000
+): Promise<T> => {
+    let retries = 0
+
+    while (retries < maxRetries) {
+        try {
+            return await requestFn()
+        } catch (error) {
+            // 使用类型断言而不是 any
+            const err = error as Error
+            if (error instanceof ApiError && error.code >= 500 && retries < maxRetries - 1) {
+                // 只有服务器错误才重试,并且不是最后一次尝试
+                retries++
+                await new Promise(resolve => setTimeout(resolve, delay * retries))
+                continue
+            }
+            throw err
+        }
+    }
+
+    throw new Error('Max retries reached')
+}
+
+// ======================
+// API 请求方法
+// ======================
+
+/**
+ * GET请求
+ * @param url 请求URL
+ * @param config 请求配置
+ * @returns 响应数据
+ */
+export const get = <T = ApiResponseData>(url: string, config?: AxiosRequestConfig): Promise<T> => {
+    return apiClient.get<APIResponse<T>>(url, config)
+        .then((response: AxiosResponse<APIResponse<T>>) => {
+            return response.data.data as T
+        })
+}
+
+/**
+ * POST请求
+ * @param url 请求URL
+ * @param data 请求数据
+ * @param config 请求配置
+ * @returns 响应数据
+ */
+export const post = <T = ApiResponseData>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> => {
+    return apiClient.post<APIResponse<T>>(url, data, config)
+        .then((response: AxiosResponse<APIResponse<T>>) => {
+            return response.data.data as T
+        })
+}
+
+/**
+ * PUT请求
+ * @param url 请求URL
+ * @param data 请求数据
+ * @param config 请求配置
+ * @returns 响应数据
+ */
+export const put = <T = ApiResponseData>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> => {
+    return apiClient.put<APIResponse<T>>(url, data, config)
+        .then((response: AxiosResponse<APIResponse<T>>) => {
+            return response.data.data as T
+        })
+}
+
+/**
+ * PATCH请求
+ * @param url 请求URL
+ * @param data 请求数据
+ * @param config 请求配置
+ * @returns 响应数据
+ */
+export const patch = <T = ApiResponseData>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> => {
+    return apiClient.patch<APIResponse<T>>(url, data, config)
+        .then((response: AxiosResponse<APIResponse<T>>) => {
+            return response.data.data as T
+        })
+}
+
+/**
+ * DELETE请求
+ * @param url 请求URL
+ * @param config 请求配置
+ * @returns 响应数据
+ */
+export const del = <T = ApiResponseData>(url: string, config?: AxiosRequestConfig): Promise<T> => {
+    return apiClient.delete<APIResponse<T>>(url, config)
+        .then((response: AxiosResponse<APIResponse<T>>) => {
+            return response.data.data as T
+        })
+}
+
+/**
+ * 带重试功能的GET请求
+ * @param url 请求URL
+ * @param config 请求配置
+ * @param retries 重试次数
+ * @returns 响应数据
+ */
+export const getWithRetry = <T = ApiResponseData>(url: string, config?: AxiosRequestConfig, retries = 3): Promise<T> => {
+    return withRetry(() => get<T>(url, config), retries)
+}
+
+/**
+ * 带重试功能的POST请求
+ * @param url 请求URL
+ * @param data 请求数据
+ * @param config 请求配置
+ * @param retries 重试次数
+ * @returns 响应数据
+ */
+export const postWithRetry = <T = ApiResponseData>(url: string, data?: unknown, config?: AxiosRequestConfig, retries = 3): Promise<T> => {
+    return withRetry(() => post<T>(url, data, config), retries)
+}
+
+/**
+ * 带重试功能的PUT请求
+ * @param url 请求URL
+ * @param data 请求数据
+ * @param config 请求配置
+ * @param retries 重试次数
+ * @returns 响应数据
+ */
+export const putWithRetry = <T = ApiResponseData>(url: string, data?: unknown, config?: AxiosRequestConfig, retries = 3): Promise<T> => {
+    return withRetry(() => put<T>(url, data, config), retries)
+}
+
+/**
+ * 带重试功能的DELETE请求
+ * @param url 请求URL
+ * @param config 请求配置
+ * @param retries 重试次数
+ * @returns 响应数据
+ */
+export const delWithRetry = <T = ApiResponseData>(url: string, config?: AxiosRequestConfig, retries = 3): Promise<T> => {
+    return withRetry(() => del<T>(url, config), retries)
+}
+
+/**
+ * 带重试功能的PATCH请求
+ * @param url 请求URL
+ * @param data 请求数据
+ * @param config 请求配置
+ * @param retries 重试次数
+ * @returns 响应数据
+ */
+export const patchWithRetry = <T = ApiResponseData>(url: string, data?: unknown, config?: AxiosRequestConfig, retries = 3): Promise<T> => {
+    return withRetry(() => patch<T>(url, data, config), retries)
+}
+
+
+export default apiClient

+ 26 - 0
web/src/api/model.ts

@@ -0,0 +1,26 @@
+// src/api/model.ts
+import { get, post, del } from './index'
+import { ModelConfig, ModelCreateRequest } from '@/types/model'
+
+
+export const modelApi = {
+    getModels: async (): Promise<ModelConfig[]> => {
+        const response = await get<ModelConfig[]>('model_configs/all')
+        return response
+    },
+
+    getModel: async (model: string): Promise<ModelConfig> => {
+        const response = await get<ModelConfig>(`model_config/${model}`)
+        return response
+    },
+
+    createModel: async (data: ModelCreateRequest): Promise<void> => {
+        await post('model_config/', data)
+        return
+    },
+
+    deleteModel: async (model: string): Promise<void> => {
+        await del(`model_config/${model}`)
+        return
+    }
+}

+ 4 - 0
web/src/api/services.ts

@@ -0,0 +1,4 @@
+export { authApi } from './auth'
+export { channelApi } from './channel'
+export { modelApi } from './model'
+export { tokenApi } from './token'

+ 33 - 0
web/src/api/token.ts

@@ -0,0 +1,33 @@
+// src/api/token.ts
+import { get, post, del } from './index'
+import { TokensResponse, Token,  TokenStatusRequest } from '@/types/token'
+
+export const tokenApi = {
+    getTokens: async (page: number, perPage: number): Promise<TokensResponse> => {
+        const response = await get<TokensResponse>('tokens/search', {
+            params: {
+                p: page,
+                per_page: perPage
+            }
+        })
+        return response
+    },
+
+    createToken: async (name: string): Promise<Token> => {
+        // 重要:group的值与name保持一致,创建时使用auto_create_group=true
+        const response = await post<Token>(`token/${name}?auto_create_group=true`, {
+            name
+        })
+        return response
+    },
+
+    deleteToken: async (id: number): Promise<void> => {
+        await del(`tokens/${id}`)
+        return
+    },
+
+    updateTokenStatus: async (id: number, status: TokenStatusRequest): Promise<void> => {
+        await post(`tokens/${id}/status`, status)
+        return
+    }
+}

File diff suppressed because it is too large
+ 0 - 0
web/src/assets/react.svg


+ 64 - 0
web/src/components/common/LanguageSelector.tsx

@@ -0,0 +1,64 @@
+import { useEffect, useState } from "react"
+import { useTranslation } from "react-i18next"
+import { Globe } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
+import { AnimatedIcon } from "../ui/animation/components/animated-icon"
+import { cn } from "@/lib/utils"
+
+interface LanguageSelectorProps {
+    variant?: "default" | "minimal"
+}
+
+export function LanguageSelector({ variant = "default" }: LanguageSelectorProps) {
+    const { i18n } = useTranslation()
+    const [language, setLanguage] = useState(i18n.language || 'zh')
+
+    // 初始化时从本地存储获取语言设置
+    useEffect(() => {
+        const savedLanguage = localStorage.getItem('i18nextLng')
+        if (savedLanguage && savedLanguage !== language) {
+            setLanguage(savedLanguage)
+            i18n.changeLanguage(savedLanguage)
+        }
+    }, []) // 只在组件挂载时执行一次
+
+    const toggleLanguage = () => {
+        const newLanguage = language === 'zh' ? 'en' : 'zh'
+        setLanguage(newLanguage)
+        i18n.changeLanguage(newLanguage)
+        localStorage.setItem('i18nextLng', newLanguage)
+    }
+
+    const displayText = language === 'zh' ? '中' : 'En'
+
+    const isMinimal = variant === "minimal"
+
+    return (
+        <TooltipProvider delayDuration={300}>
+            <Tooltip>
+                <TooltipTrigger asChild>
+                    <Button
+                        variant={isMinimal ? "outline" : "ghost"}
+                        size="icon"
+                        onClick={toggleLanguage}
+                        className={cn(
+                            "h-10 w-16 rounded-md",
+                            isMinimal 
+                                ? "bg-white/80 dark:bg-gray-800/80 hover:bg-white dark:hover:bg-gray-800 border border-gray-200 dark:border-gray-700 backdrop-blur-sm"
+                                : "bg-primary/10 text-primary hover:bg-primary/20"
+                        )}
+                    >
+                        <AnimatedIcon animationVariant="pulse" className="flex items-center justify-center">
+                            <span className="text-sm font-medium">{displayText}</span>
+                            <Globe className="h-4 w-4 ml-1" />
+                        </AnimatedIcon>
+                    </Button>
+                </TooltipTrigger>
+                <TooltipContent side={isMinimal ? "left" : "bottom"}>
+                    {language === 'zh' ? 'Switch to English' : '切换到中文'}
+                </TooltipContent>
+            </Tooltip>
+        </TooltipProvider>
+    )
+}

+ 98 - 0
web/src/components/common/LoadingFallBack.tsx

@@ -0,0 +1,98 @@
+import { useState, useEffect } from "react"
+import { motion } from "motion/react"
+
+// Loading component with translations
+export const LoadingFallback = () => {
+    const [progress, setProgress] = useState(0)
+
+    // Simulate loading progress
+    useEffect(() => {
+        const timer = setInterval(() => {
+            setProgress((prevProgress) => {
+                // Slow down as it approaches 100%
+                // 使用Math.floor确保结果是整数
+                const increment = Math.floor(Math.max(1, 10 * (1 - prevProgress / 100)))
+                const newProgress = Math.min(99, prevProgress + increment)
+                // 确保最终结果也是整数
+                return Math.floor(newProgress)
+            })
+        }, 200)
+
+        return () => {
+            clearInterval(timer)
+        }
+    }, [])
+
+    // Define the gradient for reuse
+    const purpleGradient = `linear-gradient(135deg, 
+    #6A6DE6 0%, 
+    #7B7FF6 50%, 
+    #8A8DF7 100%)`
+
+    return (
+        <div className="fixed inset-0 flex flex-col items-center justify-center z-[9999]">
+            <div
+                className="absolute inset-0"
+                style={{
+                    background: purpleGradient,
+                    backgroundSize: "200% 200%",
+                }}
+            >
+                <div className="absolute inset-0 overflow-hidden">
+                    {/* 粒子效果 */}
+                    {Array.from({ length: 25 }).map((_, i) => (
+                        <div
+                            key={i}
+                            className="absolute rounded-full bg-white/10 sidebar-particle"
+                            style={{
+                                width: `${Math.random() * 6 + 2}px`,
+                                height: `${Math.random() * 6 + 2}px`,
+                                top: `${Math.random() * 100}%`,
+                                left: `${Math.random() * 100}%`,
+                                animationDelay: `${Math.random() * 5}s`,
+                            }}
+                        />
+                    ))}
+                    
+                    {/* 光晕效果 */}
+                    <div className="absolute w-[80%] h-[80%] top-[10%] left-[10%] bg-white/10 rounded-full blur-3xl animate-float"></div>
+                    <div className="absolute w-[40%] h-[40%] top-[5%] right-[15%] bg-white/15 rounded-full blur-3xl animate-float-reverse"></div>
+                    <div className="absolute w-[50%] h-[50%] bottom-[5%] left-[15%] bg-white/10 rounded-full blur-3xl animate-pulse-glow"></div>
+                </div>
+            </div>
+
+            <div className="relative z-10 flex flex-col items-center space-y-8">
+                <motion.div
+                    initial={{ opacity: 0, y: -20 }}
+                    animate={{ opacity: 1, y: 0 }}
+                    transition={{ duration: 0.5 }}
+                    className="text-white text-2xl font-medium"
+                >
+                    Loading...
+                </motion.div>
+
+                <div className="w-64 h-3 bg-white/20 rounded-full overflow-hidden backdrop-blur-sm">
+                    <motion.div
+                        className="h-full rounded-full"
+                        style={{
+                            background: "linear-gradient(90deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.7) 100%)",
+                            width: `${progress}%`,
+                            boxShadow: "0 0 15px rgba(255, 255, 255, 0.5)",
+                        }}
+                        initial={{ width: "0%" }}
+                        animate={{ width: `${progress}%` }}
+                        transition={{ duration: 0.3 }}
+                    />
+                </div>
+
+                <motion.div
+                    className="text-white/90 text-sm font-medium"
+                    animate={{ opacity: [0.7, 1, 0.7] }}
+                    transition={{ duration: 2, repeat: Number.POSITIVE_INFINITY }}
+                >
+                    {progress}% Complete
+                </motion.div>
+            </div>
+        </div>
+    )
+}

+ 31 - 0
web/src/components/common/ThemeToggle.tsx

@@ -0,0 +1,31 @@
+import { Moon, Sun } from "lucide-react"
+import { useTheme } from "@/handler/ThemeContext"
+
+import { Switch } from "@/components/ui/switch"
+
+export function ThemeToggle() {
+    const { theme, setTheme } = useTheme()
+
+    const toggleTheme = () => {
+        setTheme(theme === "light" ? "dark" : "light")
+    }
+
+    return (
+        <div className="flex items-center space-x-2 transition-all duration-700 ease-[cubic-bezier(0.34,1.56,0.64,1)]">
+            <Sun
+                className={`h-[1.2rem] w-[1.2rem] transition-all duration-700 ease-[cubic-bezier(0.34,1.56,0.64,1)] ${theme === "dark" ? "text-[#A1A1AA] scale-75 rotate-12" : "text-foreground scale-100 rotate-0"
+                    }`}
+            />
+            <Switch
+                checked={theme === "dark"}
+                onCheckedChange={toggleTheme}
+                aria-label="Toggle theme"
+                className="transition-all duration-700 ease-[cubic-bezier(0.34,1.56,0.64,1)] hover:scale-110"
+            />
+            <Moon
+                className={`h-[1.2rem] w-[1.2rem] transition-all duration-700 ease-[cubic-bezier(0.34,1.56,0.64,1)] ${theme === "light" ? "text-[#A1A1AA] scale-75 rotate-12" : "text-foreground scale-100 rotate-0"
+                    }`}
+            />
+        </div>
+    )
+}

+ 63 - 0
web/src/components/common/error/errorConfig.tsx

@@ -0,0 +1,63 @@
+import {
+    AlertCircle,
+    WifiOff,
+    Clock,
+    ShieldAlert,
+    ServerOff,
+    Bug,
+    Ban,
+    FileWarning
+} from "lucide-react"
+import { ErrorType, ErrorTypeValue } from './errorTypes'
+import { ReactElement } from 'react'
+
+// 错误配置类型
+export interface ErrorConfig {
+    icon: ReactElement
+    titleKey: string
+    descriptionKey: string
+}
+
+// 错误配置映射
+export const errorConfigs: Record<ErrorTypeValue, ErrorConfig> = {
+    [ErrorType.NETWORK]: {
+        icon: <WifiOff className="h-5 w-5" />,
+        titleKey: 'error.network.title',
+        descriptionKey: 'error.network.description'
+    },
+    [ErrorType.TIMEOUT]: {
+        icon: <Clock className="h-5 w-5" />,
+        titleKey: 'error.timeout.title',
+        descriptionKey: 'error.timeout.description'
+    },
+    [ErrorType.FORBIDDEN]: {
+        icon: <Ban className="h-5 w-5" />,
+        titleKey: 'error.forbidden.title',
+        descriptionKey: 'error.forbidden.description'
+    },
+    [ErrorType.UNAUTHORIZED]: {
+        icon: <ShieldAlert className="h-5 w-5" />,
+        titleKey: 'error.unauthorized.title',
+        descriptionKey: 'error.unauthorized.description'
+    },
+    [ErrorType.SERVER]: {
+        icon: <ServerOff className="h-5 w-5" />,
+        titleKey: 'error.server.title',
+        descriptionKey: 'error.server.description'
+    },
+    [ErrorType.CLIENT]: {
+        icon: <FileWarning className="h-5 w-5" />,
+        titleKey: 'error.client.title',
+        descriptionKey: 'error.client.description'
+    },
+    [ErrorType.VALIDATION]: {
+        icon: <Bug className="h-5 w-5" />,
+        titleKey: 'error.validation.title',
+        descriptionKey: 'error.validation.description'
+    },
+    [ErrorType.UNKNOWN]: {
+        icon: <AlertCircle className="h-5 w-5" />,
+        titleKey: 'error.unknown.title',
+        descriptionKey: 'error.unknown.description'
+    }
+}

+ 168 - 0
web/src/components/common/error/errorDisplay.tsx

@@ -0,0 +1,168 @@
+import { useState } from "react"
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
+import { HelpCircle, RefreshCw, ChevronDown, ChevronUp } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import {
+    Collapsible,
+    CollapsibleContent,
+    CollapsibleTrigger
+} from "@/components/ui/collapsible"
+import { cn } from "@/lib/utils"
+import { ApiError } from "@/api/index"
+import { getErrorType, ErrorType } from "./errorTypes"
+import { errorConfigs } from "./errorConfig"
+import { useTranslation } from "react-i18next"
+import { Badge } from "@/components/ui/badge"
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
+
+interface AdvancedErrorDisplayProps {
+    /** 错误对象 */
+    error: unknown
+    /** 重试回调函数 */
+    onRetry?: () => void
+    /** 额外的CSS类名 */
+    className?: string
+    /** 是否使用卡片样式 (更现代化的UI) */
+    useCardStyle?: boolean
+}
+
+/**
+ * 高级错误展示组件
+ */
+export const AdvancedErrorDisplay = ({
+    error,
+    onRetry,
+    className,
+    useCardStyle = true
+}: AdvancedErrorDisplayProps) => {
+    const { t } = useTranslation()
+    const [showDetails, setShowDetails] = useState(false)
+
+    // 处理错误信息
+    const isApiError = error instanceof Error && error.name === 'ApiError'
+    const errorMessage = error instanceof Error ? error.message : t('error.loading', '加载数据时发生错误')
+    const apiError = isApiError ? (error as ApiError) : undefined
+    const errorCode = apiError?.code || (error instanceof Error && 'status' in error ? (error as unknown as { status: number }).status : undefined)
+    const errorType = getErrorType(error)
+
+    // 获取错误配置
+    const config = errorConfigs[errorType]
+    const title = t(config.titleKey, config.titleKey)
+    const description = t(config.descriptionKey, config.descriptionKey)
+    const finalDescription = errorType === ErrorType.CLIENT || errorType === ErrorType.VALIDATION
+        ? errorMessage || description
+        : description
+
+
+
+    if (useCardStyle) {
+        return (
+            <Card className={cn("mx-auto my-6 max-w-xl border-red-200 shadow-md", className)}>
+                <CardHeader className="pb-2">
+                    <div className="flex items-center gap-3">
+                        <div className="bg-red-50 p-2 rounded-full">
+                            {config.icon}
+                        </div>
+                        <div className="flex-1">
+                            <CardTitle className="text-lg text-red-700">{title}</CardTitle>
+                            {errorCode && (
+                                <Badge variant="outline" className="mt-1 text-xs border-red-200 text-red-500">
+                                    {t('error.code', '错误代码')}: {errorCode}
+                                </Badge>
+                            )}
+                        </div>
+                    </div>
+                </CardHeader>
+                <CardContent className="pb-2">
+                    <CardDescription className="text-sm text-red-600 dark:text-red-400">
+                        {finalDescription}
+                    </CardDescription>
+
+                    <Collapsible open={showDetails} onOpenChange={setShowDetails} className="mt-4">
+                        <div className="flex items-center justify-between">
+                            <CollapsibleTrigger asChild>
+                                <Button
+                                    variant="ghost"
+                                    size="sm"
+                                    className="px-2 text-xs flex items-center gap-1 text-red-600 hover:bg-red-50"
+                                >
+                                    {showDetails ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
+                                    {showDetails ? t('error.hideDetails', '隐藏详情') : t('error.viewDetails', '查看详情')}
+                                </Button>
+                            </CollapsibleTrigger>
+                        </div>
+
+                        <CollapsibleContent>
+                            <div className="mt-3 p-3 bg-red-50 rounded-md text-xs font-mono overflow-auto">
+                                <p className="break-words overflow-hidden max-h-[100px]">
+                                    {t('error.message', '错误信息')}: {String(errorMessage)}
+                                </p>
+                                {errorCode && <p className="mt-1">{t('error.code', '错误代码')}: {errorCode}</p>}
+                            </div>
+                        </CollapsibleContent>
+                    </Collapsible>
+                </CardContent>
+                {onRetry && errorType !== ErrorType.UNAUTHORIZED && (
+                    <CardFooter className="pt-2">
+                        <Button
+                            variant="outline"
+                            size="sm"
+                            onClick={onRetry}
+                            className="flex items-center gap-2 text-xs border-red-300 text-red-600 hover:bg-red-50 mt-2"
+                        >
+                            <RefreshCw className="h-3 w-3" />
+                            {t('error.retry', '重试')}
+                        </Button>
+                    </CardFooter>
+                )}
+            </Card>
+        )
+    }
+
+    // 经典 Alert 样式
+    return (
+        <Alert variant="destructive" className={cn("mx-auto my-6 max-w-xl", className)}>
+            {config.icon}
+            <AlertTitle className="font-medium mt-0">{title}</AlertTitle>
+            <AlertDescription className="mt-2 flex flex-col gap-4">
+                <p className="text-sm">{finalDescription}</p>
+
+                <Collapsible open={showDetails} onOpenChange={setShowDetails}>
+                    <div className="flex items-center justify-between">
+                        <CollapsibleTrigger asChild>
+                            <Button
+                                variant="ghost"
+                                size="sm"
+                                className="px-2 text-xs flex items-center gap-1 hover:bg-red-50"
+                            >
+                                <HelpCircle className="h-3 w-3" />
+                                {showDetails ? t('error.hideDetails', '隐藏详情') : t('error.viewDetails', '查看详情')}
+                            </Button>
+                        </CollapsibleTrigger>
+
+                        {onRetry && errorType !== ErrorType.UNAUTHORIZED && (
+                            <Button
+                                variant="outline"
+                                size="sm"
+                                onClick={onRetry}
+                                className="flex items-center gap-2 text-xs border-red-300 hover:bg-red-50 hover:text-red-600"
+                            >
+                                <RefreshCw className="h-3 w-3" />
+                                {t('error.retry', '重试')}
+                            </Button>
+                        )}
+                    </div>
+
+                    <CollapsibleContent>
+                        <div className="mt-3 p-3 bg-red-50 rounded text-xs font-mono overflow-auto">
+                            <p className="break-words overflow-hidden max-h-[100px]">
+                                {t('error.message', '错误信息')}: {String(errorMessage)}
+                            </p>
+                            {errorCode && <p className="mt-1 text-red-600">{t('error.code', '错误代码')}: {errorCode}</p>}
+                        </div>
+                    </CollapsibleContent>
+                </Collapsible>
+            </AlertDescription>
+        </Alert>
+    )
+}

+ 58 - 0
web/src/components/common/error/errorTypes.ts

@@ -0,0 +1,58 @@
+import { ApiError } from '@/api/index'
+
+// 错误类型枚举
+export const ErrorType = {
+    NETWORK: 'network',
+    TIMEOUT: 'timeout',
+    FORBIDDEN: 'forbidden',
+    UNAUTHORIZED: 'unauthorized',
+    SERVER: 'server',
+    CLIENT: 'client',
+    VALIDATION: 'validation',
+    UNKNOWN: 'unknown'
+} as const
+
+// 定义错误类型类型
+export type ErrorTypeValue = typeof ErrorType[keyof typeof ErrorType]
+
+/**
+ * 根据错误对象确定错误类型
+ * @param error - 错误对象
+ * @returns 错误类型
+ */
+export const getErrorType = (error: unknown): ErrorTypeValue => {
+    // 判断是否为ApiError
+    const isApiError = error instanceof Error && error.name === 'ApiError'
+
+    // 如果是ApiError,使用code判断错误类型
+    if (isApiError && 'code' in error) {
+        const apiError = error as ApiError
+        const code = apiError.code
+
+        if (code === 401) {
+            return ErrorType.UNAUTHORIZED
+        } else if (code === 403) {
+            return ErrorType.FORBIDDEN
+        } else if (code === 408 || code === 504) {
+            return ErrorType.TIMEOUT
+        } else if (code >= 400 && code < 500) {
+            // 判断是否为验证错误
+            if ('errorDetail' in apiError && apiError.errorDetail && typeof apiError.errorDetail === 'object') {
+                return ErrorType.VALIDATION
+            }
+            return ErrorType.CLIENT
+        } else if (code >= 500) {
+            return ErrorType.SERVER
+        }
+    } else if (error instanceof Error) {
+        // 非ApiError情况,尝试从消息判断
+        const message = error.message.toLowerCase()
+        if (message.includes('network') || message.includes('连接') || message.includes('connection')) {
+            return ErrorType.NETWORK
+        } else if (message.includes('timeout') || message.includes('超时')) {
+            return ErrorType.TIMEOUT
+        }
+    }
+
+    return ErrorType.UNKNOWN
+}

+ 62 - 0
web/src/components/layout/AnimatedRoute.tsx

@@ -0,0 +1,62 @@
+import { ReactNode } from "react"
+import { AnimatePresence, motion, useReducedMotion } from "motion/react"
+import { useLocation } from "react-router"
+import {
+    pageSlideTransition,
+    pageFadeTransition,
+    pageScaleTransition,
+    pageFlipTransition
+} from "@/components/ui/animation/route-animation"
+
+interface AnimatedRouteProps {
+    children: ReactNode
+    transitionType?: "slide" | "fade" | "scale" | "flip"
+}
+
+export function AnimatedRoute({
+    children,
+    transitionType = "slide"
+}: AnimatedRouteProps) {
+    const location = useLocation()
+    const prefersReducedMotion = useReducedMotion()
+
+    // 如果用户设置了减少动画,则使用简单淡入淡出
+    if (prefersReducedMotion) {
+        return (
+            <AnimatePresence mode="wait">
+                <motion.div
+                    key={location.pathname}
+                    {...pageFadeTransition}
+                >
+                    {children}
+                </motion.div>
+            </AnimatePresence>
+        )
+    }
+
+    // 根据传入的类型选择不同的动画效果
+    const getTransitionProps = () => {
+        switch (transitionType) {
+            case "fade":
+                return pageFadeTransition
+            case "scale":
+                return pageScaleTransition
+            case "flip":
+                return pageFlipTransition
+            case "slide":
+            default:
+                return pageSlideTransition
+        }
+    }
+
+    return (
+        <AnimatePresence mode="wait">
+            <motion.div
+                key={location.pathname}
+                {...getTransitionProps()}
+            >
+                {children}
+            </motion.div>
+        </AnimatePresence>
+    )
+} 

+ 32 - 0
web/src/components/layout/RootLayOut.tsx

@@ -0,0 +1,32 @@
+import { useState } from "react"
+import { Outlet } from "react-router"
+import { Sidebar } from "./SideBar"
+import { cn } from "@/lib/utils"
+
+export function RootLayout() {
+  const [collapsed, setCollapsed] = useState(false)
+
+  return (
+    <div className="flex h-screen bg-background">
+      <Sidebar
+        displayConfig={{
+          monitor: false,
+          key: true,
+          channel: true,
+          model: true,
+          log: false,
+          doc: true,
+          github: true,
+        }}
+        collapsed={collapsed}
+        onToggle={() => setCollapsed(!collapsed)}
+      />
+
+      <main className={cn("flex-1 flex flex-col overflow-hidden transition-all duration-300")}>
+        <div className="flex-1 overflow-auto">
+          <Outlet />
+        </div>
+      </main>
+    </div>
+  )
+}

+ 274 - 0
web/src/components/layout/SideBar.tsx

@@ -0,0 +1,274 @@
+import type React from "react"
+
+import { Link, useLocation, useNavigate } from "react-router"
+import {
+    Bot,
+    Layers,
+    BarChart2,
+    Database,
+    Calendar,
+    ChevronLeft,
+    ChevronRight,
+    FileText,
+    Github,
+    LogOut,
+} from "lucide-react"
+import { useTranslation } from "react-i18next"
+import type { TFunction } from "i18next"
+import { ROUTES } from "@/routes/constants"
+import { cn } from "@/lib/utils"
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
+import { Button } from "@/components/ui/button"
+import useAuthStore from "@/store/auth"
+
+interface SidebarItem {
+    title: string
+    icon: React.ComponentType<{ className?: string }>
+    href: string
+    display: boolean
+    external?: boolean
+}
+
+function createSidebarConfig(t: TFunction): SidebarItem[] {
+    return [
+        {
+            title: t("sidebar.monitor"),
+            icon: BarChart2,
+            href: ROUTES.MONITOR,
+            display: true,
+        },
+        {
+            title: t("sidebar.key"),
+            icon: Bot,
+            href: ROUTES.KEY,
+            display: true,
+        },
+        {
+            title: t("sidebar.channel"),
+            icon: Database,
+            href: ROUTES.CHANNEL,
+            display: true,
+        },
+        {
+            title: t("sidebar.model"),
+            icon: Layers,
+            href: ROUTES.MODEL,
+            display: true,
+        },
+        {
+            title: t("sidebar.log"),
+            icon: Calendar,
+            href: ROUTES.LOG,
+            display: true,
+        },
+        {
+            title: t("sidebar.doc"),
+            icon: FileText,
+            href: "https://sealos.run/docs/guides/ai-proxy",
+            display: true,
+            external: true,
+        },
+        {
+            title: t("sidebar.github"),
+            icon: Github,
+            href: "https://github.com/labring/aiproxy",
+            display: true,
+            external: true,
+        },
+    ]
+}
+
+interface SidebarDisplayConfig {
+    monitor?: boolean
+    key?: boolean
+    channel?: boolean
+    model?: boolean
+    log?: boolean
+    doc?: boolean
+    github?: boolean
+}
+
+interface SidebarProps {
+    displayConfig?: SidebarDisplayConfig
+    collapsed?: boolean
+    onToggle?: () => void
+}
+
+export function Sidebar({ displayConfig = {}, collapsed = false, onToggle }: SidebarProps) {
+    const location = useLocation()
+    const navigate = useNavigate()
+    const { t } = useTranslation()
+    const logout = useAuthStore((s) => s.logout)
+
+    const currentFirstLevelPath = "/" + location.pathname.split("/")[1]
+
+    const sidebarItems = createSidebarConfig(t).map((item) => {
+        // Determine which config property based on path name
+        let configKey: keyof SidebarDisplayConfig = "monitor"
+        if (item.href === ROUTES.KEY) configKey = "key"
+        if (item.href === ROUTES.CHANNEL) configKey = "channel"
+        if (item.href === ROUTES.MODEL) configKey = "model"
+        if (item.href === ROUTES.LOG) configKey = "log"
+        if (item.href === "https://sealos.run/docs/guides/ai-proxy") configKey = "doc"
+        if (item.href === "https://github.com/labring/aiproxy") configKey = "github"
+
+        const shouldDisplay = displayConfig[configKey] !== undefined ? displayConfig[configKey] : item.display
+
+        return {
+            ...item,
+            display: shouldDisplay,
+        }
+    })
+
+    const handleLogout = () => {
+        logout()
+        navigate("/login")
+    }
+
+    return (
+        <div
+            className={cn(
+                "h-full relative overflow-hidden flex flex-col transition-all duration-300 ease-in-out",
+                "bg-gradient-to-b from-[#6A6DE6] to-[#8A8DF7] dark:from-[#4A4DA0] dark:to-[#5155A5]",
+                collapsed ? "w-20" : "w-64",
+            )}
+        >
+            {/* 粒子效果 */}
+            <div className="absolute inset-0 overflow-hidden pointer-events-none">
+                {Array.from({ length: 25 }).map((_, i) => (
+                    <div
+                        key={i}
+                        className="absolute rounded-full bg-white/10 dark:bg-white/5 sidebar-particle"
+                        style={{
+                            width: `${Math.random() * 6 + 2}px`,
+                            height: `${Math.random() * 6 + 2}px`,
+                            top: `${Math.random() * 100}%`,
+                            left: `${Math.random() * 100}%`,
+                            animationDelay: `${Math.random() * 5}s`,
+                        }}
+                    />
+                ))}
+            </div>
+
+            <div className="relative z-10 flex items-center justify-between p-6 border-b border-white/20 dark:border-white/10">
+                <div
+                    className={cn(
+                        "overflow-hidden transition-all duration-300 ease-in-out flex-shrink-0",
+                        collapsed ? "w-0 opacity-0" : "w-auto opacity-100",
+                    )}
+                >
+                    <h1 className="text-lg font-semibold text-white whitespace-nowrap">AI Proxy</h1>
+                </div>
+                <Button
+                    variant="ghost"
+                    size="icon"
+                    onClick={onToggle}
+                    className={cn(
+                        "rounded-full hover:bg-white/10 hover:text-white transition-all flex-shrink-0 w-8 h-8 flex items-center justify-center text-white",
+                        collapsed ? "ml-auto mr-auto" : "ml-auto",
+                    )}
+                >
+                    {collapsed ? <ChevronRight className="h-5 w-5" /> : <ChevronLeft className="h-5 w-5" />}
+                </Button>
+            </div>
+
+            <div className="flex-1 py-2 overflow-y-auto relative z-10">
+                <TooltipProvider delayDuration={300}>
+                    {sidebarItems
+                        .filter((item) => item.display)
+                        .map((item) => {
+                            const isActive = !item.external && currentFirstLevelPath === item.href
+                            const content = (
+                                <>
+                                    <div className="flex items-center justify-center w-5 h-5">
+                                        <item.icon
+                                            className={cn(
+                                                "w-5 h-5 transition-all duration-300 ease-in-out",
+                                                isActive ? "text-white" : "text-white/90",
+                                                "group-hover:scale-125 group-hover:rotate-6 group-hover:animate-bounce-subtle",
+                                            )}
+                                        />
+                                    </div>
+
+                                    <span
+                                        className={cn(
+                                            "ml-3 font-medium whitespace-nowrap transition-all duration-300 ease-in-out",
+                                            isActive ? "text-white" : "text-white/90",
+                                            collapsed ? "opacity-0 w-0 overflow-hidden" : "opacity-100 w-auto",
+                                        )}
+                                    >
+                                        {item.title}
+                                    </span>
+                                </>
+                            )
+
+                            return (
+                                <Tooltip key={item.href}>
+                                    <TooltipTrigger asChild>
+                                        {item.external ? (
+                                            <a
+                                                href={item.href}
+                                                target="_blank"
+                                                rel="noopener noreferrer"
+                                                className={cn(
+                                                    "group flex items-center px-6 py-3 my-1 mx-2 rounded-lg transition-all duration-200",
+                                                    "text-white/90 hover:bg-white/10",
+                                                    collapsed ? "justify-center" : "",
+                                                )}
+                                            >
+                                                {content}
+                                            </a>
+                                        ) : (
+                                            <Link
+                                                to={item.href}
+                                                className={cn(
+                                                    "group flex items-center px-6 py-3 my-1 mx-2 rounded-lg transition-all duration-200",
+                                                    isActive
+                                                        ? "bg-white/15 text-white backdrop-blur-sm shadow-[0_0_10px_rgba(255,255,255,0.15)]"
+                                                        : "text-white/90 hover:bg-white/10",
+                                                    collapsed ? "justify-center" : "",
+                                                )}
+                                            >
+                                                {content}
+                                            </Link>
+                                        )}
+                                    </TooltipTrigger>
+                                    {collapsed && <TooltipContent side="right">{item.title}</TooltipContent>}
+                                </Tooltip>
+                            )
+                        })}
+                </TooltipProvider>
+            </div>
+
+            {/* Logout button */}
+            <div className="p-4 border-t border-white/20 dark:border-white/10 relative z-10">
+                <Tooltip>
+                    <TooltipTrigger asChild>
+                        <Button
+                            variant="secondary"
+                            onClick={handleLogout}
+                            className={cn(
+                                "group w-full flex items-center px-4 py-3 rounded-lg transition-all duration-200",
+                                "text-[#6A6DE6] dark:text-[#4A4DA0] bg-white hover:bg-gray-100",
+                                collapsed ? "justify-center" : "justify-start",
+                            )}
+                        >
+                            <div className="flex items-center justify-center w-5 h-5">
+                                <LogOut className="w-5 h-5 transition-all duration-300 ease-in-out group-hover:scale-125 group-hover:rotate-6 group-hover:animate-bounce-subtle" />
+                            </div>
+                            <span
+                                className={cn(
+                                    "ml-3 font-medium whitespace-nowrap transition-all duration-300 ease-in-out",
+                                    collapsed ? "opacity-0 w-0 overflow-hidden" : "opacity-100 w-auto",
+                                )}
+                            >
+                                {t("sidebar.logout")}
+                            </span>
+                        </Button>
+                    </TooltipTrigger>
+                    {collapsed && <TooltipContent side="right">{t("sidebar.logout")}</TooltipContent>}
+                </Tooltip>
+            </div>
+        </div>
+    )
+}

+ 223 - 0
web/src/components/select/ConstructMappingComponent.tsx

@@ -0,0 +1,223 @@
+import { useState, useEffect, useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Label } from '@/components/ui/label'
+import { Input } from '@/components/ui/input'
+import { Button } from '@/components/ui/button'
+import { TrashIcon, PlusIcon } from 'lucide-react'
+import { CustomSelect } from './Select'
+
+type MapKeyValuePair = { key: string; value: string }
+
+// mapKeys determines the available selection options
+export const ConstructMappingComponent = function ({
+    mapKeys,
+    mapData,
+    setMapData
+}: {
+    mapKeys: string[]
+    mapData: Record<string, string>
+    setMapData: (mapping: Record<string, string>) => void
+}) {
+    const { t } = useTranslation()
+
+    const [mapKeyValuePairs, setMapkeyValuePairs] = useState<Array<MapKeyValuePair>>([])
+    const [isInternalUpdate, setIsInternalUpdate] = useState(false)
+
+    useEffect(() => {
+        if (!isInternalUpdate) {
+            const entries = Object.entries(mapData)
+            setMapkeyValuePairs(
+                entries.length > 0
+                    ? entries.map(([key, value]) => ({ key, value }))
+                    : [{ key: '', value: '' }]
+            )
+        }
+        setIsInternalUpdate(false)
+    }, [mapData])
+
+    const handleDropdownItemDisplay = (dropdownItem: string) => {
+        if (dropdownItem === t('channel.dialog.selectModels')) {
+            return (
+                <span className="text-xs font-normal leading-4 tracking-[0.048px] text-muted-foreground">
+                    {t('channel.dialog.selectModels')}
+                </span>
+            )
+        }
+        return (
+            <span className="text-xs font-normal leading-4 tracking-[0.048px]">
+                {dropdownItem}
+            </span>
+        )
+    }
+
+    const handleSeletedItemDisplay = (selectedItem: string) => {
+        if (selectedItem === t('channel.dialog.selectModels')) {
+            return (
+                <span className="text-xs font-normal leading-4 tracking-[0.048px] text-muted-foreground">
+                    {t('channel.dialog.selectModels')}
+                </span>
+            )
+        }
+        return (
+            <div className="max-w-[114px] overflow-x-auto whitespace-nowrap scrollbar-none">
+                <span className="text-xs font-normal leading-4 tracking-[0.048px]">
+                    {selectedItem}
+                </span>
+            </div>
+        )
+    }
+
+    // Handling mapData and mapKeyValuePairs cleanup when map keys change.
+    useEffect(() => {
+        // 1. Handle mapData cleanup
+        const removedKeysFromMapData = Object.keys(mapData).filter((key) => !mapKeys.includes(key))
+        if (removedKeysFromMapData.length > 0) {
+            const newMapData = { ...mapData }
+            removedKeysFromMapData.forEach((key) => {
+                delete newMapData[key]
+            })
+            setIsInternalUpdate(true)
+            setMapData(newMapData)
+        }
+
+        // 2. Handle mapKeyValuePairs cleanup
+        const removedPairs = mapKeyValuePairs.filter((pair) => pair.key && !mapKeys.includes(pair.key))
+        if (removedPairs.length > 0) {
+            const newMapKeyValuePairs = mapKeyValuePairs.filter(
+                (pair) => !pair.key || mapKeys.includes(pair.key)
+            )
+            setMapkeyValuePairs(newMapKeyValuePairs)
+        }
+    }, [mapKeys])
+
+    // Get the keys that have been selected
+    const getSelectedMapKeys = (currentIndex: number) => {
+        const selected = new Set<string>()
+        mapKeyValuePairs.forEach((mapKeyValuePair, idx) => {
+            if (idx !== currentIndex && mapKeyValuePair.key) {
+                selected.add(mapKeyValuePair.key)
+            }
+        })
+        return selected
+    }
+
+    // Handling adding a new row
+    const handleAddNewMapKeyPair = () => {
+        setMapkeyValuePairs([...mapKeyValuePairs, { key: '', value: '' }])
+    }
+
+    // Handling deleting a row
+    const handleRemoveMapKeyPair = (index: number) => {
+        const mapKeyValuePair = mapKeyValuePairs[index]
+        const newMapData = { ...mapData }
+        if (mapKeyValuePair.key) {
+            delete newMapData[mapKeyValuePair.key]
+        }
+        setIsInternalUpdate(true)
+        setMapData(newMapData)
+
+        const newMapKeyValuePairs = mapKeyValuePairs.filter((_, idx) => idx !== index)
+        setMapkeyValuePairs(newMapKeyValuePairs)
+    }
+
+    // Handling selection/input changes
+    const handleInputChange = (index: number, field: 'key' | 'value', value: string) => {
+        const newMapKeyValuePairs = [...mapKeyValuePairs]
+        const oldValue = newMapKeyValuePairs[index][field]
+        newMapKeyValuePairs[index][field] = value
+
+        // Update the mapping relationship
+        const newMapData = { ...mapData }
+        if (field === 'key') {
+            if (oldValue) delete newMapData[oldValue]
+
+            if (!value) {
+                newMapKeyValuePairs[index].value = ''
+            }
+
+            if (value && newMapKeyValuePairs[index].value) {
+                newMapData[value] = newMapKeyValuePairs[index].value
+            }
+        } else {
+            if (newMapKeyValuePairs[index].key) {
+                newMapData[newMapKeyValuePairs[index].key] = value
+            }
+        }
+
+        setMapkeyValuePairs(newMapKeyValuePairs)
+        setIsInternalUpdate(true)
+        setMapData(newMapData)
+    }
+
+    // Check if there are still keys that can be selected
+    const hasAvailableKeys = useMemo(() => {
+        const usedKeys = new Set(
+            mapKeyValuePairs.map((mapKeyValuePair) => mapKeyValuePair.key).filter(Boolean)
+        )
+        // Ensure mapKeyValuePairs length does not exceed mapKeys length
+        return (
+            mapKeyValuePairs.length < mapKeys.length && mapKeys.some((mapKey) => !usedKeys.has(mapKey))
+        )
+    }, [mapKeys, mapKeyValuePairs])
+
+    return (
+        <div className="w-full flex flex-col gap-2 items-start">
+            <Label
+                className="text-sm font-medium leading-5 tracking-[0.1px] flex items-center h-5 m-0"
+            >
+                {t('channel.dialog.modelMapping')}
+            </Label>
+
+            {mapKeyValuePairs.map((row, index) => (
+                <div key={`${index}-${row.key}`} className="flex gap-2 w-full items-center">
+                    <CustomSelect<string>
+                        listItems={mapKeys.filter((key) => !getSelectedMapKeys(index).has(key))}
+                        initSelectedItem={row.key !== '' && row.key ? row.key : undefined}
+                        // when select placeholder, the newSelectedItem is null
+                        handleSelectedItemChange={(newSelectedItem) =>
+                            handleInputChange(index, 'key', newSelectedItem)
+                        }
+                        handleDropdownItemDisplay={handleDropdownItemDisplay}
+                        handleSelectedItemDisplay={handleSeletedItemDisplay}
+                        placeholder={t('channel.dialog.selectModels')}
+                    />
+
+                    <div className="flex-1 w-full">
+                        <Input
+                            className="h-8 py-2 px-3 text-xs"
+                            value={row.value}
+                            onChange={(e) => handleInputChange(index, 'value', e.target.value)}
+                            placeholder={t('channel.dialog.mappedName')}
+                        />
+                    </div>
+
+                    <Button
+                        type="button"
+                        variant="ghost"
+                        size="icon"
+                        className="h-8 w-8 p-0 hover:bg-accent hover:text-destructive"
+                        onClick={() => handleRemoveMapKeyPair(index)}
+                    >
+                        <TrashIcon className="h-4 w-4" />
+                    </Button>
+                </div>
+            ))}
+
+            {hasAvailableKeys && (
+                <Button
+                    type="button"
+                    variant="outline"
+                    className="h-8 w-full flex items-center justify-center gap-1.5 hover:border-primary-300"
+                    onClick={handleAddNewMapKeyPair}
+                >
+                    <PlusIcon className="h-4 w-4 text-muted-foreground" />
+                    <span className="text-xs font-medium leading-4 tracking-[0.5px] text-muted-foreground">
+                        {t('channel.dialog.create')}
+                    </span>
+                </Button>
+            )}
+        </div>
+    )
+}
+
+export default ConstructMappingComponent

+ 200 - 0
web/src/components/select/MultiSelectCombobox.tsx

@@ -0,0 +1,200 @@
+import { useState, useMemo, Dispatch, SetStateAction, ReactNode, JSX } from 'react'
+import { useCombobox, useMultipleSelection } from 'downshift'
+import { useTranslation } from 'react-i18next'
+import { cn } from '@/lib/utils'
+import { XIcon, ChevronUpIcon, ChevronDownIcon } from 'lucide-react'
+import { Label } from '@/components/ui/label'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+
+export const MultiSelectCombobox = function <T>({
+    dropdownItems,
+    selectedItems,
+    setSelectedItems,
+    handleFilteredDropdownItems,
+    handleDropdownItemDisplay,
+    handleSelectedItemDisplay,
+}: {
+    dropdownItems: T[]
+    selectedItems: T[]
+    setSelectedItems: Dispatch<SetStateAction<T[]>>
+    handleFilteredDropdownItems: (dropdownItems: T[], selectedItems: T[], inputValue: string) => T[]
+    handleDropdownItemDisplay: (dropdownItem: T) => ReactNode
+    handleSelectedItemDisplay: (selectedItem: T) => ReactNode
+}): JSX.Element {
+    const { t } = useTranslation()
+
+    const [inputValue, setInputValue] = useState<string>('')
+
+    // Dropdown list excludes already selected options and includes those matching the input.
+    const items = useMemo(
+        () => handleFilteredDropdownItems(dropdownItems, selectedItems, inputValue),
+        [inputValue, selectedItems, dropdownItems, handleFilteredDropdownItems]
+    )
+
+    const { getSelectedItemProps, getDropdownProps, removeSelectedItem } = useMultipleSelection({
+        selectedItems,
+        onStateChange({ selectedItems: newSelectedItems, type }) {
+            switch (type) {
+                case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
+                case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
+                case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
+                case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
+                    if (newSelectedItems) {
+                        setSelectedItems(newSelectedItems)
+                    }
+                    break
+                default:
+                    break
+            }
+        }
+    })
+    const {
+        isOpen,
+        getToggleButtonProps,
+        getLabelProps,
+        getMenuProps,
+        getInputProps,
+        highlightedIndex,
+        getItemProps,
+        selectedItem
+    } = useCombobox({
+        items,
+        defaultHighlightedIndex: 0, // after selection, highlight the first item.
+        selectedItem: null,
+        inputValue,
+        // @ts-expect-error 忽略未使用参数
+        stateReducer(state, actionAndChanges) {
+            const { changes, type } = actionAndChanges
+
+            switch (type) {
+                case useCombobox.stateChangeTypes.InputKeyDownEnter:
+                case useCombobox.stateChangeTypes.ItemClick:
+                    return {
+                        ...changes,
+                        isOpen: true, // keep the menu open after selection.
+                        highlightedIndex: 0 // with the first option highlighted.
+                    }
+                default:
+                    return changes
+            }
+        },
+        onStateChange({ inputValue: newInputValue, type, selectedItem: newSelectedItem }) {
+            switch (type) {
+                case useCombobox.stateChangeTypes.InputKeyDownEnter:
+                case useCombobox.stateChangeTypes.ItemClick:
+                case useCombobox.stateChangeTypes.InputBlur:
+                    if (newSelectedItem) {
+                        setSelectedItems([...selectedItems, newSelectedItem])
+                        setInputValue('')
+                    }
+                    break
+
+                case useCombobox.stateChangeTypes.InputChange:
+                    setInputValue(newInputValue ?? '')
+
+                    break
+                default:
+                    break
+            }
+        }
+    })
+
+    return (
+        <div className="w-full relative">
+            <div className="w-full flex flex-col gap-2 items-start">
+                <Label
+                    className="flex m-0 w-full h-5 justify-between items-center"
+                    {...getLabelProps()}
+                >
+                    <div className="flex gap-0.5 items-start">
+                        <span className="whitespace-nowrap text-sm font-medium leading-5 tracking-[0.1px]">
+                            {t('channel.dialog.models')}
+                        </span>
+                    </div>
+                </Label>
+
+                <div
+                    className="w-full bg-accent/30 rounded-md border border-input p-2"
+                >
+                    <div className="flex flex-wrap gap-2 items-center">
+                        {selectedItems.map((selectedItemForRender, index) => (
+                            <div
+                                key={`selected-item-${index}`}
+                                className={cn(
+                                    "bg-accent/50 rounded-md px-1.5 py-1",
+                                    "focus:bg-primary/20"
+                                )}
+                                {...getSelectedItemProps({
+                                    selectedItem: selectedItemForRender,
+                                    index: index
+                                })}
+                            >
+                                <div className="flex items-center gap-2">
+                                    {handleSelectedItemDisplay(selectedItemForRender)}
+                                    <button
+                                        className="h-4 w-4 rounded flex items-center justify-center cursor-pointer text-muted-foreground hover:text-foreground"
+                                        onClick={(e) => {
+                                            e.stopPropagation()
+                                            removeSelectedItem(selectedItemForRender)
+                                        }}
+                                    >
+                                        <XIcon className="h-3 w-3" />
+                                    </button>
+                                </div>
+                            </div>
+                        ))}
+
+                        <div className="flex flex-1 gap-1">
+                            <Input
+                                className="border-none shadow-none h-auto p-0 text-xs font-normal leading-4 tracking-[0.048px] bg-transparent"
+                                placeholder={t('channel.dialog.selectModels')}
+                                {...getInputProps(getDropdownProps({ preventKeyAction: isOpen }))}
+                            />
+
+                            <Button
+                                type="button"
+                                variant="ghost"
+                                size="icon"
+                                className="h-8 w-8 p-0 flex items-center justify-center shrink-0"
+                                {...getToggleButtonProps()}
+                            >
+                                {isOpen ? (
+                                    <ChevronUpIcon className="h-4 w-4 text-muted-foreground" />
+                                ) : (
+                                    <ChevronDownIcon className="h-4 w-4 text-muted-foreground" />
+                                )}
+                            </Button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <ul
+                className={cn(
+                    "absolute mt-1 w-full py-1.5 px-1.5 bg-popover",
+                    "border border-input max-h-60 overflow-y-auto z-10 rounded-md",
+                    isOpen && items.length ? "block" : "hidden"
+                )}
+                {...getMenuProps()}
+            >
+                {isOpen &&
+                    items.map((item, index) => (
+                        <li
+                            key={index}
+                            className={cn(
+                                "flex p-2 items-center gap-2 self-stretch rounded",
+                                "text-xs font-normal leading-4 tracking-[0.5px] cursor-pointer",
+                                highlightedIndex === index ? "bg-accent" : "bg-transparent",
+                                selectedItem === item ? "font-bold" : "font-normal",
+                                "hover:bg-accent text-foreground"
+                            )}
+                            {...getItemProps({ item, index })}
+                        >
+                            {handleDropdownItemDisplay(item)}
+                        </li>
+                    ))}
+            </ul>
+        </div>
+    )
+}

+ 102 - 0
web/src/components/select/Select.tsx

@@ -0,0 +1,102 @@
+import { ReactNode } from 'react'
+import { useSelect } from 'downshift'
+import { cn } from '@/lib/utils'
+import { ChevronDownIcon } from 'lucide-react'
+
+export const CustomSelect = function <T>({
+    listItems,
+    handleSelectedItemChange,
+    handleDropdownItemDisplay,
+    handleSelectedItemDisplay,
+    placeholder,
+    initSelectedItem
+}: {
+    listItems: T[]
+    handleSelectedItemChange: (selectedItem: T) => void
+    handleDropdownItemDisplay: (dropdownItem: T) => ReactNode
+    handleSelectedItemDisplay: (selectedItem: T) => ReactNode
+    placeholder?: string
+    initSelectedItem?: T
+}) {
+    const items = [placeholder, ...listItems]
+
+    const {
+        isOpen,
+        selectedItem,
+        getToggleButtonProps,
+        getMenuProps,
+        getItemProps,
+        highlightedIndex
+    } = useSelect({
+        items: items,
+        initialSelectedItem: initSelectedItem,
+        onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
+            if (newSelectedItem === placeholder) {
+                handleSelectedItemChange(undefined as T)
+            } else {
+                handleSelectedItemChange(newSelectedItem as T)
+            }
+        }
+    })
+
+    return (
+        <div className="w-full relative flex-1">
+            <div
+                className={cn(
+                    "h-8 w-full rounded-md border border-input flex items-center px-3",
+                    "bg-background dark:bg-background",
+                    "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+                    "hover:border-primary-300 dark:hover:border-primary"
+                )}
+                {...getToggleButtonProps()}
+            >
+                {selectedItem ? (
+                    handleSelectedItemDisplay(selectedItem as T)
+                ) : placeholder ? (
+                    <p
+                        className="flex-1 text-xs font-normal leading-4 tracking-[0.048px] text-muted-foreground"
+                    >
+                        {placeholder}
+                    </p>
+                ) : (
+                    <p
+                        className="flex-1 text-xs font-normal leading-4 tracking-[0.048px] text-muted-foreground"
+                    >
+                        Select
+                    </p>
+                )}
+                <div className="ml-auto" style={{ transform: isOpen ? 'rotate(180deg)' : undefined }}>
+                    <ChevronDownIcon className="size-3 text-foreground" />
+                </div>
+            </div>
+
+            <ul
+                {...getMenuProps()}
+                className={cn(
+                    "absolute mt-0.5 w-full py-1.5 px-1.5 bg-popover",
+                    "border border-input max-h-60 overflow-y-auto z-10 rounded-md",
+                    "dark:bg-popover dark:border-input dark:text-popover-foreground",
+                    "shadow-md",
+                    isOpen && items.length ? "block" : "hidden"
+                )}
+            >
+                {isOpen &&
+                    items.map((item, index) => (
+                        <li
+                            {...getItemProps({ item, index })}
+                            key={index}
+                            className={cn(
+                                "flex p-2 items-center gap-2 self-stretch rounded",
+                                "text-xs font-normal leading-4 tracking-[0.5px] cursor-pointer",
+                                highlightedIndex === index ? "bg-accent dark:bg-accent" : "bg-transparent",
+                                selectedItem === item ? "font-bold" : "font-normal",
+                                "hover:bg-accent dark:hover:bg-accent hover:text-accent-foreground dark:hover:text-accent-foreground text-foreground"
+                            )}
+                        >
+                            {handleDropdownItemDisplay(item as T)}
+                        </li>
+                    ))}
+            </ul>
+        </div>
+    )
+}

+ 128 - 0
web/src/components/select/SingleSelectCombobox.tsx

@@ -0,0 +1,128 @@
+import { useState, ReactNode, useEffect, JSX } from 'react'
+import { useCombobox, UseComboboxReturnValue } from 'downshift'
+import { useTranslation } from 'react-i18next'
+import { cn } from '@/lib/utils'
+import { ChevronUpIcon, ChevronDownIcon } from 'lucide-react'
+import { Label } from '@/components/ui/label'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+
+export const SingleSelectCombobox: <T>(props: {
+    dropdownItems: T[]
+    setSelectedItem: (value: T) => void
+    handleDropdownItemFilter: (dropdownItems: T[], inputValue: string) => T[]
+    handleDropdownItemDisplay: (dropdownItem: T) => ReactNode
+    handleInputDisplay?: (item: T) => string
+    initSelectedItem?: T
+}) => JSX.Element = function <T>({
+    dropdownItems,
+    setSelectedItem,
+    handleDropdownItemFilter,
+    handleDropdownItemDisplay,
+    handleInputDisplay,
+    initSelectedItem
+}: {
+    dropdownItems: T[]
+    setSelectedItem: (value: T) => void
+    handleDropdownItemFilter: (dropdownItems: T[], inputValue: string) => T[]
+    handleDropdownItemDisplay: (dropdownItem: T) => ReactNode
+    handleInputDisplay?: (item: T) => string
+    initSelectedItem?: T
+}) {
+        const { t } = useTranslation()
+        const [getFilteredDropdownItems, setGetFilteredDropdownItems] = useState<T[]>(dropdownItems)
+
+        useEffect(() => {
+            setGetFilteredDropdownItems(dropdownItems)
+        }, [dropdownItems])
+
+        const {
+            isOpen: isComboboxOpen,
+            getToggleButtonProps,
+            getLabelProps,
+            getMenuProps,
+            getInputProps,
+            highlightedIndex,
+            getItemProps,
+            selectedItem
+        }: UseComboboxReturnValue<T> = useCombobox({
+            items: getFilteredDropdownItems,
+            onInputValueChange: ({ inputValue }) => {
+                setGetFilteredDropdownItems(handleDropdownItemFilter(dropdownItems, inputValue || ''))
+            },
+
+            initialSelectedItem: initSelectedItem || undefined,
+
+            itemToString: (item) => {
+                if (!item) return ''
+                return handleInputDisplay ? handleInputDisplay(item) : String(item)
+            },
+
+            onSelectedItemChange: ({ selectedItem }) => {
+                const selectedDropdownItem = dropdownItems.find((item) => item === selectedItem)
+                if (selectedDropdownItem) {
+                    setSelectedItem(selectedDropdownItem)
+                }
+            }
+        })
+
+        return (
+            <div className="w-full relative">
+                <div className="w-full flex flex-col gap-2 items-start">
+                    <Label
+                        className="text-sm font-medium leading-5 h-5 m-0 whitespace-nowrap"
+                        {...getLabelProps()}
+                    >
+                        {t('channel.dialog.type')}
+                    </Label>
+
+                    <div className="w-full relative flex items-center">
+                        <Input
+                            className="h-8 py-2 pl-3 pr-11 rounded-md text-xs font-normal leading-4 tracking-[0.048px]"
+                            placeholder={t('channel.dialog.selectType')}
+                            {...getInputProps()}
+                        />
+                        <Button
+                            type="button"
+                            variant="ghost"
+                            size="icon"
+                            className="absolute right-0 h-8 w-8 p-0 flex items-center justify-center"
+                            {...getToggleButtonProps()}
+                        >
+                            {isComboboxOpen ? (
+                                <ChevronUpIcon className="h-4 w-4 text-muted-foreground" />
+                            ) : (
+                                <ChevronDownIcon className="h-4 w-4 text-muted-foreground" />
+                            )}
+                        </Button>
+                    </div>
+                </div>
+
+                <ul
+                    className={cn(
+                        "absolute mt-1 w-full py-1.5 px-1.5 bg-popover",
+                        "border border-input max-h-60 overflow-y-auto z-10 rounded-md",
+                        isComboboxOpen && getFilteredDropdownItems.length ? "block" : "hidden"
+                    )}
+                    {...getMenuProps()}
+                >
+                    {isComboboxOpen &&
+                        getFilteredDropdownItems.map((item, index) => (
+                            <li
+                                key={index}
+                                {...getItemProps({ item, index })}
+                                className={cn(
+                                    "flex p-2 items-center gap-2 self-stretch rounded",
+                                    "text-xs font-normal leading-4 tracking-[0.5px] cursor-pointer",
+                                    highlightedIndex === index ? "bg-accent" : "bg-transparent",
+                                    selectedItem === item ? "font-bold" : "font-normal",
+                                    "hover:bg-accent text-foreground"
+                                )}
+                            >
+                                {handleDropdownItemDisplay(item)}
+                            </li>
+                        ))}
+                </ul>
+            </div>
+        )
+    }

+ 66 - 0
web/src/components/table/column-header.tsx

@@ -0,0 +1,66 @@
+import { Column } from "@tanstack/react-table"
+import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import {
+    DropdownMenu,
+    DropdownMenuContent,
+    DropdownMenuItem,
+    DropdownMenuSeparator,
+    DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+
+interface DataTableColumnHeaderProps<TData, TValue>
+    extends React.HTMLAttributes<HTMLDivElement> {
+    column: Column<TData, TValue>
+    title: string
+}
+
+export function DataTableColumnHeader<TData, TValue>({
+    column,
+    title,
+    className,
+}: DataTableColumnHeaderProps<TData, TValue>) {
+    if (!column.getCanSort()) {
+        return <div className={cn(className)}>{title}</div>
+    }
+
+    return (
+        <div className={cn("flex items-center space-x-2", className)}>
+            <DropdownMenu>
+                <DropdownMenuTrigger asChild>
+                    <Button
+                        variant="ghost"
+                        size="sm"
+                        className="-ml-3 h-8 data-[state=open]:bg-accent"
+                    >
+                        <span>{title}</span>
+                        {column.getIsSorted() === "desc" ? (
+                            <ArrowDown />
+                        ) : column.getIsSorted() === "asc" ? (
+                            <ArrowUp />
+                        ) : (
+                            <ChevronsUpDown />
+                        )}
+                    </Button>
+                </DropdownMenuTrigger>
+                <DropdownMenuContent align="start">
+                    <DropdownMenuItem onClick={() => column.toggleSorting(false)}>
+                        <ArrowUp className="h-3.5 w-3.5 text-muted-foreground/70" />
+                        Asc
+                    </DropdownMenuItem>
+                    <DropdownMenuItem onClick={() => column.toggleSorting(true)}>
+                        <ArrowDown className="h-3.5 w-3.5 text-muted-foreground/70" />
+                        Desc
+                    </DropdownMenuItem>
+                    <DropdownMenuSeparator />
+                    <DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
+                        <EyeOff className="h-3.5 w-3.5 text-muted-foreground/70" />
+                        Hide
+                    </DropdownMenuItem>
+                </DropdownMenuContent>
+            </DropdownMenu>
+        </div>
+    )
+}

+ 59 - 0
web/src/components/table/column-toggle.tsx

@@ -0,0 +1,59 @@
+"use client"
+
+import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"
+import { Table } from "@tanstack/react-table"
+import { Settings2 } from "lucide-react"
+
+import { Button } from "@/components/ui/button"
+import {
+    DropdownMenu,
+    DropdownMenuCheckboxItem,
+    DropdownMenuContent,
+    DropdownMenuLabel,
+    DropdownMenuSeparator,
+} from "@/components/ui/dropdown-menu"
+
+interface DataTableViewOptionsProps<TData> {
+    table: Table<TData>
+}
+
+export function DataTableViewOptions<TData>({
+    table,
+}: DataTableViewOptionsProps<TData>) {
+    return (
+        <DropdownMenu>
+            <DropdownMenuTrigger asChild>
+                <Button
+                    variant="outline"
+                    size="sm"
+                    className="ml-auto hidden h-8 lg:flex"
+                >
+                    <Settings2 />
+                    View
+                </Button>
+            </DropdownMenuTrigger>
+            <DropdownMenuContent align="end" className="w-[150px]">
+                <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
+                <DropdownMenuSeparator />
+                {table
+                    .getAllColumns()
+                    .filter(
+                        (column) =>
+                            typeof column.accessorFn !== "undefined" && column.getCanHide()
+                    )
+                    .map((column) => {
+                        return (
+                            <DropdownMenuCheckboxItem
+                                key={column.id}
+                                className="capitalize"
+                                checked={column.getIsVisible()}
+                                onCheckedChange={(value) => column.toggleVisibility(!!value)}
+                            >
+                                {column.id}
+                            </DropdownMenuCheckboxItem>
+                        )
+                    })}
+            </DropdownMenuContent>
+        </DropdownMenu>
+    )
+}

+ 194 - 0
web/src/components/table/data-table.tsx

@@ -0,0 +1,194 @@
+import {
+    Table,
+    TableBody,
+    TableCell,
+    TableHead,
+    TableHeader,
+    TableRow,
+} from "@/components/ui/table"
+import { flexRender, Table as TableType, ColumnDef } from "@tanstack/react-table"
+import { Loader2 } from "lucide-react"
+import { useTranslation } from "react-i18next"
+import { cn } from "@/lib/utils" // 确保导入了工具函数,如果没有可以手动添加
+
+interface DataTableProps<TData, TValue> {
+    table: TableType<TData>
+    columns: ColumnDef<TData, TValue>[]
+    style?: 'default' | 'border' | 'simple'
+    isLoading?: boolean
+    loadingRows?: number
+    loadingStyle?: 'centered' | 'skeleton'
+    fixedHeader?: boolean
+}
+
+// 加载状态骨架屏组件
+const TableSkeleton = <TData, TValue>({
+    columns,
+    rows = 5
+}: {
+    columns: ColumnDef<TData, TValue>[],
+    rows?: number
+}) => (
+    <>
+        {Array.from({ length: rows }).map((_, index) => (
+            <TableRow key={`skeleton-row-${index}`} className="animate-pulse">
+                {Array.from({ length: columns.length }).map((_, cellIndex) => (
+                    <TableCell key={`skeleton-cell-${index}-${cellIndex}`}>
+                        <div className="h-4 bg-gray-200 rounded w-3/4 dark:bg-gray-700"></div>
+                    </TableCell>
+                ))}
+            </TableRow>
+        ))}
+    </>
+)
+
+// 中心加载动画组件
+const CenteredLoader = <TData, TValue>({
+    columns
+}: {
+    columns: ColumnDef<TData, TValue>[]
+}) => (
+    <TableRow>
+        <TableCell colSpan={columns.length} className="h-24">
+            <div className="flex items-center justify-center space-x-2">
+                <Loader2 className="h-6 w-6 animate-spin text-primary" />
+                <span className="text-sm text-muted-foreground">加载中...</span>
+            </div>
+        </TableCell>
+    </TableRow>
+)
+
+// 无数据状态组件
+const NoResults = <TData, TValue>({
+    columns
+}: {
+    columns: ColumnDef<TData, TValue>[]
+}) => {
+    const { t } = useTranslation()
+    return (
+        <TableRow>
+            <TableCell colSpan={columns.length} className="h-24 text-center">
+                {t('common.noResult')}
+            </TableCell>
+        </TableRow>
+    )
+}
+
+export function DataTable<TData, TValue>({
+    table,
+    columns,
+    style = 'default',
+    isLoading = false,
+    loadingRows = 5,
+    loadingStyle = 'centered',
+    fixedHeader = false,
+}: DataTableProps<TData, TValue>) {
+    // 渲染表格主体内容
+    const renderTableBody = () => {
+        if (isLoading) {
+            // 根据 loadingStyle 选项决定使用哪种加载动画
+            return loadingStyle === 'centered'
+                ? <CenteredLoader<TData, TValue> columns={columns} />
+                : <TableSkeleton<TData, TValue> columns={columns} rows={loadingRows} />
+        }
+
+        if (!table.getRowModel().rows?.length) {
+            return <NoResults<TData, TValue> columns={columns} />
+        }
+
+        return table.getRowModel().rows.map((row) => (
+            <TableRow
+                key={row.id}
+                data-state={row.getIsSelected() && "selected"}
+            >
+                {row.getVisibleCells().map((cell) => (
+                    <TableCell key={cell.id}>
+                        {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                    </TableCell>
+                ))}
+            </TableRow>
+        ))
+    }
+
+    // 表头渲染函数
+    const renderTableHeader = () => (
+        <TableHeader className={fixedHeader ? "sticky top-0 z-10 bg-background border-b" : ""}>
+            {table.getHeaderGroups().map((headerGroup) => (
+                <TableRow key={headerGroup.id}>
+                    {headerGroup.headers.map((header) => (
+                        <TableHead key={header.id}>
+                            {header.isPlaceholder
+                                ? null
+                                : flexRender(
+                                    header.column.columnDef.header,
+                                    header.getContext()
+                                )}
+                        </TableHead>
+                    ))}
+                </TableRow>
+            ))}
+        </TableHeader>
+    )
+
+    // 根据样式选择和固定表头选项构建表格
+    if (fixedHeader) {
+        // 使用固定表头的布局结构
+        return (
+            <div className={cn(
+                "w-full h-full relative",
+                style === 'border' && "rounded-md border"
+            )}>
+                <div className="overflow-auto h-full">
+                    <table className="w-full caption-bottom text-sm">
+                        {renderTableHeader()}
+                        <tbody className={cn(
+                            // 只有当isLoading为true且没有行数据时才移除最后一行的边框
+                            (isLoading || !table.getRowModel().rows?.length) ? "[&_tr:last-child]:border-0" : ""
+                        )}>
+                            {renderTableBody()}
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+        )
+    }
+
+    // 原始表格布局(无固定表头)
+    switch (style) {
+        case 'simple':
+            return (
+                <div className="w-full h-full">
+                    <Table>
+                        {renderTableHeader()}
+                        <TableBody>
+                            {renderTableBody()}
+                        </TableBody>
+                    </Table>
+                </div>
+            )
+
+        case 'border':
+            return (
+                <div className="rounded-md border h-full w-full">
+                    <Table>
+                        {renderTableHeader()}
+                        <TableBody>
+                            {renderTableBody()}
+                        </TableBody>
+                    </Table>
+                </div>
+            )
+
+        default:
+            return (
+                <div className="w-full h-full">
+                    <Table>
+                        {renderTableHeader()}
+                        <TableBody className={isLoading || !table.getRowModel().rows?.length ? "[&_tr:last-child]:!border-b-0" : ""}>
+                            {renderTableBody()}
+                        </TableBody>
+                    </Table>
+                </div>
+            )
+    }
+}

+ 272 - 0
web/src/components/table/motion-data-table.tsx

@@ -0,0 +1,272 @@
+import {
+    Table,
+    TableBody,
+    TableCell,
+    TableHead,
+    TableHeader,
+    TableRow,
+} from "@/components/ui/table"
+import { flexRender, Table as TableType, ColumnDef } from "@tanstack/react-table"
+import { Loader2 } from "lucide-react"
+import { useTranslation } from "react-i18next"
+import { cn } from "@/lib/utils"
+import { TableScrollContainer } from "@/components/ui/animation/components/table-scroll"
+import { useEffect, useRef, useState } from "react"
+
+interface DataTableProps<TData, TValue> {
+    table: TableType<TData>
+    columns: ColumnDef<TData, TValue>[]
+    style?: 'default' | 'border' | 'simple'
+    isLoading?: boolean
+    loadingRows?: number
+    loadingStyle?: 'centered' | 'skeleton'
+    fixedHeader?: boolean
+    animatedRows?: boolean
+    showScrollShadows?: boolean
+}
+
+// 加载状态骨架屏组件
+const TableSkeleton = <TData, TValue>({
+    columns,
+    rows = 5
+}: {
+    columns: ColumnDef<TData, TValue>[],
+    rows?: number
+}) => (
+    <>
+        {Array.from({ length: rows }).map((_, index) => (
+            <TableRow key={`skeleton-row-${index}`} className="animate-pulse">
+                {Array.from({ length: columns.length }).map((_, cellIndex) => (
+                    <TableCell key={`skeleton-cell-${index}-${cellIndex}`}>
+                        <div className="h-4 bg-gray-200 rounded w-3/4 dark:bg-gray-700"></div>
+                    </TableCell>
+                ))}
+            </TableRow>
+        ))}
+    </>
+)
+
+// 中心加载动画组件
+const CenteredLoader = <TData, TValue>({
+    columns
+}: {
+    columns: ColumnDef<TData, TValue>[]
+}) => (
+    <TableRow>
+        <TableCell colSpan={columns.length} className="h-24">
+            <div className="flex items-center justify-center space-x-2">
+                <Loader2 className="h-6 w-6 animate-spin text-primary" />
+                <span className="text-sm text-muted-foreground">加载中...</span>
+            </div>
+        </TableCell>
+    </TableRow>
+)
+
+// 无数据状态组件
+const NoResults = <TData, TValue>({
+    columns
+}: {
+    columns: ColumnDef<TData, TValue>[]
+}) => {
+    const { t } = useTranslation()
+    return (
+        <TableRow>
+            <TableCell colSpan={columns.length} className="h-24 text-center">
+                {t('common.noResult')}
+            </TableCell>
+        </TableRow>
+    )
+}
+
+export function DataTable<TData, TValue>({
+    table,
+    columns,
+    style = 'default',
+    isLoading = false,
+    loadingRows = 5,
+    loadingStyle = 'centered',
+    fixedHeader = false,
+    animatedRows = false,
+    showScrollShadows = true,
+}: DataTableProps<TData, TValue>) {
+    // 用于跟踪已渲染行的ref
+    const rowsRef = useRef<HTMLElement[]>([])
+    const [inViewRows, setInViewRows] = useState<Set<string>>(new Set())
+
+    // 提取复杂表达式为变量
+    const tableRows = table.getRowModel().rows
+
+    // 监听滚动以检测哪些行在视口中
+    useEffect(() => {
+        if (!animatedRows) return
+
+        // 仅在表格数据变化时清空引用数组,不重新分配
+        if (rowsRef.current.length > tableRows.length) {
+            rowsRef.current.length = tableRows.length
+        }
+
+        const observer = new IntersectionObserver(
+            (entries) => {
+                entries.forEach(entry => {
+                    const rowId = entry.target.getAttribute('data-row-id')
+                    if (rowId) {
+                        setInViewRows(prev => {
+                            const updated = new Set(prev)
+                            if (entry.isIntersecting) {
+                                updated.add(rowId)
+                            }
+                            return updated
+                        })
+                    }
+                })
+            },
+            { threshold: 0.1 }
+        )
+
+        // 直接观察现有行
+        rowsRef.current.forEach(row => {
+            if (row) observer.observe(row)
+        })
+
+        return () => {
+            observer.disconnect()
+        }
+    }, [tableRows, animatedRows])
+
+    // 渲染表格主体内容
+    const renderTableBody = () => {
+        if (isLoading) {
+            // 根据 loadingStyle 选项决定使用哪种加载动画
+            return loadingStyle === 'centered'
+                ? <CenteredLoader<TData, TValue> columns={columns} />
+                : <TableSkeleton<TData, TValue> columns={columns} rows={loadingRows} />
+        }
+
+        if (!table.getRowModel().rows?.length) {
+            return <NoResults<TData, TValue> columns={columns} />
+        }
+
+        return table.getRowModel().rows.map((row, rowIndex) => {
+            const isInView = inViewRows.has(row.id) || !animatedRows
+
+            return (
+                <TableRow
+                    key={row.id}
+                    data-row-id={row.id}
+                    data-state={row.getIsSelected() && "selected"}
+                    ref={el => {
+                        if (el && animatedRows) {
+                            rowsRef.current[rowIndex] = el
+                        }
+                    }}
+                    className={cn(
+                        animatedRows && "transition-opacity duration-300",
+                        animatedRows && !isInView ? "opacity-0" : "opacity-100"
+                    )}
+                    style={{
+                        transitionDelay: animatedRows ? `${rowIndex * 30}ms` : '0ms'
+                    }}
+                >
+                    {row.getVisibleCells().map((cell) => (
+                        <TableCell key={cell.id}>
+                            {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                        </TableCell>
+                    ))}
+                </TableRow>
+            )
+        })
+    }
+
+    // 表头渲染函数
+    const renderTableHeader = () => (
+        <TableHeader className={fixedHeader ? "sticky top-0 z-10 bg-background border-b" : ""}>
+            {table.getHeaderGroups().map((headerGroup) => (
+                <TableRow key={headerGroup.id}>
+                    {headerGroup.headers.map((header) => (
+                        <TableHead key={header.id}>
+                            {header.isPlaceholder
+                                ? null
+                                : flexRender(
+                                    header.column.columnDef.header,
+                                    header.getContext()
+                                )}
+                        </TableHead>
+                    ))}
+                </TableRow>
+            ))}
+        </TableHeader>
+    )
+
+    // 使用滚动容器
+    const renderScrollableTable = () => (
+        <TableScrollContainer showShadows={showScrollShadows}>
+            <table className="w-full caption-bottom text-sm">
+                {renderTableHeader()}
+                <tbody className={cn(
+                    // 只有当isLoading为true且没有行数据时才移除最后一行的边框
+                    (isLoading || !table.getRowModel().rows?.length) ? "[&_tr:last-child]:border-0" : ""
+                )}>
+                    {renderTableBody()}
+                </tbody>
+            </table>
+        </TableScrollContainer>
+    )
+
+    // 根据样式选择和固定表头选项构建表格
+    if (fixedHeader) {
+        // 使用固定表头的布局结构
+        return (
+            <div className={cn(
+                "w-full h-full relative",
+                style === 'border' && "rounded-md border"
+            )}>
+                {renderScrollableTable()}
+            </div>
+        )
+    }
+
+    // 原始表格布局(无固定表头)
+    switch (style) {
+        case 'simple':
+            return (
+                <div className="w-full h-full">
+                    <TableScrollContainer showShadows={showScrollShadows}>
+                        <Table>
+                            {renderTableHeader()}
+                            <TableBody>
+                                {renderTableBody()}
+                            </TableBody>
+                        </Table>
+                    </TableScrollContainer>
+                </div>
+            )
+
+        case 'border':
+            return (
+                <div className="rounded-md border h-full w-full">
+                    <TableScrollContainer showShadows={showScrollShadows}>
+                        <Table>
+                            {renderTableHeader()}
+                            <TableBody>
+                                {renderTableBody()}
+                            </TableBody>
+                        </Table>
+                    </TableScrollContainer>
+                </div>
+            )
+
+        default:
+            return (
+                <div className="w-full h-full">
+                    <TableScrollContainer showShadows={showScrollShadows}>
+                        <Table>
+                            {renderTableHeader()}
+                            <TableBody className={isLoading || !table.getRowModel().rows?.length ? "[&_tr:last-child]:!border-b-0" : ""}>
+                                {renderTableBody()}
+                            </TableBody>
+                        </Table>
+                    </TableScrollContainer>
+                </div>
+            )
+    }
+}

+ 103 - 0
web/src/components/table/pagination.tsx

@@ -0,0 +1,103 @@
+import { Table } from "@tanstack/react-table"
+import {
+    ChevronLeft,
+    ChevronRight,
+    ChevronsLeft,
+    ChevronsRight,
+} from "lucide-react"
+
+import { Button } from "@/components/ui/button"
+import {
+    Select,
+    SelectContent,
+    SelectItem,
+    SelectTrigger,
+    SelectValue,
+} from "@/components/ui/select"
+import { useTranslation } from "react-i18next"
+
+interface DataTablePaginationProps<TData> {
+    table: Table<TData>
+}
+
+export function DataTablePagination<TData>({
+    table,
+}: DataTablePaginationProps<TData>) {
+    const { t } = useTranslation()
+    return (
+        <div className="flex items-center justify-between px-2">
+            <div className="flex-1 text-sm text-muted-foreground whitespace-nowrap">
+                {t('table.selected', {
+                    selected: table.getFilteredSelectedRowModel().rows.length,
+                    total: table.getFilteredRowModel().rows.length
+                })}
+            </div>
+            <div className="flex items-center space-x-6 lg:space-x-8">
+                <div className="flex items-center space-x-2">
+                    <p className="text-sm font-medium whitespace-nowrap">{t('table.rowsPerPage')}</p>
+                    <Select
+                        value={`${table.getState().pagination.pageSize}`}
+                        onValueChange={(value) => {
+                            table.setPageSize(Number(value))
+                        }}
+                    >
+                        <SelectTrigger className="h-8 w-[70px]">
+                            <SelectValue placeholder={table.getState().pagination.pageSize} />
+                        </SelectTrigger>
+                        <SelectContent side="top">
+                            {[10, 20, 30, 40, 50].map((pageSize) => (
+                                <SelectItem key={pageSize} value={`${pageSize}`}>
+                                    {pageSize}
+                                </SelectItem>
+                            ))}
+                        </SelectContent>
+                    </Select>
+                </div>
+                <div className="flex w-[100px] items-center justify-center text-sm font-medium whitespace-nowrap">
+                    {t('table.pageInfo', {
+                        current: table.getState().pagination.pageIndex + 1,
+                        total: table.getPageCount()
+                    })}
+                </div>
+                <div className="flex items-center space-x-2">
+                    <Button
+                        variant="outline"
+                        className="hidden h-8 w-8 p-0 lg:flex"
+                        onClick={() => table.setPageIndex(0)}
+                        disabled={!table.getCanPreviousPage()}
+                        aria-label={t('table.firstPage')}
+                    >
+                        <ChevronsLeft className="h-4 w-4" />
+                    </Button>
+                    <Button
+                        variant="outline"
+                        className="h-8 w-8 p-0"
+                        onClick={() => table.previousPage()}
+                        disabled={!table.getCanPreviousPage()}
+                        aria-label={t('table.previousPage')}
+                    >
+                        <ChevronLeft className="h-4 w-4" />
+                    </Button>
+                    <Button
+                        variant="outline"
+                        className="h-8 w-8 p-0"
+                        onClick={() => table.nextPage()}
+                        disabled={!table.getCanNextPage()}
+                        aria-label={t('table.nextPage')}
+                    >
+                        <ChevronRight className="h-4 w-4" />
+                    </Button>
+                    <Button
+                        variant="outline"
+                        className="hidden h-8 w-8 p-0 lg:flex"
+                        onClick={() => table.setPageIndex(table.getPageCount() - 1)}
+                        disabled={!table.getCanNextPage()}
+                        aria-label={t('table.lastPage')}
+                    >
+                        <ChevronsRight className="h-4 w-4" />
+                    </Button>
+                </div>
+            </div>
+        </div>
+    )
+}

+ 155 - 0
web/src/components/ui/alert-dialog.tsx

@@ -0,0 +1,155 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+function AlertDialog({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
+  return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
+}
+
+function AlertDialogTrigger({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
+  return (
+    <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
+  )
+}
+
+function AlertDialogPortal({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
+  return (
+    <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
+  )
+}
+
+function AlertDialogOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
+  return (
+    <AlertDialogPrimitive.Overlay
+      data-slot="alert-dialog-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
+  return (
+    <AlertDialogPortal>
+      <AlertDialogOverlay />
+      <AlertDialogPrimitive.Content
+        data-slot="alert-dialog-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
+          className
+        )}
+        {...props}
+      />
+    </AlertDialogPortal>
+  )
+}
+
+function AlertDialogHeader({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-dialog-header"
+      className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogFooter({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-dialog-footer"
+      className={cn(
+        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
+  return (
+    <AlertDialogPrimitive.Title
+      data-slot="alert-dialog-title"
+      className={cn("text-lg font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
+  return (
+    <AlertDialogPrimitive.Description
+      data-slot="alert-dialog-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogAction({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
+  return (
+    <AlertDialogPrimitive.Action
+      className={cn(buttonVariants(), className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogCancel({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
+  return (
+    <AlertDialogPrimitive.Cancel
+      className={cn(buttonVariants({ variant: "outline" }), className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  AlertDialog,
+  AlertDialogPortal,
+  AlertDialogOverlay,
+  AlertDialogTrigger,
+  AlertDialogContent,
+  AlertDialogHeader,
+  AlertDialogFooter,
+  AlertDialogTitle,
+  AlertDialogDescription,
+  AlertDialogAction,
+  AlertDialogCancel,
+}

+ 66 - 0
web/src/components/ui/alert.tsx

@@ -0,0 +1,66 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+  "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+  {
+    variants: {
+      variant: {
+        default: "bg-card text-card-foreground",
+        destructive:
+          "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+    },
+  }
+)
+
+function Alert({
+  className,
+  variant,
+  ...props
+}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
+  return (
+    <div
+      data-slot="alert"
+      role="alert"
+      className={cn(alertVariants({ variant }), className)}
+      {...props}
+    />
+  )
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-title"
+      className={cn(
+        "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function AlertDescription({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-description"
+      className={cn(
+        "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Alert, AlertTitle, AlertDescription }

+ 150 - 0
web/src/components/ui/animation/button-animation.ts

@@ -0,0 +1,150 @@
+// src/components/ui/animation/button-animation.ts
+import { HTMLMotionProps } from "motion/react"
+
+// 标准按钮动画 - 流畅的序列缩放效果
+export const buttonAnimation: HTMLMotionProps<"div"> = {
+    whileHover: {
+        scale: [null, 1.05, 1.02],
+        transition: {
+            duration: 0.4,
+            times: [0, 0.6, 1],
+            ease: ["easeInOut", "easeOut"],
+        },
+    },
+    whileTap: {
+        scale: 0.95,
+        transition: {
+            duration: 0.1,
+            ease: "easeIn",
+        }
+    },
+    // 添加触摸板轻触反馈
+    transition: {
+        duration: 0.3,
+        ease: "easeOut",
+        type: "spring", // 使用弹簧动画提高触摸反馈
+        stiffness: 400,
+        damping: 17
+    }
+}
+
+// 主要按钮动画 - 带有阴影增强的效果
+export const primaryButtonAnimation: HTMLMotionProps<"div"> = {
+    whileHover: {
+        scale: [null, 1.08, 1.03],
+        boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
+        transition: {
+            duration: 0.5,
+            times: [0, 0.6, 1],
+            ease: ["easeInOut", "easeOut"],
+        },
+    },
+    whileTap: {
+        scale: 0.95,
+        boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)",
+        transition: {
+            duration: 0.1,
+            ease: "easeIn",
+            // 触摸板优化参数
+            type: "spring",
+            stiffness: 600,
+            damping: 25
+        }
+    },
+    transition: {
+        duration: 0.3,
+        ease: "easeOut",
+        type: "spring",
+        stiffness: 400,
+        damping: 17
+    }
+}
+
+// 次要按钮动画 - 更微妙的效果
+export const secondaryButtonAnimation: HTMLMotionProps<"div"> = {
+    whileHover: {
+        scale: [null, 1.04, 1.02],
+        transition: {
+            duration: 0.3,
+            times: [0, 0.5, 1],
+            ease: ["easeInOut", "easeOut"],
+        },
+    },
+    whileTap: {
+        scale: 0.97,
+        transition: {
+            duration: 0.1,
+            ease: "easeIn",
+            // 为触摸板响应优化
+            type: "spring",
+            stiffness: 500,
+            damping: 20
+        }
+    },
+    transition: {
+        duration: 0.3,
+        ease: "easeOut",
+        type: "spring",
+        stiffness: 350,
+        damping: 15
+    }
+}
+
+// 幽灵按钮动画 - 轻微效果
+export const ghostButtonAnimation: HTMLMotionProps<"div"> = {
+    whileHover: {
+        scale: 1.03,
+        transition: {
+            duration: 0.3,
+            ease: "easeOut",
+        },
+    },
+    whileTap: {
+        scale: 0.98,
+        transition: {
+            duration: 0.1,
+            ease: "easeIn",
+            // 更敏感的轻触反馈
+            type: "spring",
+            stiffness: 450,
+            damping: 15
+        }
+    },
+    transition: {
+        duration: 0.3,
+        ease: "easeOut",
+        type: "spring",
+        stiffness: 300,
+        damping: 12
+    }
+}
+
+// 破坏性操作按钮动画 - 更强调的警告效果
+export const destructiveButtonAnimation: HTMLMotionProps<"div"> = {
+    whileHover: {
+        scale: [null, 1.06, 1.03],
+        transition: {
+            duration: 0.4,
+            times: [0, 0.5, 1],
+            ease: ["easeInOut", "easeOut"],
+        },
+    },
+    whileTap: {
+        // 碎裂消失效果
+        scale: [0.95, 0.75, 0],
+        opacity: [1, 0.7, 0],
+        x: [0, 5, -5], // 左右轻微抖动
+        transition: {
+            duration: 0.4,
+            times: [0, 0.5, 1],
+            ease: "easeOut",
+        }
+    },
+    transition: {
+        duration: 0.3,
+        ease: "easeOut",
+        type: "spring",
+        stiffness: 450,
+        damping: 18
+    }
+}

+ 255 - 0
web/src/components/ui/animation/collapse-animation.ts

@@ -0,0 +1,255 @@
+// src/components/ui/animations/collapse-animations.tsx
+import { HTMLMotionProps } from "motion/react"
+
+// 优化基础折叠/展开动画,避免布局跳动
+export const collapseAnimation: HTMLMotionProps<"div"> = {
+    initial: "collapsed",
+    animate: "open",
+    exit: "collapsed",
+    variants: {
+        open: {
+            height: "auto",
+            opacity: 1,
+            marginBottom: 16, // 增加展开状态下的底部边距以预留空间
+            transition: {
+                height: {
+                    duration: 0.35, // 略微增加持续时间使过渡更平滑
+                    ease: [0.33, 1, 0.68, 1]
+                },
+                opacity: {
+                    duration: 0.25,
+                    delay: 0.1 // 增加不透明度变化的延迟
+                },
+                marginBottom: {
+                    duration: 0.35,
+                    ease: [0.33, 1, 0.68, 1]
+                }
+            }
+        },
+        collapsed: {
+            height: 0,
+            opacity: 0,
+            marginBottom: 0,
+            transition: {
+                height: {
+                    duration: 0.3,
+                    ease: [0.33, 1, 0.68, 1]
+                },
+                opacity: {
+                    duration: 0.2,
+                    ease: "easeIn"
+                },
+                marginBottom: {
+                    duration: 0.3,
+                    ease: [0.33, 1, 0.68, 1]
+                }
+            }
+        }
+    },
+    style: {
+        overflow: "hidden",
+        position: "relative",
+        willChange: "height, opacity, margin",
+        transformOrigin: "top center", // 确保任何变换都从顶部开始
+        minHeight: 0 // 确保折叠时可以完全收起
+    }
+}
+
+// 带缩放效果的折叠动画,优化以减少视觉跳动
+export const collapseScaleAnimation: HTMLMotionProps<"div"> = {
+    initial: "collapsed",
+    animate: "open",
+    exit: "collapsed",
+    variants: {
+        open: {
+            height: "auto",
+            opacity: 1,
+            scale: 1,
+            marginBottom: 16, // 添加展开状态的边距
+            transformOrigin: "top center",
+            y: 0, // 保持垂直位置不变
+            transition: {
+                height: {
+                    duration: 0.35,
+                    ease: [0.33, 1, 0.68, 1]
+                },
+                scale: {
+                    duration: 0.25,
+                    delay: 0.05
+                },
+                opacity: {
+                    duration: 0.25,
+                    delay: 0.1
+                },
+                marginBottom: {
+                    duration: 0.35
+                },
+                y: {
+                    duration: 0.25
+                }
+            }
+        },
+        collapsed: {
+            height: 0,
+            opacity: 0,
+            scale: 0.95, // 稍微调整以减少视觉跳动
+            marginBottom: 0,
+            transformOrigin: "top center",
+            y: -5, // 轻微向上位移以更自然地消失
+            transition: {
+                height: {
+                    duration: 0.3,
+                    ease: [0.33, 1, 0.68, 1]
+                },
+                scale: {
+                    duration: 0.25
+                },
+                opacity: {
+                    duration: 0.2
+                },
+                marginBottom: {
+                    duration: 0.3
+                },
+                y: {
+                    duration: 0.2
+                }
+            }
+        }
+    },
+    style: {
+        overflow: "hidden",
+        position: "relative",
+        willChange: "height, opacity, margin, transform",
+        minHeight: 0,
+        isolation: "isolate" // 创建新的层叠上下文,减少重绘区域
+    }
+}
+
+// 轻度动效的展开动画,优化以减少视觉跳动
+export const collapseLightAnimation: HTMLMotionProps<"div"> = {
+    initial: "collapsed",
+    animate: "open",
+    exit: "collapsed",
+    variants: {
+        open: {
+            height: "auto",
+            opacity: 1,
+            marginBottom: 12, // 提供适当的边距
+            y: 0,
+            transition: {
+                height: {
+                    duration: 0.25,
+                    ease: [0.33, 1, 0.68, 1]
+                },
+                opacity: {
+                    duration: 0.2,
+                    delay: 0.05
+                },
+                marginBottom: {
+                    duration: 0.25
+                },
+                y: {
+                    duration: 0.2
+                }
+            }
+        },
+        collapsed: {
+            height: 0,
+            opacity: 0,
+            marginBottom: 0,
+            y: -3, // 轻微向上位移
+            transition: {
+                height: {
+                    duration: 0.25,
+                    ease: [0.33, 1, 0.68, 1]
+                },
+                opacity: {
+                    duration: 0.15
+                },
+                marginBottom: {
+                    duration: 0.25
+                },
+                y: {
+                    duration: 0.15
+                }
+            }
+        }
+    },
+    style: {
+        overflow: "hidden",
+        position: "relative",
+        willChange: "height, opacity, margin, transform",
+        minHeight: 0,
+        transformOrigin: "top center"
+    }
+}
+
+// 新增一个用于列表项的动画,特别适合添加/删除服务器的场景
+export const listItemAnimation: HTMLMotionProps<"div"> = {
+    initial: "collapsed",
+    animate: "open",
+    exit: "collapsed",
+    layout: true, // 启用布局动画
+    variants: {
+        open: {
+            height: "auto",
+            opacity: 1,
+            scale: 1,
+            y: 0,
+            marginBottom: 16,
+            transition: {
+                height: {
+                    duration: 0.35,
+                    ease: [0.33, 1, 0.68, 1]
+                },
+                opacity: {
+                    duration: 0.25,
+                    delay: 0.05
+                },
+                scale: {
+                    duration: 0.25,
+                    delay: 0.05
+                },
+                y: {
+                    duration: 0.25
+                },
+                marginBottom: {
+                    duration: 0.35
+                }
+            }
+        },
+        collapsed: {
+            height: 0,
+            opacity: 0,
+            scale: 0.98,
+            y: -10,
+            marginBottom: 0,
+            transition: {
+                height: {
+                    duration: 0.3,
+                    ease: [0.33, 1, 0.68, 1]
+                },
+                opacity: {
+                    duration: 0.2
+                },
+                scale: {
+                    duration: 0.2
+                },
+                y: {
+                    duration: 0.2
+                },
+                marginBottom: {
+                    duration: 0.3
+                }
+            }
+        }
+    },
+    style: {
+        overflow: "hidden",
+        position: "relative",
+        willChange: "height, opacity, margin, transform",
+        minHeight: 0,
+        transformOrigin: "top center",
+        zIndex: 1
+    }
+}

+ 55 - 0
web/src/components/ui/animation/components/animated-button.tsx

@@ -0,0 +1,55 @@
+// src/components/ui/animation/components/animated-button.tsx
+import { motion } from "motion/react"
+import React, { forwardRef } from "react"
+import {
+    buttonAnimation,
+    primaryButtonAnimation,
+    secondaryButtonAnimation,
+    ghostButtonAnimation,
+    destructiveButtonAnimation
+} from "../button-animation"
+
+interface AnimatedButtonProps {
+    children: React.ReactNode
+    className?: string
+    animationVariant?: "default" | "primary" | "secondary" | "ghost" | "destructive"
+}
+
+export const AnimatedButton = forwardRef<HTMLDivElement, AnimatedButtonProps>(
+    ({
+        children,
+        className = "",
+        animationVariant = "default",
+        ...props
+    }, ref) => {
+        // 根据动画变体选择动画属性
+        const getAnimationProps = () => {
+            switch (animationVariant) {
+                case "primary":
+                    return primaryButtonAnimation
+                case "secondary":
+                    return secondaryButtonAnimation
+                case "ghost":
+                    return ghostButtonAnimation
+                case "destructive":
+                    return destructiveButtonAnimation
+                case "default":
+                default:
+                    return buttonAnimation
+            }
+        }
+
+        return (
+            <motion.div
+                ref={ref}
+                className={className}
+                {...getAnimationProps()}
+                {...props}
+            >
+                {children}
+            </motion.div>
+        )
+    }
+)
+
+AnimatedButton.displayName = "AnimatedButton"

+ 57 - 0
web/src/components/ui/animation/components/animated-container.tsx

@@ -0,0 +1,57 @@
+import { motion } from "motion/react"
+import React, { forwardRef } from "react"
+import {
+    containerAnimation,
+    smoothContainerAnimation,
+    scrollContainerAnimation,
+    layoutRootAnimation
+} from "../container-animation"
+
+interface AnimatedContainerProps {
+    children: React.ReactNode
+    className?: string
+    variant?: "default" | "smooth" | "scroll" | "root"
+    layoutId?: string
+    layoutDependency?: unknown
+}
+
+export const AnimatedContainer = forwardRef<HTMLDivElement, AnimatedContainerProps>(
+    ({
+        children,
+        className = "",
+        variant = "default",
+        layoutId,
+        layoutDependency,
+        ...props
+    }, ref) => {
+        // 根据变体选择动画属性
+        const getAnimationProps = () => {
+            switch (variant) {
+                case "smooth":
+                    return smoothContainerAnimation
+                case "scroll":
+                    return scrollContainerAnimation
+                case "root":
+                    return layoutRootAnimation
+                case "default":
+                default:
+                    return containerAnimation
+            }
+        }
+
+        return (
+            <motion.div
+                ref={ref}
+                className={className}
+                layoutId={layoutId}
+                layoutDependency={layoutDependency}
+                {...getAnimationProps()}
+                {...props}
+            >
+                {children}
+            </motion.div>
+        )
+    }
+)
+
+AnimatedContainer.displayName = "AnimatedContainer" 

+ 69 - 0
web/src/components/ui/animation/components/animated-icon.tsx

@@ -0,0 +1,69 @@
+// src/components/ui/animation/components/animated-icon.tsx
+import { motion } from "motion/react"
+import React, { forwardRef } from "react"
+import {
+    spinIconAnimation,
+    continuousSpinAnimation,
+    shakeIconAnimation,
+    bounceIconAnimation,
+    pulseIconAnimation,
+    glowIconAnimation
+} from "../icon-animation"
+
+interface AnimatedIconProps {
+    children: React.ReactNode
+    className?: string
+    animationVariant?: "spin" | "continuous-spin" | "shake" | "bounce" | "pulse" | "glow"
+    isAnimating?: boolean // 用于控制连续动画
+    onClick?: () => void
+}
+
+export const AnimatedIcon = forwardRef<HTMLDivElement, AnimatedIconProps>(
+    ({
+        children,
+        className = "",
+        animationVariant = "spin",
+        isAnimating = false,
+        onClick,
+        ...props
+    }, ref) => {
+        // 根据动画变体选择动画属性
+        const getAnimationProps = () => {
+            // 对于连续旋转,根据 isAnimating 决定是否应用动画
+            if (animationVariant === "continuous-spin" && isAnimating) {
+                return continuousSpinAnimation
+            }
+
+            // 对于其他交互动画,根据变体选择
+            switch (animationVariant) {
+                case "continuous-spin":
+                case "spin":
+                    return spinIconAnimation
+                case "shake":
+                    return shakeIconAnimation
+                case "bounce":
+                    return bounceIconAnimation
+                case "pulse":
+                    return pulseIconAnimation
+                case "glow":
+                    return glowIconAnimation
+                default:
+                    return spinIconAnimation
+            }
+        }
+
+        return (
+            <motion.div
+                ref={ref}
+                className={className}
+                {...getAnimationProps()}
+                onClick={onClick}
+                {...props}
+            >
+                {children}
+            </motion.div>
+        )
+    }
+)
+
+AnimatedIcon.displayName = "AnimatedIcon"

+ 46 - 0
web/src/components/ui/animation/components/collapse.tsx

@@ -0,0 +1,46 @@
+import { AnimatePresence, motion } from "motion/react"
+import React from "react"
+import { collapseAnimation, collapseScaleAnimation, collapseLightAnimation } from "../collapse-animation"
+
+interface CollapseProps {
+    isOpen: boolean
+    children: React.ReactNode
+    className?: string
+    animationType?: "default" | "scale" | "light"
+    initial?: boolean
+}
+
+export function Collapse({
+    isOpen,
+    children,
+    className = "",
+    animationType = "default",
+    initial = false
+}: CollapseProps) {
+    // Select the appropriate animation based on the animationType
+    const getAnimationProps = () => {
+        switch (animationType) {
+            case "scale":
+                return collapseScaleAnimation
+            case "light":
+                return collapseLightAnimation
+            case "default":
+            default:
+                return collapseAnimation
+        }
+    }
+
+    return (
+        <AnimatePresence initial={initial}>
+            {isOpen && (
+                <motion.div
+                    key="collapse-content"
+                    className={className}
+                    {...getAnimationProps()}
+                >
+                    {children}
+                </motion.div>
+            )}
+        </AnimatePresence>
+    )
+}

+ 48 - 0
web/src/components/ui/animation/components/display.tsx

@@ -0,0 +1,48 @@
+import { AnimatePresence, motion } from "motion/react"
+import React from "react"
+import { fadeAnimation, slideAnimation, scaleAnimation } from "../display-animation"
+
+interface DisplayProps {
+    visible: boolean
+    children: React.ReactNode
+    className?: string
+    animationType?: "fade" | "slide" | "scale"
+    initial?: boolean
+    mode?: "sync" | "wait" | "popLayout"
+}
+
+export function Display({
+    visible,
+    children,
+    className = "",
+    animationType = "fade",
+    initial = false,
+    mode = "sync"
+}: DisplayProps) {
+    // 根据动画类型选择适当的动画属性
+    const getAnimationProps = () => {
+        switch (animationType) {
+            case "slide":
+                return slideAnimation
+            case "scale":
+                return scaleAnimation
+            case "fade":
+            default:
+                return fadeAnimation
+        }
+    }
+
+    return (
+        <AnimatePresence initial={initial} mode={mode}>
+            {visible && (
+                <motion.div
+                    key="display-content"
+                    className={className}
+                    {...getAnimationProps()}
+                >
+                    {children}
+                </motion.div>
+            )}
+        </AnimatePresence>
+    )
+} 

+ 139 - 0
web/src/components/ui/animation/components/particles-background.tsx

@@ -0,0 +1,139 @@
+import { useEffect, useRef } from 'react'
+import { cn } from '@/lib/utils'
+
+interface ParticlesBackgroundProps {
+  className?: string
+  particleColor?: string
+  particleSize?: number
+  particleCount?: number
+  speed?: number
+}
+
+export function ParticlesBackground({
+  className,
+  particleColor = "rgba(26, 99, 212, 0.1)",
+  particleSize = 6,
+  particleCount = 25,
+  speed = 1
+}: ParticlesBackgroundProps) {
+  const canvasRef = useRef<HTMLCanvasElement>(null)
+
+  useEffect(() => {
+    const canvas = canvasRef.current
+    if (!canvas) return
+
+    const ctx = canvas.getContext('2d')
+    if (!ctx) return
+
+    let animationFrameId: number
+    const particles: {
+      x: number
+      y: number
+      size: number
+      baseSize: number
+      speedX: number
+      speedY: number
+      opacity: number
+      baseOpacity: number
+      rotation: number
+      rotationSpeed: number
+      pulseSpeed: number
+      pulseAmount: number
+      pulseOffset: number
+    }[] = []
+
+    // 设置canvas尺寸为窗口大小
+    const resizeCanvas = () => {
+      if (canvas && canvas.parentElement) {
+        canvas.width = canvas.parentElement.offsetWidth
+        canvas.height = canvas.parentElement.offsetHeight
+
+        // 重新初始化粒子
+        initParticles()
+      }
+    }
+
+    // 初始化粒子
+    const initParticles = () => {
+      particles.length = 0
+      for (let i = 0; i < particleCount; i++) {
+        const baseSize = Math.random() * particleSize + particleSize / 2
+        const baseOpacity = Math.random() * 0.4 + 0.2
+
+        particles.push({
+          x: Math.random() * canvas.width,
+          y: Math.random() * canvas.height,
+          baseSize: baseSize,
+          size: baseSize,
+          speedX: (Math.random() - 0.5) * speed,
+          speedY: (Math.random() - 0.5) * speed,
+          baseOpacity: baseOpacity,
+          opacity: baseOpacity,
+          rotation: Math.random() * 360,
+          rotationSpeed: (Math.random() - 0.5) * 0.5,
+          pulseSpeed: Math.random() * 0.02 + 0.01,
+          pulseAmount: Math.random() * 0.5 + 0.5,
+          pulseOffset: Math.random() * Math.PI * 2 // 随机相位偏移
+        })
+      }
+    }
+
+    // 绘制粒子
+    const drawParticles = () => {
+      ctx.clearRect(0, 0, canvas.width, canvas.height)
+
+      const now = Date.now() / 1000 // 当前时间(秒)用于动画
+
+      particles.forEach(particle => {
+        // 呼吸效果 - 大小和透明度随时间变化
+        const pulse = Math.sin(now * particle.pulseSpeed + particle.pulseOffset) * particle.pulseAmount
+        particle.size = particle.baseSize * (1 + pulse * 0.2)
+        particle.opacity = particle.baseOpacity * (1 + pulse * 0.1)
+
+        ctx.save()
+        ctx.translate(particle.x + particle.size / 2, particle.y + particle.size / 2)
+        ctx.rotate((particle.rotation * Math.PI) / 180)
+
+        // 设置方块颜色和透明度
+        ctx.fillStyle = particleColor.replace(/rgba?\(([^)]+)\)/,
+          (_, p) => `rgba(${p.split(',').slice(0, 3).join(',')}, ${particle.opacity})`)
+
+        // 绘制方块
+        ctx.fillRect(-particle.size / 2, -particle.size / 2, particle.size, particle.size)
+
+        ctx.restore()
+
+        // 更新粒子位置
+        particle.x += particle.speedX
+        particle.y += particle.speedY
+        particle.rotation += particle.rotationSpeed
+
+        // 边界检测与循环
+        if (particle.x < -particle.size) particle.x = canvas.width
+        if (particle.x > canvas.width) particle.x = -particle.size
+        if (particle.y < -particle.size) particle.y = canvas.height
+        if (particle.y > canvas.height) particle.y = -particle.size
+      })
+
+      animationFrameId = requestAnimationFrame(drawParticles)
+    }
+
+    // 初始化和启动动画
+    window.addEventListener('resize', resizeCanvas)
+    resizeCanvas()
+    drawParticles()
+
+    // 清理
+    return () => {
+      window.removeEventListener('resize', resizeCanvas)
+      cancelAnimationFrame(animationFrameId)
+    }
+  }, [particleColor, particleSize, particleCount, speed])
+
+  return (
+    <canvas
+      ref={canvasRef}
+      className={cn("absolute inset-0 w-full h-full pointer-events-none z-0", className)}
+    />
+  )
+} 

+ 39 - 0
web/src/components/ui/animation/components/tab-animation.tsx

@@ -0,0 +1,39 @@
+import { tabFadeAnimation, tabScaleAnimation, tabContentAnimation } from "../tab-animation"
+import React from "react"
+import { AnimatePresence, motion } from "motion/react"
+
+interface TabsAnimationProviderProps {
+    children: React.ReactNode
+    currentView: string
+    animationVariant?: "slide" | "fade" | "scale"
+}
+
+export function TabsAnimationProvider({
+    children,
+    currentView,
+    animationVariant = "slide"
+}: TabsAnimationProviderProps) {
+    // 根据选择的变体选择相应的动画
+    const getAnimationProps = () => {
+        switch (animationVariant) {
+            case "fade":
+                return tabFadeAnimation
+            case "scale":
+                return tabScaleAnimation
+            case "slide":
+            default:
+                return tabContentAnimation
+        }
+    }
+
+    return (
+        <AnimatePresence mode="wait">
+            <motion.div
+                key={currentView}
+                {...getAnimationProps()}
+            >
+                {children}
+            </motion.div>
+        </AnimatePresence>
+    )
+}

+ 82 - 0
web/src/components/ui/animation/components/table-scroll.tsx

@@ -0,0 +1,82 @@
+import { ReactNode, useRef, useEffect, useState } from "react"
+import { motion, useScroll, useTransform } from "motion/react"
+import { containerAnimation } from "../container-animation"
+
+interface TableScrollContainerProps {
+    children: ReactNode
+    className?: string
+    showShadows?: boolean
+    shadowOpacity?: number
+}
+
+export function TableScrollContainer({
+    children,
+    className = "",
+    showShadows = true,
+    shadowOpacity = 0.15
+}: TableScrollContainerProps) {
+    const containerRef = useRef<HTMLDivElement>(null)
+    const { scrollYProgress } = useScroll({ container: containerRef })
+    const [canScroll, setCanScroll] = useState(false)
+
+    // 顶部和底部阴影透明度
+    const topShadowOpacity = useTransform(scrollYProgress, [0, 0.1], [0, shadowOpacity])
+    const bottomShadowOpacity = useTransform(scrollYProgress, [0.9, 1], [shadowOpacity, 0])
+
+    // 检查内容是否可滚动
+    useEffect(() => {
+        const checkScrollability = () => {
+            if (containerRef.current) {
+                const { scrollHeight, clientHeight } = containerRef.current
+                setCanScroll(scrollHeight > clientHeight)
+            }
+        }
+
+        checkScrollability()
+
+        // 添加窗口大小变化的监听
+        window.addEventListener('resize', checkScrollability)
+        return () => window.removeEventListener('resize', checkScrollability)
+    }, [])
+
+    return (
+        <div className={`relative w-full h-full ${className}`}>
+            {/* 顶部滚动阴影 */}
+            {showShadows && canScroll && (
+                <motion.div
+                    className="absolute top-0 left-0 right-0 h-4 pointer-events-none z-10"
+                    style={{
+                        opacity: topShadowOpacity,
+                        background: `linear-gradient(to bottom, rgba(0,0,0,${shadowOpacity}), transparent)`
+                    }}
+                />
+            )}
+
+            {/* 滚动容器 */}
+            <motion.div
+                ref={containerRef}
+                className="overflow-auto h-full w-full scroll-smooth"
+                style={{
+                    // 添加平滑滚动效果
+                    scrollBehavior: 'smooth',
+                    // 优化移动端滚动
+                    WebkitOverflowScrolling: 'touch'
+                }}
+                {...containerAnimation}
+            >
+                {children}
+            </motion.div>
+
+            {/* 底部滚动阴影 */}
+            {showShadows && canScroll && (
+                <motion.div
+                    className="absolute bottom-0 left-0 right-0 h-4 pointer-events-none z-10"
+                    style={{
+                        opacity: bottomShadowOpacity,
+                        background: `linear-gradient(to top, rgba(0,0,0,${shadowOpacity}), transparent)`
+                    }}
+                />
+            )}
+        </div>
+    )
+} 

+ 50 - 0
web/src/components/ui/animation/container-animation.ts

@@ -0,0 +1,50 @@
+import { HTMLMotionProps } from "motion/react"
+
+// 基础容器动画 - 使用弹性动画效果
+export const containerAnimation: HTMLMotionProps<"div"> = {
+    layout: true,
+    transition: {
+        layout: {
+            type: "spring",
+            stiffness: 300,
+            damping: 30
+        }
+    }
+}
+
+// 平滑容器动画 - 使用补间动画,更均匀的过渡
+export const smoothContainerAnimation: HTMLMotionProps<"div"> = {
+    layout: true,
+    transition: {
+        layout: {
+            duration: 0.4,
+            ease: [0.4, 0, 0.2, 1],
+            type: "tween"
+        }
+    }
+}
+
+// 优化的滚动容器动画 - 减少过渡时间,提高响应性
+export const scrollContainerAnimation: HTMLMotionProps<"div"> = {
+    layout: true,
+    layoutRoot: true,
+    transition: {
+        layout: {
+            duration: 0.25,
+            ease: [0.25, 0.1, 0.25, 1.0],
+            type: "tween"
+        }
+    }
+}
+
+// 高性能布局根动画 - 适用于复杂列表和表格
+export const layoutRootAnimation: HTMLMotionProps<"div"> = {
+    layout: true,
+    layoutRoot: true,
+    transition: {
+        layout: {
+            type: "tween",
+            duration: 0.2
+        }
+    }
+} 

+ 119 - 0
web/src/components/ui/animation/dialog-animation.ts

@@ -0,0 +1,119 @@
+import { HTMLMotionProps } from "motion/react"
+
+// 对话框进入和退出的容器动画
+export const dialogEnterExitAnimation: HTMLMotionProps<"div"> = {
+    className: "fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%]",
+    initial: "hidden",
+    animate: "visible",
+    exit: "exit",
+    variants: {
+        hidden: {
+            opacity: 0,
+            scale: 0.96,
+            y: 8
+        },
+        visible: {
+            opacity: 1,
+            scale: 1,
+            y: 0,
+            transition: {
+                duration: 0.3,
+                ease: [0.16, 1, 0.3, 1], // custom ease curve for natural feel
+                when: "beforeChildren"
+            }
+        },
+        exit: {
+            opacity: 0,
+            scale: 0.96,
+            y: -8,
+            transition: {
+                duration: 0.25,
+                ease: [0.32, 0, 0.67, 0], // easeInCubic for natural exit
+                when: "afterChildren"
+            }
+        }
+    }
+}
+
+// DialogContent 内容区域动画 - 更自然的涌动效果
+export const dialogContentAnimation: HTMLMotionProps<"div"> = {
+    variants: {
+        hidden: {
+            opacity: 0,
+        },
+        visible: {
+            opacity: 1,
+            transition: {
+                duration: 0.2,
+                ease: "easeOut",
+                staggerChildren: 0.06,
+                delayChildren: 0.05
+            }
+        },
+        exit: {
+            opacity: 0,
+            transition: {
+                duration: 0.15,
+                ease: "easeIn",
+                staggerChildren: 0.03,
+                staggerDirection: -1
+            }
+        }
+    }
+}
+
+// 内容涌动的子元素动画 - 更自然的流动感
+export const dialogContentItemAnimation: HTMLMotionProps<"div"> = {
+    variants: {
+        hidden: {
+            opacity: 0,
+            y: 12,
+            scale: 0.98
+        },
+        visible: {
+            opacity: 1,
+            y: 0,
+            scale: 1,
+            transition: {
+                type: "spring",
+                damping: 20, // 更高的阻尼使动画更自然不过度弹跳
+                stiffness: 260, // 适当的刚度平衡速度和流畅度
+                mass: 0.4 // 较轻的质量使动画更灵活
+            }
+        },
+        exit: {
+            opacity: 0,
+            y: -8,
+            transition: {
+                duration: 0.18,
+                ease: [0.32, 0, 0.67, 0] // easeInCubic for natural exit
+            }
+        }
+    }
+}
+
+// DialogHeader 标题区域动画
+export const dialogHeaderAnimation: HTMLMotionProps<"div"> = {
+    variants: {
+        hidden: {
+            opacity: 0,
+            y: -10
+        },
+        visible: {
+            opacity: 1,
+            y: 0,
+            transition: {
+                duration: 0.3,
+                ease: "easeOut"
+            }
+        },
+        exit: {
+            opacity: 0,
+            y: -5,
+            transition: {
+                duration: 0.2,
+                ease: "easeIn"
+            }
+        }
+    }
+} 

+ 122 - 0
web/src/components/ui/animation/display-animation.ts

@@ -0,0 +1,122 @@
+import { HTMLMotionProps } from "motion/react"
+
+// 淡入淡出动画
+export const fadeAnimation: HTMLMotionProps<"div"> = {
+    initial: "hidden",
+    animate: "visible",
+    exit: "hidden",
+    variants: {
+        visible: {
+            opacity: 1,
+            scale: 1,
+            transition: {
+                opacity: {
+                    duration: 0.3,
+                    ease: "easeOut"
+                },
+                scale: {
+                    duration: 0.25,
+                    ease: "easeOut"
+                }
+            }
+        },
+        hidden: {
+            opacity: 0,
+            scale: 0.98,
+            transition: {
+                opacity: {
+                    duration: 0.25,
+                    ease: "easeIn"
+                },
+                scale: {
+                    duration: 0.2,
+                    ease: "easeIn"
+                }
+            }
+        }
+    }
+}
+
+// 滑动动画
+export const slideAnimation: HTMLMotionProps<"div"> = {
+    initial: "hidden",
+    animate: "visible",
+    exit: "hidden",
+    variants: {
+        visible: {
+            opacity: 1,
+            x: 0,
+            transition: {
+                opacity: {
+                    duration: 0.3,
+                    ease: "easeOut"
+                },
+                x: {
+                    duration: 0.3,
+                    ease: [0.33, 1, 0.68, 1]
+                }
+            }
+        },
+        hidden: {
+            opacity: 0,
+            x: -20,
+            transition: {
+                opacity: {
+                    duration: 0.25,
+                    ease: "easeIn"
+                },
+                x: {
+                    duration: 0.25,
+                    ease: [0.33, 1, 0.68, 1]
+                }
+            }
+        }
+    }
+}
+
+// 缩放动画
+export const scaleAnimation: HTMLMotionProps<"div"> = {
+    initial: "hidden",
+    animate: "visible",
+    exit: "hidden",
+    variants: {
+        visible: {
+            opacity: 1,
+            scale: 1,
+            y: 0,
+            transition: {
+                opacity: {
+                    duration: 0.3,
+                    ease: "easeOut"
+                },
+                scale: {
+                    duration: 0.3,
+                    ease: [0.33, 1, 0.68, 1]
+                },
+                y: {
+                    duration: 0.3,
+                    ease: [0.33, 1, 0.68, 1]
+                }
+            }
+        },
+        hidden: {
+            opacity: 0,
+            scale: 0.95,
+            y: 10,
+            transition: {
+                opacity: {
+                    duration: 0.2,
+                    ease: "easeIn"
+                },
+                scale: {
+                    duration: 0.25,
+                    ease: [0.33, 1, 0.68, 1]
+                },
+                y: {
+                    duration: 0.25,
+                    ease: [0.33, 1, 0.68, 1]
+                }
+            }
+        }
+    }
+} 

+ 44 - 0
web/src/components/ui/animation/grid-animation.ts

@@ -0,0 +1,44 @@
+import { HTMLMotionProps } from "motion/react"
+
+// 布局动画配置 - 用于处理元素添加、删除和重排序的动画效果
+export const layoutAnimationProps = {
+    layout: true,
+    initial: { opacity: 0, scale: 0.8 },
+    animate: { opacity: 1, scale: 1 },
+    exit: { opacity: 0, scale: 0.8 },
+    transition: {
+        layout: {
+            type: "spring",
+            damping: 25,
+            stiffness: 300,
+            mass: 0.8
+        },
+        opacity: { duration: 0.3 },
+        scale: {
+            type: "spring",
+            damping: 15,
+            stiffness: 200
+        }
+    }
+}
+
+// 保留网格项目的动画配置,因为它在SiteCard中使用
+export const gridItemAnimation: HTMLMotionProps<"div"> = {
+    variants: {
+        initial: {
+            opacity: 0,
+            y: 20,
+            scale: 0.95
+        },
+        animate: {
+            opacity: 1,
+            y: 0,
+            scale: 1,
+            transition: {
+                type: "spring",
+                damping: 15,
+                stiffness: 200
+            }
+        }
+    }
+}

+ 170 - 0
web/src/components/ui/animation/icon-animation.ts

@@ -0,0 +1,170 @@
+// src/components/ui/animation/icon-animation.ts
+import { HTMLMotionProps } from "motion/react"
+// 旋转动画 - 适用于刷新、重置图标
+export const spinIconAnimation: HTMLMotionProps<"div"> = {
+    whileHover: {
+        scale: 1.05,
+        transition: {
+            type: "spring",
+            stiffness: 400,
+            damping: 10
+        }
+    },
+    whileTap: {
+        rotate: 360,
+        scale: 0.95,
+        transition: {
+            duration: 0.5,
+            ease: "easeInOut",
+            scale: {
+                type: "spring",
+                stiffness: 500,
+                damping: 15
+            }
+        }
+    },
+    transition: {
+        duration: 0.3,
+        ease: "easeOut",
+        type: "spring",
+    }
+}
+
+// 持续旋转动画 - 适用于加载状态
+export const continuousSpinAnimation: HTMLMotionProps<"div"> = {
+    animate: {
+        rotate: 360,
+        transition: {
+            duration: 1.5,
+            ease: "linear",
+            repeat: Infinity
+        }
+    },
+    // 点击时可暂时加快旋转速度
+    whileTap: {
+        scale: 0.95,
+        transition: {
+            type: "spring",
+            stiffness: 400,
+            damping: 10
+        }
+    }
+}
+
+// 震动动画 - 适用于通知、警告图标
+export const shakeIconAnimation: HTMLMotionProps<"div"> = {
+    whileHover: {
+        scale: 1.05,
+        transition: {
+            type: "spring",
+            stiffness: 400,
+            damping: 10
+        }
+    },
+    whileTap: {
+        rotate: [0, -12, 10, -6, 3, -2, 0],
+        scale: 0.95,
+        transition: {
+            duration: 0.7,
+            times: [0, 0.25, 0.5, 0.75, 0.85, 0.92, 1],
+            ease: "easeOut",
+            scale: {
+                type: "spring",
+                stiffness: 500,
+                damping: 15,
+                duration: 0.1
+            }
+        }
+    },
+    transition: {
+        type: "spring",
+        stiffness: 350,
+        damping: 15
+    }
+}
+
+// 弹跳动画 - 适用于交互按钮
+export const bounceIconAnimation: HTMLMotionProps<"div"> = {
+    whileHover: {
+        scale: 1.05,
+        y: -2,
+        transition: {
+            type: "spring",
+            stiffness: 400,
+            damping: 10
+        }
+    },
+    whileTap: {
+        y: [0, -8, 4, -2, 0],
+        scale: 0.95,
+        transition: {
+            duration: 0.5,
+            times: [0, 0.3, 0.6, 0.8, 1],
+            ease: "easeOut",
+            scale: {
+                type: "spring",
+                stiffness: 500,
+                damping: 15,
+                duration: 0.1
+            }
+        }
+    },
+    transition: {
+        type: "spring",
+        stiffness: 400,
+        damping: 12
+    }
+}
+
+// 脉冲动画 - 适用于强调图标
+export const pulseIconAnimation: HTMLMotionProps<"div"> = {
+    whileHover: {
+        scale: 1.05,
+        transition: {
+            type: "spring",
+            stiffness: 400,
+            damping: 10
+        }
+    },
+    whileTap: {
+        scale: [1, 1.3, 0.95, 1.05, 1],
+        transition: {
+            duration: 0.5,
+            times: [0, 0.2, 0.4, 0.6, 1],
+            ease: "easeOut"
+        }
+    },
+    transition: {
+        type: "spring",
+        stiffness: 380,
+        damping: 15
+    }
+}
+
+// 新增:通知点亮动画 - 轻触时有发光效果
+export const glowIconAnimation: HTMLMotionProps<"div"> = {
+    whileHover: {
+        scale: 1.08,
+        filter: "drop-shadow(0 0 3px rgba(255, 255, 255, 0.7))",
+        transition: {
+            type: "spring",
+            stiffness: 400,
+            damping: 10
+        }
+    },
+    whileTap: {
+        scale: 0.92,
+        filter: "drop-shadow(0 0 6px rgba(255, 255, 255, 0.9))",
+        transition: {
+            type: "spring",
+            stiffness: 500,
+            damping: 12,
+            duration: 0.2
+        }
+    },
+    transition: {
+        type: "spring",
+        stiffness: 400,
+        damping: 15
+    }
+}

+ 145 - 0
web/src/components/ui/animation/route-animation.ts

@@ -0,0 +1,145 @@
+import { HTMLMotionProps } from "motion/react"
+
+// 页面过渡动画配置 - 优化后
+export const pageTransitionAnimation: HTMLMotionProps<"div"> = {
+    className: "w-full h-full",
+    initial: "initial",
+    animate: "animate",
+    exit: "exit",
+    variants: {
+        initial: {
+            opacity: 0,
+            x: 15,
+            scale: 0.98
+        },
+        animate: {
+            opacity: 1,
+            x: 0,
+            scale: 1,
+            transition: {
+                opacity: { duration: 0.3, ease: [0.22, 1, 0.36, 1] },
+                x: { duration: 0.35, ease: [0.32, 0.72, 0, 1] },
+                scale: { duration: 0.35, ease: [0.34, 1.56, 0.64, 1] }
+            }
+        },
+        exit: {
+            opacity: 0,
+            x: -15,
+            scale: 0.96,
+            transition: {
+                opacity: { duration: 0.2, ease: "easeOut" },
+                x: { duration: 0.25, ease: "easeInOut" },
+                scale: { duration: 0.2, ease: "easeIn" }
+            }
+        }
+    }
+}
+
+// 页面淡入淡出过渡 - 优化后
+export const pageFadeTransition: HTMLMotionProps<"div"> = {
+    className: "w-full h-full",
+    initial: { opacity: 0, scale: 0.985 },
+    animate: {
+        opacity: 1,
+        scale: 1,
+        transition: {
+            opacity: { duration: 0.35 },
+            scale: { duration: 0.25, ease: [0.34, 1.56, 0.64, 1] }
+        }
+    },
+    exit: {
+        opacity: 0,
+        scale: 0.985,
+        transition: {
+            opacity: { duration: 0.25 },
+            scale: { duration: 0.2, ease: "easeOut" }
+        }
+    }
+}
+
+// 页面滑动过渡 - 优化后
+export const pageSlideTransition: HTMLMotionProps<"div"> = {
+    className: "w-full h-full",
+    initial: { x: 25, opacity: 0, filter: "blur(2px)" },
+    animate: {
+        x: 0,
+        opacity: 1,
+        filter: "blur(0px)",
+        transition: {
+            x: { duration: 0.4, ease: [0.22, 1, 0.36, 1] },
+            opacity: { duration: 0.3, ease: "easeOut" },
+            filter: { duration: 0.2, ease: "easeOut", delay: 0.1 }
+        }
+    },
+    exit: {
+        x: -20,
+        opacity: 0,
+        filter: "blur(2px)",
+        transition: {
+            x: { duration: 0.3, ease: "easeInOut" },
+            opacity: { duration: 0.25, ease: "easeIn" },
+            filter: { duration: 0.15, ease: "easeIn" }
+        }
+    }
+}
+
+// 新增:弹性缩放过渡
+export const pageScaleTransition: HTMLMotionProps<"div"> = {
+    className: "w-full h-full",
+    initial: {
+        opacity: 0,
+        scale: 0.92,
+        y: 10
+    },
+    animate: {
+        opacity: 1,
+        scale: 1,
+        y: 0,
+        transition: {
+            duration: 0.4,
+            scale: { type: "spring", stiffness: 120, damping: 20 },
+            opacity: { duration: 0.3 },
+            y: { duration: 0.3, ease: [0.22, 1, 0.36, 1] }
+        }
+    },
+    exit: {
+        opacity: 0,
+        scale: 0.96,
+        y: -8,
+        transition: {
+            duration: 0.25,
+            ease: "easeOut"
+        }
+    }
+}
+
+// 新增:3D卡片翻转效果
+export const pageFlipTransition: HTMLMotionProps<"div"> = {
+    className: "w-full h-full",
+    initial: {
+        opacity: 0,
+        rotateX: 8,
+        y: 20,
+        transformPerspective: 1200
+    },
+    animate: {
+        opacity: 1,
+        rotateX: 0,
+        y: 0,
+        transition: {
+            duration: 0.5,
+            rotateX: { duration: 0.5, ease: [0.2, 0.65, 0.3, 0.9] },
+            y: { duration: 0.45, ease: [0.22, 1, 0.36, 1] },
+            opacity: { duration: 0.4 }
+        }
+    },
+    exit: {
+        opacity: 0,
+        rotateX: -8,
+        y: -20,
+        transition: {
+            duration: 0.3,
+            ease: "easeInOut"
+        }
+    }
+} 

+ 67 - 0
web/src/components/ui/animation/tab-animation.ts

@@ -0,0 +1,67 @@
+// src/components/ui/animations/tabs-animations.tsx
+import { HTMLMotionProps } from "motion/react"
+
+// 标签内容切换动画 - 水平滑动效果
+export const tabContentAnimation: HTMLMotionProps<"div"> = {
+    initial: {
+        opacity: 0,
+        x: 10
+    },
+    animate: {
+        opacity: 1,
+        x: 0,
+        transition: {
+            duration: 0.3,
+            ease: [0.22, 1, 0.36, 1]
+        }
+    },
+    exit: {
+        opacity: 0,
+        x: -10,
+        transition: {
+            duration: 0.2
+        }
+    }
+}
+
+// 标签内容淡入淡出动画 - 没有位移只有透明度变化
+export const tabFadeAnimation: HTMLMotionProps<"div"> = {
+    initial: {
+        opacity: 0
+    },
+    animate: {
+        opacity: 1,
+        transition: {
+            duration: 0.25
+        }
+    },
+    exit: {
+        opacity: 0,
+        transition: {
+            duration: 0.2
+        }
+    }
+}
+
+// 标签内容缩放动画 - 带有轻微的缩放效果
+export const tabScaleAnimation: HTMLMotionProps<"div"> = {
+    initial: {
+        opacity: 0,
+        scale: 0.98
+    },
+    animate: {
+        opacity: 1,
+        scale: 1,
+        transition: {
+            duration: 0.25,
+            ease: [0.25, 1, 0.5, 1]
+        }
+    },
+    exit: {
+        opacity: 0,
+        scale: 0.98,
+        transition: {
+            duration: 0.2
+        }
+    }
+}

+ 51 - 0
web/src/components/ui/avatar.tsx

@@ -0,0 +1,51 @@
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+function Avatar({
+  className,
+  ...props
+}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
+  return (
+    <AvatarPrimitive.Root
+      data-slot="avatar"
+      className={cn(
+        "relative flex size-8 shrink-0 overflow-hidden rounded-full",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function AvatarImage({
+  className,
+  ...props
+}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
+  return (
+    <AvatarPrimitive.Image
+      data-slot="avatar-image"
+      className={cn("aspect-square size-full", className)}
+      {...props}
+    />
+  )
+}
+
+function AvatarFallback({
+  className,
+  ...props
+}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
+  return (
+    <AvatarPrimitive.Fallback
+      data-slot="avatar-fallback"
+      className={cn(
+        "bg-muted flex size-full items-center justify-center rounded-full",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Avatar, AvatarImage, AvatarFallback }

+ 46 - 0
web/src/components/ui/badge.tsx

@@ -0,0 +1,46 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+  "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+  {
+    variants: {
+      variant: {
+        default:
+          "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+        secondary:
+          "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+        destructive:
+          "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+        outline:
+          "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+    },
+  }
+)
+
+function Badge({
+  className,
+  variant,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"span"> &
+  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
+  const Comp = asChild ? Slot : "span"
+
+  return (
+    <Comp
+      data-slot="badge"
+      className={cn(badgeVariants({ variant }), className)}
+      {...props}
+    />
+  )
+}
+
+export { Badge, badgeVariants }

+ 59 - 0
web/src/components/ui/button.tsx

@@ -0,0 +1,59 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+  {
+    variants: {
+      variant: {
+        default:
+          "bg-[#7B7FF6] text-primary-foreground shadow-xs hover:bg-[#6A6DE6]",
+        destructive:
+          "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+        outline:
+          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+        secondary:
+          "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+        ghost:
+          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+        link: "text-primary underline-offset-4 hover:underline",
+      },
+      size: {
+        default: "h-9 px-4 py-2 has-[>svg]:px-3",
+        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+        icon: "size-9",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  }
+)
+
+function Button({
+  className,
+  variant,
+  size,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"button"> &
+  VariantProps<typeof buttonVariants> & {
+    asChild?: boolean
+  }) {
+  const Comp = asChild ? Slot : "button"
+
+  return (
+    <Comp
+      data-slot="button"
+      className={cn(buttonVariants({ variant, size, className }))}
+      {...props}
+    />
+  )
+}
+
+export { Button, buttonVariants }

+ 92 - 0
web/src/components/ui/card.tsx

@@ -0,0 +1,92 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card"
+      className={cn(
+        "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-header"
+      className={cn(
+        "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-title"
+      className={cn("leading-none font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-action"
+      className={cn(
+        "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-content"
+      className={cn("px-6", className)}
+      {...props}
+    />
+  )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-footer"
+      className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  Card,
+  CardHeader,
+  CardFooter,
+  CardTitle,
+  CardAction,
+  CardDescription,
+  CardContent,
+}

+ 31 - 0
web/src/components/ui/collapsible.tsx

@@ -0,0 +1,31 @@
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+function Collapsible({
+  ...props
+}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
+  return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
+}
+
+function CollapsibleTrigger({
+  ...props
+}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
+  return (
+    <CollapsiblePrimitive.CollapsibleTrigger
+      data-slot="collapsible-trigger"
+      {...props}
+    />
+  )
+}
+
+function CollapsibleContent({
+  ...props
+}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
+  return (
+    <CollapsiblePrimitive.CollapsibleContent
+      data-slot="collapsible-content"
+      {...props}
+    />
+  )
+}
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }

+ 133 - 0
web/src/components/ui/dialog.tsx

@@ -0,0 +1,133 @@
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Dialog({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Root>) {
+  return <DialogPrimitive.Root data-slot="dialog" {...props} />
+}
+
+function DialogTrigger({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
+  return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
+}
+
+function DialogPortal({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
+  return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
+}
+
+function DialogClose({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Close>) {
+  return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
+}
+
+function DialogOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
+  return (
+    <DialogPrimitive.Overlay
+      data-slot="dialog-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DialogContent({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Content>) {
+  return (
+    <DialogPortal data-slot="dialog-portal">
+      <DialogOverlay />
+      <DialogPrimitive.Content
+        data-slot="dialog-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
+          <XIcon />
+          <span className="sr-only">Close</span>
+        </DialogPrimitive.Close>
+      </DialogPrimitive.Content>
+    </DialogPortal>
+  )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="dialog-header"
+      className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
+      {...props}
+    />
+  )
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="dialog-footer"
+      className={cn(
+        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DialogTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Title>) {
+  return (
+    <DialogPrimitive.Title
+      data-slot="dialog-title"
+      className={cn("text-lg leading-none font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function DialogDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Description>) {
+  return (
+    <DialogPrimitive.Description
+      data-slot="dialog-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  Dialog,
+  DialogClose,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogOverlay,
+  DialogPortal,
+  DialogTitle,
+  DialogTrigger,
+}

+ 130 - 0
web/src/components/ui/drawer.tsx

@@ -0,0 +1,130 @@
+import * as React from "react"
+import { Drawer as DrawerPrimitive } from "vaul"
+
+import { cn } from "@/lib/utils"
+
+function Drawer({
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
+  return <DrawerPrimitive.Root data-slot="drawer" {...props} />
+}
+
+function DrawerTrigger({
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
+  return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
+}
+
+function DrawerPortal({
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
+  return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
+}
+
+function DrawerClose({
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
+  return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
+}
+
+function DrawerOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
+  return (
+    <DrawerPrimitive.Overlay
+      data-slot="drawer-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DrawerContent({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
+  return (
+    <DrawerPortal data-slot="drawer-portal">
+      <DrawerOverlay />
+      <DrawerPrimitive.Content
+        data-slot="drawer-content"
+        className={cn(
+          "group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
+          "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
+          "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
+          "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
+          "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
+          className
+        )}
+        {...props}
+      >
+        <div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
+        {children}
+      </DrawerPrimitive.Content>
+    </DrawerPortal>
+  )
+}
+
+function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="drawer-header"
+      className={cn("flex flex-col gap-1.5 p-4", className)}
+      {...props}
+    />
+  )
+}
+
+function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="drawer-footer"
+      className={cn("mt-auto flex flex-col gap-2 p-4", className)}
+      {...props}
+    />
+  )
+}
+
+function DrawerTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
+  return (
+    <DrawerPrimitive.Title
+      data-slot="drawer-title"
+      className={cn("text-foreground font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function DrawerDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
+  return (
+    <DrawerPrimitive.Description
+      data-slot="drawer-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  Drawer,
+  DrawerPortal,
+  DrawerOverlay,
+  DrawerTrigger,
+  DrawerClose,
+  DrawerContent,
+  DrawerHeader,
+  DrawerFooter,
+  DrawerTitle,
+  DrawerDescription,
+}

+ 255 - 0
web/src/components/ui/dropdown-menu.tsx

@@ -0,0 +1,255 @@
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function DropdownMenu({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
+  return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
+}
+
+function DropdownMenuPortal({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
+  return (
+    <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
+  )
+}
+
+function DropdownMenuTrigger({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
+  return (
+    <DropdownMenuPrimitive.Trigger
+      data-slot="dropdown-menu-trigger"
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuContent({
+  className,
+  sideOffset = 4,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
+  return (
+    <DropdownMenuPrimitive.Portal>
+      <DropdownMenuPrimitive.Content
+        data-slot="dropdown-menu-content"
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
+          className
+        )}
+        {...props}
+      />
+    </DropdownMenuPrimitive.Portal>
+  )
+}
+
+function DropdownMenuGroup({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
+  return (
+    <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
+  )
+}
+
+function DropdownMenuItem({
+  className,
+  inset,
+  variant = "default",
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
+  inset?: boolean
+  variant?: "default" | "destructive"
+}) {
+  return (
+    <DropdownMenuPrimitive.Item
+      data-slot="dropdown-menu-item"
+      data-inset={inset}
+      data-variant={variant}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuCheckboxItem({
+  className,
+  children,
+  checked,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
+  return (
+    <DropdownMenuPrimitive.CheckboxItem
+      data-slot="dropdown-menu-checkbox-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      checked={checked}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <DropdownMenuPrimitive.ItemIndicator>
+          <CheckIcon className="size-4" />
+        </DropdownMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </DropdownMenuPrimitive.CheckboxItem>
+  )
+}
+
+function DropdownMenuRadioGroup({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
+  return (
+    <DropdownMenuPrimitive.RadioGroup
+      data-slot="dropdown-menu-radio-group"
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuRadioItem({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
+  return (
+    <DropdownMenuPrimitive.RadioItem
+      data-slot="dropdown-menu-radio-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <DropdownMenuPrimitive.ItemIndicator>
+          <CircleIcon className="size-2 fill-current" />
+        </DropdownMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </DropdownMenuPrimitive.RadioItem>
+  )
+}
+
+function DropdownMenuLabel({
+  className,
+  inset,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
+  inset?: boolean
+}) {
+  return (
+    <DropdownMenuPrimitive.Label
+      data-slot="dropdown-menu-label"
+      data-inset={inset}
+      className={cn(
+        "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
+  return (
+    <DropdownMenuPrimitive.Separator
+      data-slot="dropdown-menu-separator"
+      className={cn("bg-border -mx-1 my-1 h-px", className)}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuShortcut({
+  className,
+  ...props
+}: React.ComponentProps<"span">) {
+  return (
+    <span
+      data-slot="dropdown-menu-shortcut"
+      className={cn(
+        "text-muted-foreground ml-auto text-xs tracking-widest",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuSub({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
+  return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
+}
+
+function DropdownMenuSubTrigger({
+  className,
+  inset,
+  children,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
+  inset?: boolean
+}) {
+  return (
+    <DropdownMenuPrimitive.SubTrigger
+      data-slot="dropdown-menu-sub-trigger"
+      data-inset={inset}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
+        className
+      )}
+      {...props}
+    >
+      {children}
+      <ChevronRightIcon className="ml-auto size-4" />
+    </DropdownMenuPrimitive.SubTrigger>
+  )
+}
+
+function DropdownMenuSubContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
+  return (
+    <DropdownMenuPrimitive.SubContent
+      data-slot="dropdown-menu-sub-content"
+      className={cn(
+        "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export {
+  DropdownMenu,
+  DropdownMenuPortal,
+  DropdownMenuTrigger,
+  DropdownMenuContent,
+  DropdownMenuGroup,
+  DropdownMenuLabel,
+  DropdownMenuItem,
+  DropdownMenuCheckboxItem,
+  DropdownMenuRadioGroup,
+  DropdownMenuRadioItem,
+  DropdownMenuSeparator,
+  DropdownMenuShortcut,
+  DropdownMenuSub,
+  DropdownMenuSubTrigger,
+  DropdownMenuSubContent,
+}

+ 165 - 0
web/src/components/ui/form.tsx

@@ -0,0 +1,165 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+  Controller,
+  FormProvider,
+  useFormContext,
+  useFormState,
+  type ControllerProps,
+  type FieldPath,
+  type FieldValues,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+  TFieldValues extends FieldValues = FieldValues,
+  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+> = {
+  name: TName
+}
+
+const FormFieldContext = React.createContext<FormFieldContextValue>(
+  {} as FormFieldContextValue
+)
+
+const FormField = <
+  TFieldValues extends FieldValues = FieldValues,
+  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+>({
+  ...props
+}: ControllerProps<TFieldValues, TName>) => {
+  return (
+    <FormFieldContext.Provider value={{ name: props.name }}>
+      <Controller {...props} />
+    </FormFieldContext.Provider>
+  )
+}
+
+const useFormField = () => {
+  const fieldContext = React.useContext(FormFieldContext)
+  const itemContext = React.useContext(FormItemContext)
+  const { getFieldState } = useFormContext()
+  const formState = useFormState({ name: fieldContext.name })
+  const fieldState = getFieldState(fieldContext.name, formState)
+
+  if (!fieldContext) {
+    throw new Error("useFormField should be used within <FormField>")
+  }
+
+  const { id } = itemContext
+
+  return {
+    id,
+    name: fieldContext.name,
+    formItemId: `${id}-form-item`,
+    formDescriptionId: `${id}-form-item-description`,
+    formMessageId: `${id}-form-item-message`,
+    ...fieldState,
+  }
+}
+
+type FormItemContextValue = {
+  id: string
+}
+
+const FormItemContext = React.createContext<FormItemContextValue>(
+  {} as FormItemContextValue
+)
+
+function FormItem({ className, ...props }: React.ComponentProps<"div">) {
+  const id = React.useId()
+
+  return (
+    <FormItemContext.Provider value={{ id }}>
+      <div
+        data-slot="form-item"
+        className={cn("grid gap-2", className)}
+        {...props}
+      />
+    </FormItemContext.Provider>
+  )
+}
+
+function FormLabel({
+  className,
+  ...props
+}: React.ComponentProps<typeof LabelPrimitive.Root>) {
+  const { error, formItemId } = useFormField()
+
+  return (
+    <Label
+      data-slot="form-label"
+      data-error={!!error}
+      className={cn("data-[error=true]:text-destructive", className)}
+      htmlFor={formItemId}
+      {...props}
+    />
+  )
+}
+
+function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
+  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+  return (
+    <Slot
+      data-slot="form-control"
+      id={formItemId}
+      aria-describedby={
+        !error
+          ? `${formDescriptionId}`
+          : `${formDescriptionId} ${formMessageId}`
+      }
+      aria-invalid={!!error}
+      {...props}
+    />
+  )
+}
+
+function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
+  const { formDescriptionId } = useFormField()
+
+  return (
+    <p
+      data-slot="form-description"
+      id={formDescriptionId}
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
+  const { error, formMessageId } = useFormField()
+  const body = error ? String(error?.message ?? "") : props.children
+
+  if (!body) {
+    return null
+  }
+
+  return (
+    <p
+      data-slot="form-message"
+      id={formMessageId}
+      className={cn("text-destructive text-sm", className)}
+      {...props}
+    >
+      {body}
+    </p>
+  )
+}
+
+export {
+  useFormField,
+  Form,
+  FormItem,
+  FormLabel,
+  FormControl,
+  FormDescription,
+  FormMessage,
+  FormField,
+}

+ 21 - 0
web/src/components/ui/input.tsx

@@ -0,0 +1,21 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+  return (
+    <input
+      type={type}
+      data-slot="input"
+      className={cn(
+        "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+        "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+        "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Input }

+ 24 - 0
web/src/components/ui/label.tsx

@@ -0,0 +1,24 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+
+import { cn } from "@/lib/utils"
+
+function Label({
+  className,
+  ...props
+}: React.ComponentProps<typeof LabelPrimitive.Root>) {
+  return (
+    <LabelPrimitive.Root
+      data-slot="label"
+      className={cn(
+        "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Label }

+ 183 - 0
web/src/components/ui/select.tsx

@@ -0,0 +1,183 @@
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Select({
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Root>) {
+  return <SelectPrimitive.Root data-slot="select" {...props} />
+}
+
+function SelectGroup({
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Group>) {
+  return <SelectPrimitive.Group data-slot="select-group" {...props} />
+}
+
+function SelectValue({
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Value>) {
+  return <SelectPrimitive.Value data-slot="select-value" {...props} />
+}
+
+function SelectTrigger({
+  className,
+  size = "default",
+  children,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
+  size?: "sm" | "default"
+}) {
+  return (
+    <SelectPrimitive.Trigger
+      data-slot="select-trigger"
+      data-size={size}
+      className={cn(
+        "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    >
+      {children}
+      <SelectPrimitive.Icon asChild>
+        <ChevronDownIcon className="size-4 opacity-50" />
+      </SelectPrimitive.Icon>
+    </SelectPrimitive.Trigger>
+  )
+}
+
+function SelectContent({
+  className,
+  children,
+  position = "popper",
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Content>) {
+  return (
+    <SelectPrimitive.Portal>
+      <SelectPrimitive.Content
+        data-slot="select-content"
+        className={cn(
+          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
+          position === "popper" &&
+            "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
+          className
+        )}
+        position={position}
+        {...props}
+      >
+        <SelectScrollUpButton />
+        <SelectPrimitive.Viewport
+          className={cn(
+            "p-1",
+            position === "popper" &&
+              "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
+          )}
+        >
+          {children}
+        </SelectPrimitive.Viewport>
+        <SelectScrollDownButton />
+      </SelectPrimitive.Content>
+    </SelectPrimitive.Portal>
+  )
+}
+
+function SelectLabel({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Label>) {
+  return (
+    <SelectPrimitive.Label
+      data-slot="select-label"
+      className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
+      {...props}
+    />
+  )
+}
+
+function SelectItem({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Item>) {
+  return (
+    <SelectPrimitive.Item
+      data-slot="select-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
+        className
+      )}
+      {...props}
+    >
+      <span className="absolute right-2 flex size-3.5 items-center justify-center">
+        <SelectPrimitive.ItemIndicator>
+          <CheckIcon className="size-4" />
+        </SelectPrimitive.ItemIndicator>
+      </span>
+      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
+    </SelectPrimitive.Item>
+  )
+}
+
+function SelectSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
+  return (
+    <SelectPrimitive.Separator
+      data-slot="select-separator"
+      className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
+      {...props}
+    />
+  )
+}
+
+function SelectScrollUpButton({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
+  return (
+    <SelectPrimitive.ScrollUpButton
+      data-slot="select-scroll-up-button"
+      className={cn(
+        "flex cursor-default items-center justify-center py-1",
+        className
+      )}
+      {...props}
+    >
+      <ChevronUpIcon className="size-4" />
+    </SelectPrimitive.ScrollUpButton>
+  )
+}
+
+function SelectScrollDownButton({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
+  return (
+    <SelectPrimitive.ScrollDownButton
+      data-slot="select-scroll-down-button"
+      className={cn(
+        "flex cursor-default items-center justify-center py-1",
+        className
+      )}
+      {...props}
+    >
+      <ChevronDownIcon className="size-4" />
+    </SelectPrimitive.ScrollDownButton>
+  )
+}
+
+export {
+  Select,
+  SelectContent,
+  SelectGroup,
+  SelectItem,
+  SelectLabel,
+  SelectScrollDownButton,
+  SelectScrollUpButton,
+  SelectSeparator,
+  SelectTrigger,
+  SelectValue,
+}

+ 26 - 0
web/src/components/ui/separator.tsx

@@ -0,0 +1,26 @@
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+function Separator({
+  className,
+  orientation = "horizontal",
+  decorative = true,
+  ...props
+}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
+  return (
+    <SeparatorPrimitive.Root
+      data-slot="separator-root"
+      decorative={decorative}
+      orientation={orientation}
+      className={cn(
+        "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Separator }

+ 137 - 0
web/src/components/ui/sheet.tsx

@@ -0,0 +1,137 @@
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
+  return <SheetPrimitive.Root data-slot="sheet" {...props} />
+}
+
+function SheetTrigger({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
+  return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
+}
+
+function SheetClose({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Close>) {
+  return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
+}
+
+function SheetPortal({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
+  return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
+}
+
+function SheetOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
+  return (
+    <SheetPrimitive.Overlay
+      data-slot="sheet-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SheetContent({
+  className,
+  children,
+  side = "right",
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Content> & {
+  side?: "top" | "right" | "bottom" | "left"
+}) {
+  return (
+    <SheetPortal>
+      <SheetOverlay />
+      <SheetPrimitive.Content
+        data-slot="sheet-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+          side === "right" &&
+            "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
+          side === "left" &&
+            "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
+          side === "top" &&
+            "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
+          side === "bottom" &&
+            "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
+          <XIcon className="size-4" />
+          <span className="sr-only">Close</span>
+        </SheetPrimitive.Close>
+      </SheetPrimitive.Content>
+    </SheetPortal>
+  )
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sheet-header"
+      className={cn("flex flex-col gap-1.5 p-4", className)}
+      {...props}
+    />
+  )
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sheet-footer"
+      className={cn("mt-auto flex flex-col gap-2 p-4", className)}
+      {...props}
+    />
+  )
+}
+
+function SheetTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Title>) {
+  return (
+    <SheetPrimitive.Title
+      data-slot="sheet-title"
+      className={cn("text-foreground font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function SheetDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Description>) {
+  return (
+    <SheetPrimitive.Description
+      data-slot="sheet-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  Sheet,
+  SheetTrigger,
+  SheetClose,
+  SheetContent,
+  SheetHeader,
+  SheetFooter,
+  SheetTitle,
+  SheetDescription,
+}

+ 13 - 0
web/src/components/ui/skeleton.tsx

@@ -0,0 +1,13 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="skeleton"
+      className={cn("bg-accent animate-pulse rounded-md", className)}
+      {...props}
+    />
+  )
+}
+
+export { Skeleton }

+ 29 - 0
web/src/components/ui/sonner.tsx

@@ -0,0 +1,29 @@
+import { useTheme } from "next-themes"
+import { Toaster as Sonner, ToasterProps } from "sonner"
+
+const Toaster = ({ ...props }: ToasterProps) => {
+  const { theme = "system" } = useTheme()
+
+  return (
+    <Sonner
+      theme={theme as ToasterProps["theme"]}
+      className="toaster group"
+      style={
+        {
+          "--normal-bg": "var(--background)",
+          "--normal-text": "var(--primary)",
+          "--normal-border": "var(--border)",
+          "--success-bg": "var(--background)",
+          "--success-text": "var(--primary)",
+          "--success-border": "var(--border)",
+          "--error-bg": "var(--background)",
+          "--error-border": "var(--border)",
+          "--error-text": "var(--destructive)",
+        } as React.CSSProperties
+      }
+      {...props}
+    />
+  )
+}
+
+export { Toaster }

+ 29 - 0
web/src/components/ui/switch.tsx

@@ -0,0 +1,29 @@
+import * as React from "react"
+import * as SwitchPrimitive from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+function Switch({
+  className,
+  ...props
+}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
+  return (
+    <SwitchPrimitive.Root
+      data-slot="switch"
+      className={cn(
+        "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
+        className
+      )}
+      {...props}
+    >
+      <SwitchPrimitive.Thumb
+        data-slot="switch-thumb"
+        className={cn(
+          "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
+        )}
+      />
+    </SwitchPrimitive.Root>
+  )
+}
+
+export { Switch }

+ 114 - 0
web/src/components/ui/table.tsx

@@ -0,0 +1,114 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Table({ className, ...props }: React.ComponentProps<"table">) {
+  return (
+    <div
+      data-slot="table-container"
+      className="relative w-full overflow-x-auto"
+    >
+      <table
+        data-slot="table"
+        className={cn("w-full caption-bottom text-sm", className)}
+        {...props}
+      />
+    </div>
+  )
+}
+
+function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
+  return (
+    <thead
+      data-slot="table-header"
+      className={cn("[&_tr]:border-b", className)}
+      {...props}
+    />
+  )
+}
+
+function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
+  return (
+    <tbody
+      data-slot="table-body"
+      className={cn("[&_tr:last-child]:border-0", className)}
+      {...props}
+    />
+  )
+}
+
+function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
+  return (
+    <tfoot
+      data-slot="table-footer"
+      className={cn(
+        "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+  return (
+    <tr
+      data-slot="table-row"
+      className={cn(
+        "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+  return (
+    <th
+      data-slot="table-head"
+      className={cn(
+        "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+  return (
+    <td
+      data-slot="table-cell"
+      className={cn(
+        "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function TableCaption({
+  className,
+  ...props
+}: React.ComponentProps<"caption">) {
+  return (
+    <caption
+      data-slot="table-caption"
+      className={cn("text-muted-foreground mt-4 text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  Table,
+  TableHeader,
+  TableBody,
+  TableFooter,
+  TableHead,
+  TableRow,
+  TableCell,
+  TableCaption,
+}

+ 59 - 0
web/src/components/ui/tooltip.tsx

@@ -0,0 +1,59 @@
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({
+  delayDuration = 0,
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
+  return (
+    <TooltipPrimitive.Provider
+      data-slot="tooltip-provider"
+      delayDuration={delayDuration}
+      {...props}
+    />
+  )
+}
+
+function Tooltip({
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
+  return (
+    <TooltipProvider>
+      <TooltipPrimitive.Root data-slot="tooltip" {...props} />
+    </TooltipProvider>
+  )
+}
+
+function TooltipTrigger({
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
+  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
+}
+
+function TooltipContent({
+  className,
+  sideOffset = 0,
+  children,
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
+  return (
+    <TooltipPrimitive.Portal>
+      <TooltipPrimitive.Content
+        data-slot="tooltip-content"
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        <TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
+      </TooltipPrimitive.Content>
+    </TooltipPrimitive.Portal>
+  )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

+ 152 - 0
web/src/constant/index.ts

@@ -0,0 +1,152 @@
+import { ENV } from "@/utils/env"
+
+/**
+ * Constant type definition
+ */
+export type ConstantValue = string | number | boolean | null | undefined
+
+/**
+ * Constant category enumeration
+ */
+export enum ConstantCategory {
+    SYSTEM = 'system',
+    UI = 'ui',
+    CONFIG = 'config',
+    FEATURE = 'feature'
+}
+
+/**
+ * Constant storage interface
+ */
+interface ConstantStore {
+    [category: string]: {
+        [key: string]: ConstantValue
+    }
+}
+
+/**
+ * Constant storage object
+ * Using categories to store different types of constants
+ */
+const constantStore: ConstantStore = {
+    [ConstantCategory.SYSTEM]: {
+        VERSION: '1.0.0',
+        API_TIMEOUT: Number(ENV.API_TIMEOUT),
+        DEBUG_MODE: ENV.isDevelopment,
+    },
+    [ConstantCategory.UI]: {
+        DEFAULT_THEME: 'light',
+        // MOBILE_BREAKPOINT: 768,
+    },
+    [ConstantCategory.CONFIG]: {
+        DEFAULT_PAGE_SIZE: 10,
+    },
+    [ConstantCategory.FEATURE]: {
+        QUERY_STALE_TIME: 5 * 60 * 1000,
+        DEFAULT_QUERY_RETRY: 1,
+        TOAST_DURATION: 2000,
+    }
+}
+
+/**
+ * Function to get a constant
+ * @param category Constant category
+ * @param key Constant key name
+ * @param defaultValue Default value (returned when the constant does not exist)
+ * @returns Constant value or default value
+ */
+export function getConstant<T extends ConstantValue>(
+    category: ConstantCategory,
+    key: string,
+    defaultValue?: T
+): T {
+    if (
+        constantStore[category] &&
+        constantStore[category][key] !== undefined
+    ) {
+        return constantStore[category][key] as T
+    }
+    return defaultValue as T
+}
+
+/**
+ * Function to set a constant
+ * @param category Constant category
+ * @param key Constant key name
+ * @param value Value to set
+ * @returns Whether the setting was successful
+ */
+export function setConstant(
+    category: ConstantCategory,
+    key: string,
+    value: ConstantValue
+): boolean {
+    try {
+        // Ensure the category exists
+        if (!constantStore[category]) {
+            constantStore[category] = {}
+        }
+
+        // Set the constant value
+        constantStore[category][key] = value
+        return true
+    } catch (error) {
+        console.error(`Failed to set constant [${category}.${key}]:`, error)
+        return false
+    }
+}
+
+/**
+ * Check if a constant exists
+ * @param category Constant category
+ * @param key Constant key name
+ * @returns Whether it exists
+ */
+export function hasConstant(
+    category: ConstantCategory,
+    key: string
+): boolean {
+    return (
+        constantStore[category] !== undefined &&
+        constantStore[category][key] !== undefined
+    )
+}
+
+/**
+ * Get all constants under a category
+ * @param category Constant category
+ * @returns Constant object
+ */
+export function getCategoryConstants(
+    category: ConstantCategory
+): Record<string, ConstantValue> {
+    return constantStore[category] || {}
+}
+
+/**
+ * Batch set constants
+ * @param category Constant category
+ * @param constants Constant key-value pairs
+ * @returns Whether all were set successfully
+ */
+export function setBatchConstants(
+    category: ConstantCategory,
+    constants: Record<string, ConstantValue>
+): boolean {
+    try {
+        // Ensure the category exists
+        if (!constantStore[category]) {
+            constantStore[category] = {}
+        }
+
+        // Batch set constants
+        Object.entries(constants).forEach(([key, value]) => {
+            constantStore[category][key] = value
+        })
+
+        return true
+    } catch (error) {
+        console.error(`Failed to set batch constants for category [${category}]:`, error)
+        return false
+    }
+}

+ 19 - 0
web/src/feature/auth/components/ProtectedRoute.tsx

@@ -0,0 +1,19 @@
+import { useEffect } from 'react'
+import { useNavigate, useLocation, Outlet } from 'react-router'
+import useAuthStore from '@/store/auth'
+
+export function ProtectedRoute() {
+    const { isAuthenticated } = useAuthStore()
+    const navigate = useNavigate()
+    const location = useLocation()
+
+    useEffect(() => {
+        if (!isAuthenticated) {
+            // Redirect to login, but save the current location
+            navigate('/login', { state: { from: location } })
+        }
+    }, [isAuthenticated, navigate, location])
+
+    // If authenticated, render children
+    return isAuthenticated ? <Outlet /> : null
+}

+ 41 - 0
web/src/feature/auth/hooks.ts

@@ -0,0 +1,41 @@
+// src/feature/auth/hooks.ts
+import { useMutation } from '@tanstack/react-query'
+import { useNavigate, useLocation } from 'react-router'
+import { authApi } from '@/api/services'
+import { useAuthStore } from '@/store/auth'
+import { toast } from 'sonner'
+import { ApiError } from '@/api/index'
+
+export function useLoginMutation() {
+    const navigate = useNavigate()
+    const location = useLocation()
+    const { login } = useAuthStore()
+
+    // get redirect url from location
+    const from = (location.state as { from?: { pathname: string } })?.from?.pathname || '/'
+
+    return useMutation({
+        mutationFn: async (token: string) => {
+            const result = await authApi.getChannelTypeMetas(token)
+            return { token, result }
+        },
+        onSuccess: ({ token }) => {
+            // login success, save token
+            login(token)
+            toast.success('login success')
+            // redirect to previous page or home page
+            navigate(from, { replace: true })
+        },
+        onError: (error: unknown) => {
+            if (error instanceof ApiError) {
+                if (error.code === 401) {
+                    toast.error('Token无效,请重新输入')
+                } else {
+                    toast.error(`API错误 (${error.code}): ${error.message}`)
+                }
+            } else {
+                toast.error('登录失败,请重试')
+            }
+        }
+    })
+}

+ 93 - 0
web/src/feature/channel/components/ChannelDialog.tsx

@@ -0,0 +1,93 @@
+// src/feature/channel/components/ChannelDialog.tsx
+import {
+    Dialog,
+    DialogContent,
+    DialogDescription,
+    DialogHeader,
+    DialogTitle
+} from '@/components/ui/dialog'
+import { ChannelForm } from './ChannelForm'
+import { Channel } from '@/types/channel'
+import { AnimatePresence, motion } from "motion/react"
+import { useTranslation } from 'react-i18next'
+import {
+    dialogEnterExitAnimation,
+    dialogContentAnimation,
+    dialogHeaderAnimation,
+    dialogContentItemAnimation
+} from '@/components/ui/animation/dialog-animation'
+
+interface ChannelDialogProps {
+    open: boolean
+    onOpenChange: (open: boolean) => void
+    mode: 'create' | 'update'
+    channel?: Channel | null
+}
+
+export function ChannelDialog({
+    open,
+    onOpenChange,
+    mode = 'create',
+    channel = null
+}: ChannelDialogProps) {
+    const { t } = useTranslation()
+
+    // Determine title and description based on mode
+    const title = mode === 'create' ? t("channel.dialog.createTitle") : t("channel.dialog.updateTitle")
+    const description = mode === 'create'
+        ? t("channel.dialog.createDescription")
+        : t("channel.dialog.updateDescription")
+
+    // Default values for form
+    const defaultValues = mode === 'update' && channel
+        ? {
+            type: channel.type,
+            name: channel.name,
+            key: channel.key,
+            base_url: channel.base_url,
+            models: channel.models || [],
+            model_mapping: channel.model_mapping || {}
+        }
+        : {
+            type: 0,
+            name: '',
+            key: '',
+            base_url: '',
+            models: [],
+            model_mapping: {}
+        }
+
+    return (
+        <Dialog open={open} onOpenChange={onOpenChange}>
+            <AnimatePresence mode="wait">
+                {open && (
+                    <motion.div {...dialogEnterExitAnimation}>
+                        <DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto p-0">
+                            <motion.div {...dialogContentAnimation}>
+                                <motion.div {...dialogHeaderAnimation}>
+                                    <DialogHeader className="p-6 pb-3">
+                                        <DialogTitle className="text-xl">{title}</DialogTitle>
+                                        <DialogDescription>{description}</DialogDescription>
+                                    </DialogHeader>
+                                </motion.div>
+
+                                <motion.div
+                                    {...dialogContentItemAnimation}
+                                    className="px-6 pb-6"
+                                >
+                                    <ChannelForm
+                                        mode={mode}
+                                        channelId={channel?.id}
+                                        channel={channel}
+                                        defaultValues={defaultValues}
+                                        onSuccess={() => onOpenChange(false)}
+                                    />
+                                </motion.div>
+                            </motion.div>
+                        </DialogContent>
+                    </motion.div>
+                )}
+            </AnimatePresence>
+        </Dialog>
+    )
+}

+ 439 - 0
web/src/feature/channel/components/ChannelForm.tsx

@@ -0,0 +1,439 @@
+// src/feature/channel/components/ChannelForm.tsx
+import { useState } from 'react'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { Input } from '@/components/ui/input'
+import { Button } from '@/components/ui/button'
+import {
+    Form,
+    FormControl,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from '@/components/ui/form'
+import { channelCreateSchema } from '@/validation/channel'
+import { useChannelTypeMetas, useCreateChannel, useUpdateChannel } from '../hooks'
+import { useModels } from '@/feature/model/hooks'
+import { useTranslation } from 'react-i18next'
+import { ChannelCreateForm } from '@/validation/channel'
+import { ModelDialog } from '@/feature/model/components/ModelDialog'
+import { Channel } from '@/types/channel'
+import { SingleSelectCombobox } from '@/components/select/SingleSelectCombobox'
+import { MultiSelectCombobox } from '@/components/select/MultiSelectCombobox'
+import { ConstructMappingComponent } from '@/components/select/ConstructMappingComponent'
+import { AdvancedErrorDisplay } from '@/components/common/error/errorDisplay'
+import { Skeleton } from "@/components/ui/skeleton"
+import { AnimatedContainer } from '@/components/ui/animation/components/animated-container'
+
+interface ChannelFormProps {
+    mode?: 'create' | 'update'
+    channelId?: number
+    channel?: Channel | null
+    onSuccess?: () => void
+    defaultValues?: {
+        type: number
+        name: string
+        key: string
+        base_url: string
+        models: string[]
+        model_mapping?: Record<string, string>
+    }
+}
+
+export function ChannelForm({
+    mode = 'create',
+    channelId,
+    // @ts-expect-error 忽略未使用参数
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    channel,
+    onSuccess,
+    defaultValues = {
+        type: 0,
+        name: '',
+        key: '',
+        base_url: '',
+        models: [],
+        model_mapping: {}
+    },
+}: ChannelFormProps) {
+    const { t } = useTranslation()
+    const [modelDialogOpen, setModelDialogOpen] = useState(false)
+
+    // 获取渠道类型元数据
+    const { data: typeMetas, isLoading: isTypeMetasLoading } = useChannelTypeMetas()
+
+    // 获取所有模型
+    const { data: models, isLoading: isModelsLoading } = useModels()
+
+
+    // API hooks
+    const {
+        createChannel,
+        isLoading: isCreating,
+        error: createError,
+        clearError: clearCreateError
+    } = useCreateChannel()
+
+    const {
+        updateChannel,
+        isLoading: isUpdating,
+        error: updateError,
+        clearError: clearUpdateError
+    } = useUpdateChannel()
+
+    // 动态状态
+    const isLoading = mode === 'create' ? isCreating : isUpdating
+    const error = mode === 'create' ? createError : updateError
+    const clearError = mode === 'create' ? clearCreateError : clearUpdateError
+
+    // 表单设置
+    const form = useForm<ChannelCreateForm>({
+        resolver: zodResolver(channelCreateSchema),
+        defaultValues,
+    })
+
+
+
+
+    // 表单提交处理
+    const handleFormSubmit = (data: ChannelCreateForm) => {
+        // 清除之前的错误
+        if (clearError) clearError()
+
+
+        // 准备提交数据
+        const formData = {
+            type: data.type,
+            name: data.name,
+            key: data.key,
+            base_url: data.base_url,
+            models: data.models,
+            model_mapping: data.model_mapping
+        }
+
+        if (mode === 'create') {
+            createChannel(formData, {
+                onSuccess: () => {
+                    form.reset()
+                    if (onSuccess) onSuccess()
+                }
+            })
+        } else if (mode === 'update' && channelId) {
+            updateChannel({ id: channelId, data: formData }, {
+                onSuccess: () => {
+                    form.reset()
+                    if (onSuccess) onSuccess()
+                }
+            })
+        }
+    }
+
+    // 获取类型对应的字段提示
+    const getTypeHelp = (typeId: number) => {
+        if (!typeMetas || !typeId) return { keyHelp: '', defaultBaseUrl: '' }
+        return typeMetas[typeId] || { keyHelp: '', defaultBaseUrl: '' }
+    }
+
+    // 表单骨架屏渲染
+    const renderFormSkeleton = () => (
+        <div className="space-y-6 animate-pulse">
+            {/* 厂商字段骨架 */}
+            <div className="space-y-2">
+                <Skeleton className="h-5 w-24" />
+                <Skeleton className="h-9 w-full" />
+            </div>
+
+            {/* 名称字段骨架 */}
+            <div className="space-y-2">
+                <Skeleton className="h-5 w-32" />
+                <Skeleton className="h-9 w-full" />
+            </div>
+
+            {/* 模型选择字段骨架 */}
+            <div className="space-y-2">
+                <Skeleton className="h-5 w-28" />
+                <Skeleton className="h-[72px] w-full rounded-md" />
+            </div>
+
+            {/* 模型映射字段骨架 */}
+            <div className="space-y-2">
+                <Skeleton className="h-5 w-36" />
+                <Skeleton className="h-32 w-full" />
+            </div>
+
+            {/* 密钥字段骨架 */}
+            <div className="space-y-2">
+                <Skeleton className="h-5 w-24" />
+                <Skeleton className="h-9 w-full" />
+            </div>
+
+            {/* 代理地址字段骨架 */}
+            <div className="space-y-2">
+                <Skeleton className="h-5 w-32" />
+                <Skeleton className="h-9 w-full" />
+            </div>
+
+            {/* 提交按钮骨架 */}
+            <div className="flex justify-end">
+                <Skeleton className="h-9 w-24" />
+            </div>
+        </div>
+    )
+
+    return (
+        <AnimatedContainer>
+            <div>
+                {isTypeMetasLoading || !typeMetas || isModelsLoading || !models ? (
+                    renderFormSkeleton()
+                ) : (
+                    <Form {...form}>
+                        <form onSubmit={form.handleSubmit(handleFormSubmit)} className="space-y-6">
+                            {/* API错误提示 */}
+                            {error && (
+                                <AdvancedErrorDisplay error={error} />
+                            )}
+
+                            {/* 厂商字段 */}
+                            <FormField
+                                control={form.control}
+                                name="type"
+                                render={({ field }) => {
+
+                                    const availableChannels = Object.values(typeMetas).map(
+                                        (type) => type.name
+                                    )
+
+                                    const initSelectedItem = field.value
+                                        ? typeMetas[String(field.value)].name
+                                        : undefined
+
+                                    const getKeyByName = (name: string): string | undefined => {
+                                        for (const key in typeMetas) {
+                                            if (typeMetas[key].name === name) {
+                                                return key
+                                            }
+                                        }
+                                        return undefined
+                                    }
+
+                                    return (
+
+                                        <SingleSelectCombobox
+                                            dropdownItems={availableChannels}
+                                            initSelectedItem={initSelectedItem}
+                                            setSelectedItem={(channelName: string) => {
+                                                if (channelName) {
+                                                    const channelType = getKeyByName(channelName)
+                                                    if (channelType) {
+                                                        field.onChange(Number(channelType))
+                                                        form.setValue('models', [])
+                                                        form.setValue('model_mapping', {})
+                                                    }
+                                                }
+                                            }}
+                                            handleDropdownItemFilter={(
+                                                dropdownItems: string[],
+                                                inputValue: string
+                                            ) => {
+                                                const lowerCasedInput = inputValue.toLowerCase()
+
+                                                return dropdownItems.filter((item) => {
+                                                    return (
+                                                        !inputValue ||
+                                                        item.toLowerCase().includes(lowerCasedInput)
+                                                    )
+                                                })
+
+
+                                            }}
+                                            handleDropdownItemDisplay={(
+                                                dropdownItem: string
+                                            ) => {
+                                                return (
+                                                    dropdownItem
+                                                )
+                                            }}
+                                        />
+                                    )
+                                }}
+                            />
+
+                            {/* 名称字段 */}
+                            <FormField
+                                control={form.control}
+                                name="name"
+                                render={({ field }) => (
+                                    <FormItem>
+                                        <FormLabel>{t("channel.dialog.name")}</FormLabel>
+                                        <FormControl>
+                                            <Input placeholder={t("channel.dialog.namePlaceholder")} {...field} />
+                                        </FormControl>
+                                        <FormMessage />
+                                    </FormItem>
+                                )}
+                            />
+
+
+                            {/* 模型选择字段 */}
+                            <FormField
+                                control={form.control}
+                                name="models"
+                                render={({ field }) => {
+                                    const allModels = models.map((model) => model.model)
+
+                                    const handleModelFilteredDropdownItems = (
+                                        dropdownItems: string[],
+                                        selectedItems: string[],
+                                        inputValue: string
+                                    ) => {
+                                        const lowerCasedInputValue = inputValue.toLowerCase()
+
+                                        // 过滤匹配的模型
+                                        const filteredModels = dropdownItems.filter(
+                                            (item) =>
+                                                !selectedItems.includes(item) &&
+                                                item.toLowerCase().includes(lowerCasedInputValue)
+                                        )
+
+                                        // 始终添加"创建新模型"选项作为第一个选项
+                                        const createNewOption = t('model.dialog.createDescription')
+
+                                        // 只在搜索为空或选项匹配"创建"相关文字时显示创建选项
+                                        if (!inputValue || createNewOption.toLowerCase().includes(lowerCasedInputValue)) {
+                                            return [createNewOption, ...filteredModels]
+                                        }
+
+                                        return filteredModels
+                                    }
+
+                                    return (
+                                        <MultiSelectCombobox<string>
+                                            dropdownItems={allModels}
+                                            selectedItems={field.value || []}
+                                            setSelectedItems={(modelsOrFunction) => {
+                                                // Ensure we're working with array
+                                                const models = Array.isArray(modelsOrFunction) ? modelsOrFunction : []
+
+                                                // Now we can use includes safely
+                                                if (models.includes(t('model.dialog.createDescription'))) {
+                                                    const filteredModels = models.filter(m => m !== t('model.dialog.createDescription'))
+                                                    field.onChange(filteredModels)
+                                                    setModelDialogOpen(true)
+                                                } else {
+                                                    field.onChange(models)
+                                                }
+                                            }}
+                                            handleFilteredDropdownItems={handleModelFilteredDropdownItems}
+                                            handleDropdownItemDisplay={(item) => {
+                                                // 为"创建新模型"选项添加特殊样式
+                                                if (item === t('model.dialog.createDescription')) {
+                                                    return (
+                                                        <div className="flex items-center gap-2 text-primary">
+                                                            <span className="flex h-4 w-4 items-center justify-center rounded-full border border-primary">
+                                                                <span className="text-xs">+</span>
+                                                            </span>
+                                                            {item}
+                                                        </div>
+                                                    )
+                                                }
+                                                return item
+                                            }}
+                                            handleSelectedItemDisplay={(item) => {
+                                                return item
+                                            }}
+                                        />
+                                    )
+                                }}
+                            />
+
+
+
+                            {/* 模型映射字段 */}
+                            <FormField
+                                control={form.control}
+                                name="model_mapping"
+                                render={({ field }) => {
+                                    const selectedModels = form.watch('models')
+
+                                    return (
+                                        <ConstructMappingComponent
+                                            mapKeys={selectedModels}
+                                            mapData={field.value as Record<string, string>}
+                                            setMapData={(mapping) => {
+                                                field.onChange(mapping)
+                                            }}
+                                        />
+                                    )
+                                }}
+                            />
+
+
+                            {/* 密钥字段 */}
+                            <FormField
+                                control={form.control}
+                                name="key"
+                                render={({ field }) => {
+                                    const typeId = Number(form.getValues('type'))
+                                    const { keyHelp } = getTypeHelp(typeId)
+
+                                    return (
+                                        <FormItem>
+                                            <FormLabel>{t("channel.dialog.key")}</FormLabel>
+                                            <FormControl>
+                                                <Input
+                                                    placeholder={keyHelp || t("channel.dialog.keyPlaceholder")}
+                                                    {...field}
+                                                />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )
+                                }}
+                            />
+
+                            {/* 代理地址字段 */}
+                            <FormField
+                                control={form.control}
+                                name="base_url"
+                                render={({ field }) => {
+                                    const typeId = Number(form.getValues('type'))
+                                    const { defaultBaseUrl } = getTypeHelp(typeId)
+
+                                    return (
+                                        <FormItem>
+                                            <FormLabel>{t("channel.dialog.baseUrl")}</FormLabel>
+                                            <FormControl>
+                                                <Input
+                                                    placeholder={defaultBaseUrl || t("channel.dialog.baseUrlPlaceholder")}
+                                                    {...field}
+                                                />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )
+                                }}
+                            />
+
+
+                            {/* 提交按钮 */}
+                            <div className="flex justify-end">
+                                <Button type="submit" disabled={isLoading}>
+                                    {isLoading ? t("channel.dialog.submitting") : mode === 'create' ? t("channel.dialog.create") : t("channel.dialog.update")}
+                                </Button>
+                            </div>
+                        </form>
+                    </Form>
+                )}
+
+                {/* 创建模型对话框 */}
+                <ModelDialog
+                    open={modelDialogOpen}
+                    onOpenChange={setModelDialogOpen}
+                    mode="create"
+                    model={null}
+                />
+            </div>
+        </AnimatedContainer>
+    )
+}

+ 317 - 0
web/src/feature/channel/components/ChannelTable.tsx

@@ -0,0 +1,317 @@
+// src/feature/channel/components/ChannelTable.tsx
+import { useState, useRef, useEffect, useMemo } from 'react'
+import {
+    useReactTable,
+    getCoreRowModel,
+    ColumnDef,
+} from '@tanstack/react-table'
+import { useChannels, useChannelTypeMetas, useUpdateChannelStatus } from '../hooks'
+import { Channel } from '@/types/channel'
+import { Button } from '@/components/ui/button'
+import {
+    MoreHorizontal, Plus, Trash2, RefreshCcw, Pencil,
+    PowerOff, Power
+} from 'lucide-react'
+import {
+    DropdownMenu, DropdownMenuContent,
+    DropdownMenuItem, DropdownMenuTrigger
+} from '@/components/ui/dropdown-menu'
+import { Card } from '@/components/ui/card'
+import { ChannelDialog } from './ChannelDialog'
+import { Loader2 } from 'lucide-react'
+import { DataTable } from '@/components/table/motion-data-table'
+import { DeleteChannelDialog } from './DeleteChannelDialog'
+import { useTranslation } from 'react-i18next'
+import { AnimatedIcon } from '@/components/ui/animation/components/animated-icon'
+import { AnimatedButton } from '@/components/ui/animation/components/animated-button'
+import { Badge } from '@/components/ui/badge'
+import { cn } from '@/lib/utils'
+
+export function ChannelTable() {
+    const { t } = useTranslation()
+
+    // 状态管理
+    const [channelDialogOpen, setChannelDialogOpen] = useState(false)
+    const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
+    const [selectedChannelId, setSelectedChannelId] = useState<number | null>(null)
+    const sentinelRef = useRef<HTMLDivElement>(null)
+    const [dialogMode, setDialogMode] = useState<'create' | 'update'>('create')
+    const [selectedChannel, setSelectedChannel] = useState<Channel | null>(null)
+    const [isRefreshAnimating, setIsRefreshAnimating] = useState(false)
+
+    // 获取渠道类型元数据
+    const { data: typeMetas } = useChannelTypeMetas()
+
+    // 获取渠道列表
+    const {
+        data,
+        isLoading,
+        fetchNextPage,
+        hasNextPage,
+        isFetchingNextPage,
+        refetch
+    } = useChannels()
+
+    // 更新渠道状态
+    const { updateStatus, isLoading: isStatusUpdating } = useUpdateChannelStatus()
+
+    // 扁平化分页数据
+    const flatData = useMemo(() =>
+        (data?.pages.flatMap(page => page.channels) || []).filter(channel => channel != null),
+        [data]
+    )
+
+    // 优化的无限滚动实现
+    useEffect(() => {
+        // 只有当有更多页面可加载时才创建观察器
+        if (!hasNextPage) return
+
+        const options = {
+            threshold: 0.1,
+            rootMargin: '100px 0px'
+        }
+
+        const handleObserver = (entries: IntersectionObserverEntry[]) => {
+            const [entry] = entries
+            if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) {
+                fetchNextPage()
+            }
+        }
+
+        const observer = new IntersectionObserver(handleObserver, options)
+
+        const sentinel = sentinelRef.current
+        if (sentinel) {
+            observer.observe(sentinel)
+        }
+
+        return () => {
+            if (sentinel) {
+                observer.unobserve(sentinel)
+            }
+            observer.disconnect()
+        }
+    }, [hasNextPage, isFetchingNextPage, fetchNextPage])
+
+    // 打开创建渠道对话框
+    const openCreateDialog = () => {
+        setDialogMode('create')
+        setSelectedChannel(null)
+        setChannelDialogOpen(true)
+    }
+
+    // 打开更新渠道对话框
+    const openUpdateDialog = (channel: Channel) => {
+        setDialogMode('update')
+        setSelectedChannel(channel)
+        setChannelDialogOpen(true)
+    }
+
+    // 打开删除对话框
+    const openDeleteDialog = (id: number) => {
+        setSelectedChannelId(id)
+        setDeleteDialogOpen(true)
+    }
+
+    // 更新渠道状态
+    const handleStatusChange = (id: number, currentStatus: number) => {
+        // 状态切换: 1 -> 2 (禁用 -> 启用), 2 -> 1 (启用 -> 禁用)
+        const newStatus = currentStatus === 1 ? 2 : 1
+        updateStatus({ id, status: { status: newStatus } })
+    }
+
+    // 刷新渠道列表
+    const refreshChannels = () => {
+        setIsRefreshAnimating(true)
+        refetch()
+
+        // 停止动画,延迟1秒以匹配动画效果
+        setTimeout(() => {
+            setIsRefreshAnimating(false)
+        }, 1000)
+    }
+
+    // 获取渠道类型名称
+    const getChannelTypeName = (typeId: number): string => {
+        if (!typeMetas) return String(typeId)
+        const meta = typeMetas[typeId]
+        return meta ? meta.name : String(typeId)
+    }
+
+    // 表格列定义
+    const columns: ColumnDef<Channel>[] = [
+        {
+            accessorKey: 'id',
+            header: () => <div className="font-medium py-3.5 whitespace-nowrap">{t("channel.id")}</div>,
+            cell: ({ row }) => <div className="font-medium">{row.original.id}</div>,
+        },
+        {
+            accessorKey: 'name',
+            header: () => <div className="font-medium py-3.5 whitespace-nowrap">{t("channel.name")}</div>,
+            cell: ({ row }) => <div className="font-medium">{row.original.name}</div>,
+        },
+        {
+            accessorKey: 'type',
+            header: () => <div className="font-medium py-3.5 whitespace-nowrap">{t("channel.type")}</div>,
+            cell: ({ row }) => (
+                <div className="font-medium">
+                    {getChannelTypeName(row.original.type)}
+                </div>
+            ),
+        },
+        {
+            accessorKey: 'request_count',
+            header: () => <div className="font-medium py-3.5 whitespace-nowrap">{t("channel.requestCount")}</div>,
+            cell: ({ row }) => <div>{row.original.request_count}</div>,
+        },
+        {
+            accessorKey: 'status',
+            header: () => <div className="font-medium py-3.5 whitespace-nowrap">{t("channel.status")}</div>,
+            cell: ({ row }) => (
+                <div>
+                    {row.original.status === 1 ? (
+                        <Badge variant="outline" className={cn(
+                            "text-white dark:text-white/90",
+                            "bg-destructive dark:bg-red-600/90"
+                        )}>
+                            {t("token.disabled")}
+                        </Badge>
+                    ) : (
+                        <Badge variant="outline" className={cn(
+                            "text-white dark:text-white/90",
+                            "bg-primary dark:bg-[#4A4DA0]"
+                        )}>
+                            {t("token.enabled")}
+                        </Badge>
+                    )}
+                </div>
+            ),
+        },
+        {
+            id: 'actions',
+            cell: ({ row }) => (
+                <DropdownMenu>
+                    <DropdownMenuTrigger asChild>
+                        <Button variant="ghost" size="icon">
+                            <MoreHorizontal className="h-4 w-4" />
+                        </Button>
+                    </DropdownMenuTrigger>
+                    <DropdownMenuContent align="end">
+                        <DropdownMenuItem
+                            onClick={() => openUpdateDialog(row.original)}
+                        >
+                            <Pencil className="mr-2 h-4 w-4" />
+                            {t("channel.edit")}
+                        </DropdownMenuItem>
+                        <DropdownMenuItem
+                            onClick={() => handleStatusChange(row.original.id, row.original.status)}
+                            disabled={isStatusUpdating}
+                        >
+                            {row.original.status === 1 ? (
+                                <>
+                                    <Power className="mr-2 h-4 w-4 text-emerald-600 dark:text-emerald-500" />
+                                    {t("channel.enable")}
+                                </>
+                            ) : (
+                                <>
+                                    <PowerOff className="mr-2 h-4 w-4 text-yellow-600 dark:text-yellow-500" />
+                                    {t("channel.disable")}
+                                </>
+                            )}
+                        </DropdownMenuItem>
+                        <DropdownMenuItem
+                            onClick={() => openDeleteDialog(row.original.id)}
+                        >
+                            <Trash2 className="mr-2 h-4 w-4 text-red-600 dark:text-red-500" />
+                            {t("channel.delete")}
+                        </DropdownMenuItem>
+                    </DropdownMenuContent>
+                </DropdownMenu>
+            ),
+        },
+    ]
+
+    // 初始化表格
+    const table = useReactTable({
+        data: flatData,
+        columns,
+        getCoreRowModel: getCoreRowModel(),
+    })
+
+    return (
+        <>
+            <Card className="border-none shadow-none p-6 flex flex-col h-full">
+                {/* 标题和操作按钮 - 固定在顶部 */}
+                <div className="flex items-center justify-between mb-6">
+                    <h2 className="text-xl font-semibold text-primary dark:text-[#6A6DE6]">{t("channel.management")}</h2>
+                    <div className="flex gap-2">
+                        <AnimatedButton>
+                            <Button
+                                variant="outline"
+                                size="sm"
+                                onClick={refreshChannels}
+                                className="flex items-center gap-2 justify-center"
+                            >
+                                <AnimatedIcon animationVariant="continuous-spin" isAnimating={isRefreshAnimating} className="h-4 w-4">
+                                    <RefreshCcw className="h-4 w-4" />
+                                </AnimatedIcon>
+                                {t("channel.refresh")}
+                            </Button>
+                        </AnimatedButton>
+                        <AnimatedButton>
+                            <Button
+                                size="sm"
+                                onClick={openCreateDialog}
+                                className="flex items-center gap-1 bg-primary hover:bg-primary/90 dark:bg-[#4A4DA0] dark:hover:bg-[#5155A5]"
+                            >
+                                <Plus className="h-3.5 w-3.5" />
+                                {t("channel.add")}
+                            </Button>
+                        </AnimatedButton>
+                    </div>
+                </div>
+
+                {/* 表格容器 - 设置固定高度和滚动 */}
+                <div className="flex-1 overflow-hidden flex flex-col">
+                    <div className="overflow-auto h-full">
+                        <DataTable
+                            table={table}
+                            loadingStyle="skeleton"
+                            columns={columns}
+                            isLoading={isLoading}
+                            fixedHeader={true}
+                            animatedRows={true}
+                            showScrollShadows={true}
+                        />
+
+                        {/* 无限滚动监测元素 - 在滚动区域内 */}
+                        {hasNextPage && <div
+                            ref={sentinelRef}
+                            className="h-5 flex justify-center items-center mt-4"
+                        >
+                            {isFetchingNextPage && (
+                                <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
+                            )}
+                        </div>}
+                    </div>
+                </div>
+            </Card>
+
+            {/* 渠道对话框 */}
+            <ChannelDialog
+                open={channelDialogOpen}
+                onOpenChange={setChannelDialogOpen}
+                mode={dialogMode}
+                channel={selectedChannel}
+            />
+
+            {/* 删除渠道对话框 */}
+            <DeleteChannelDialog
+                open={deleteDialogOpen}
+                onOpenChange={setDeleteDialogOpen}
+                channelId={selectedChannelId}
+                onDeleted={() => setSelectedChannelId(null)}
+            />
+        </>
+    )
+}

+ 93 - 0
web/src/feature/channel/components/DeleteChannelDialog.tsx

@@ -0,0 +1,93 @@
+// src/feature/channel/components/DeleteChannelDialog.tsx
+import {
+    AlertDialog,
+    AlertDialogAction,
+    AlertDialogCancel,
+    AlertDialogContent,
+    AlertDialogDescription,
+    AlertDialogFooter,
+    AlertDialogHeader,
+    AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+import { useDeleteChannel } from '../hooks'
+import { AnimatePresence, motion } from "motion/react"
+import { useTranslation } from 'react-i18next'
+import {
+    dialogEnterExitAnimation,
+    dialogContentAnimation,
+    dialogHeaderAnimation,
+    dialogContentItemAnimation
+} from '@/components/ui/animation/dialog-animation'
+import { AnimatedButton } from "@/components/ui/animation/components/animated-button"
+
+interface DeleteChannelDialogProps {
+    open: boolean
+    onOpenChange: (open: boolean) => void
+    channelId: number | null
+    onDeleted?: () => void
+}
+
+export function DeleteChannelDialog({
+    open,
+    onOpenChange,
+    channelId,
+    onDeleted
+}: DeleteChannelDialogProps) {
+    const { t } = useTranslation()
+    const { deleteChannel, isLoading } = useDeleteChannel()
+
+    // Handle delete channel
+    const handleDeleteChannel = () => {
+        if (!channelId) return
+
+        deleteChannel(channelId, {
+            onSettled: () => {
+                onOpenChange(false)
+                onDeleted?.()
+            }
+        })
+    }
+
+    return (
+        <AlertDialog open={open} onOpenChange={onOpenChange}>
+            <AnimatePresence mode="wait">
+                {open && (
+                    <motion.div {...dialogEnterExitAnimation}>
+                        <AlertDialogContent className="p-0 overflow-hidden">
+                            <motion.div {...dialogContentAnimation}>
+                                <motion.div {...dialogHeaderAnimation}>
+                                    <AlertDialogHeader className="p-6 pb-3">
+                                        <AlertDialogTitle className="text-xl">{t("channel.deleteDialog.confirmTitle")}</AlertDialogTitle>
+                                        <AlertDialogDescription>
+                                            {t("channel.deleteDialog.confirmDescription")}
+                                        </AlertDialogDescription>
+                                    </AlertDialogHeader>
+                                </motion.div>
+
+                                <motion.div
+                                    {...dialogContentItemAnimation}
+                                    className="px-6 pb-6"
+                                >
+                                    <AlertDialogFooter className="mt-2 flex justify-end space-x-2">
+                                        <AnimatedButton >
+                                            <AlertDialogCancel>{t("channel.deleteDialog.cancel")}</AlertDialogCancel>
+                                        </AnimatedButton>
+                                        <AnimatedButton >
+                                            <AlertDialogAction
+                                                onClick={handleDeleteChannel}
+                                                disabled={isLoading}
+                                                className="bg-red-600 hover:bg-red-700"
+                                            >
+                                                {isLoading ? t("channel.deleteDialog.deleting") : t("channel.deleteDialog.delete")}
+                                            </AlertDialogAction>
+                                        </AnimatedButton>
+                                    </AlertDialogFooter>
+                                </motion.div>
+                            </motion.div>
+                        </AlertDialogContent>
+                    </motion.div>
+                )}
+            </AnimatePresence>
+        </AlertDialog>
+    )
+}

+ 163 - 0
web/src/feature/channel/hooks.ts

@@ -0,0 +1,163 @@
+// src/feature/channel/hooks.ts
+import { useMutation, useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query'
+import { channelApi } from '@/api/channel'
+import { useState } from 'react'
+import { ChannelCreateRequest, ChannelUpdateRequest, ChannelStatusRequest } from '@/types/channel'
+import { toast } from 'sonner'
+import { ConstantCategory, getConstant } from '@/constant'
+
+// 获取渠道类型元数据
+export const useChannelTypeMetas = () => {
+    const query = useQuery({
+        queryKey: ['channelTypeMetas'],
+        queryFn: channelApi.getTypeMetas,
+    })
+
+    return {
+        ...query,
+    }
+}
+
+// 获取渠道列表(支持无限滚动)
+export const useChannels = () => {
+    const query = useInfiniteQuery({
+        queryKey: ['channels'],
+        queryFn: ({ pageParam }) => channelApi.getChannels(pageParam as number, getConstant(ConstantCategory.CONFIG, 'DEFAULT_PAGE_SIZE', 20)),
+        initialPageParam: 1,
+        getNextPageParam: (lastPage, allPages) => {
+            if (!lastPage || typeof lastPage.total === 'undefined') {
+                return undefined
+            }
+
+            // 检查allPages是否存在
+            if (!allPages) {
+                return undefined
+            }
+
+            // 计算已加载的项目总数
+            const loadedItemsCount = allPages.reduce((count, page) => {
+                return count + (page.channels?.length || 0)
+            }, 0)
+
+            // 如果服务器返回的总数大于已加载的数量,则还有下一页
+            return lastPage.total > loadedItemsCount ? allPages.length + 1 : undefined
+        },
+        enabled: true,
+    })
+
+    return {
+        ...query,
+    }
+}
+
+// 创建渠道
+export const useCreateChannel = () => {
+    const queryClient = useQueryClient()
+    const [error, setError] = useState<ApiError | null>(null)
+
+    const mutation = useMutation({
+        mutationFn: (data: ChannelCreateRequest) => {
+            return channelApi.createChannel(data)
+        },
+        onSuccess: () => {
+            queryClient.invalidateQueries({ queryKey: ['channels'] })
+            setError(null)
+            toast.success('渠道创建成功')
+        },
+        onError: (err: ApiError) => {
+            setError(err)
+            toast.error(err.message || '创建渠道失败')
+        },
+    })
+
+    return {
+        createChannel: mutation.mutate,
+        isLoading: mutation.isPending,
+        error,
+        clearError: () => setError(null),
+    }
+}
+
+// 更新渠道
+export const useUpdateChannel = () => {
+    const queryClient = useQueryClient()
+    const [error, setError] = useState<ApiError | null>(null)
+
+    const mutation = useMutation({
+        mutationFn: ({ id, data }: { id: number, data: ChannelUpdateRequest }) => {
+            return channelApi.updateChannel(id, data)
+        },
+        onSuccess: () => {
+            queryClient.invalidateQueries({ queryKey: ['channels'] })
+            setError(null)
+            toast.success('渠道更新成功')
+        },
+        onError: (err: ApiError) => {
+            setError(err)
+            toast.error(err.message || '更新渠道失败')
+        },
+    })
+
+    return {
+        updateChannel: mutation.mutate,
+        isLoading: mutation.isPending,
+        error,
+        clearError: () => setError(null),
+    }
+}
+
+// 删除渠道
+export const useDeleteChannel = () => {
+    const queryClient = useQueryClient()
+    const [error, setError] = useState<ApiError | null>(null)
+
+    const mutation = useMutation({
+        mutationFn: (id: number) => {
+            return channelApi.deleteChannel(id)
+        },
+        onSuccess: () => {
+            queryClient.invalidateQueries({ queryKey: ['channels'] })
+            setError(null)
+            toast.success('渠道删除成功')
+        },
+        onError: (err: ApiError) => {
+            setError(err)
+            toast.error(err.message || '删除渠道失败')
+        },
+    })
+
+    return {
+        deleteChannel: mutation.mutate,
+        isLoading: mutation.isPending,
+        error,
+        clearError: () => setError(null),
+    }
+}
+
+// 更新渠道状态
+export const useUpdateChannelStatus = () => {
+    const queryClient = useQueryClient()
+    const [error, setError] = useState<ApiError | null>(null)
+
+    const mutation = useMutation({
+        mutationFn: ({ id, status }: { id: number, status: ChannelStatusRequest }) => {
+            return channelApi.updateChannelStatus(id, status)
+        },
+        onSuccess: () => {
+            queryClient.invalidateQueries({ queryKey: ['channels'] })
+            setError(null)
+            toast.success('状态更新成功')
+        },
+        onError: (err: ApiError) => {
+            setError(err)
+            toast.error(err.message || '状态更新失败')
+        },
+    })
+
+    return {
+        updateStatus: mutation.mutate,
+        isLoading: mutation.isPending,
+        error,
+        clearError: () => setError(null),
+    }
+}

+ 93 - 0
web/src/feature/model/components/DeleteModelDialog.tsx

@@ -0,0 +1,93 @@
+// src/feature/model/components/DeleteModelDialog.tsx
+import {
+    AlertDialog,
+    AlertDialogAction,
+    AlertDialogCancel,
+    AlertDialogContent,
+    AlertDialogDescription,
+    AlertDialogFooter,
+    AlertDialogHeader,
+    AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+import { useDeleteModel } from '../hooks'
+import { AnimatePresence, motion } from "motion/react"
+import { useTranslation } from 'react-i18next'
+import {
+    dialogEnterExitAnimation,
+    dialogContentAnimation,
+    dialogHeaderAnimation,
+    dialogContentItemAnimation
+} from '@/components/ui/animation/dialog-animation'
+import { AnimatedButton } from "@/components/ui/animation/components/animated-button"
+
+interface DeleteModelDialogProps {
+    open: boolean
+    onOpenChange: (open: boolean) => void
+    modelId: string | null
+    onDeleted?: () => void
+}
+
+export function DeleteModelDialog({
+    open,
+    onOpenChange,
+    modelId,
+    onDeleted
+}: DeleteModelDialogProps) {
+    const { t } = useTranslation()
+    const { deleteModel, isLoading } = useDeleteModel()
+
+    // Handle delete model
+    const handleDeleteModel = () => {
+        if (!modelId) return
+
+        deleteModel(modelId, {
+            onSettled: () => {
+                onOpenChange(false)
+                onDeleted?.()
+            }
+        })
+    }
+
+    return (
+        <AlertDialog open={open} onOpenChange={onOpenChange}>
+            <AnimatePresence mode="wait">
+                {open && (
+                    <motion.div {...dialogEnterExitAnimation}>
+                        <AlertDialogContent className="p-0 overflow-hidden">
+                            <motion.div {...dialogContentAnimation}>
+                                <motion.div {...dialogHeaderAnimation}>
+                                    <AlertDialogHeader className="p-6 pb-3">
+                                        <AlertDialogTitle className="text-xl">{t("model.deleteDialog.confirmTitle")}</AlertDialogTitle>
+                                        <AlertDialogDescription>
+                                            {t("model.deleteDialog.confirmDescription")}
+                                        </AlertDialogDescription>
+                                    </AlertDialogHeader>
+                                </motion.div>
+
+                                <motion.div
+                                    {...dialogContentItemAnimation}
+                                    className="px-6 pb-6"
+                                >
+                                    <AlertDialogFooter className="mt-2 flex justify-end space-x-2">
+                                        <AnimatedButton >
+                                            <AlertDialogCancel>{t("model.deleteDialog.cancel")}</AlertDialogCancel>
+                                        </AnimatedButton>
+                                        <AnimatedButton >
+                                            <AlertDialogAction
+                                                onClick={handleDeleteModel}
+                                                disabled={isLoading}
+                                                className="bg-red-600 hover:bg-red-700"
+                                            >
+                                                {isLoading ? t("model.deleteDialog.deleting") : t("model.deleteDialog.delete")}
+                                            </AlertDialogAction>
+                                        </AnimatedButton>
+                                    </AlertDialogFooter>
+                                </motion.div>
+                            </motion.div>
+                        </AlertDialogContent>
+                    </motion.div>
+                )}
+            </AnimatePresence>
+        </AlertDialog>
+    )
+}

+ 84 - 0
web/src/feature/model/components/ModelDialog.tsx

@@ -0,0 +1,84 @@
+// src/feature/model/components/ModelDialog.tsx
+import {
+    Dialog,
+    DialogContent,
+    DialogDescription,
+    DialogHeader,
+    DialogTitle
+} from '@/components/ui/dialog'
+import { ModelForm } from './ModelForm'
+import { ModelConfig } from '@/types/model'
+import { AnimatePresence, motion } from "motion/react"
+import { useTranslation } from 'react-i18next'
+import {
+    dialogEnterExitAnimation,
+    dialogContentAnimation,
+    dialogHeaderAnimation,
+    dialogContentItemAnimation
+} from '@/components/ui/animation/dialog-animation'
+
+interface ModelDialogProps {
+    open: boolean
+    onOpenChange: (open: boolean) => void
+    mode: 'create' | 'update'
+    model?: ModelConfig | null
+}
+
+export function ModelDialog({
+    open,
+    onOpenChange,
+    mode = 'create',
+    model = null
+}: ModelDialogProps) {
+    const { t } = useTranslation()
+
+    // Determine title and description based on mode
+    const title = mode === 'create' ? t("model.dialog.createTitle") : t("model.dialog.updateTitle")
+    const description = mode === 'create'
+        ? t("model.dialog.createDescription")
+        : t("model.dialog.updateDescription")
+
+    // Default values for form
+    const defaultValues = mode === 'update' && model
+        ? {
+            model: model.model,
+            type: model.type
+        }
+        : {
+            model: '',
+            type: 1
+        }
+
+    return (
+        <Dialog open={open} onOpenChange={onOpenChange}>
+            <AnimatePresence mode="wait">
+                {open && (
+                    <motion.div {...dialogEnterExitAnimation}>
+                        <DialogContent className="max-w-md max-h-[85vh] overflow-y-auto p-0">
+                            <motion.div {...dialogContentAnimation}>
+                                <motion.div {...dialogHeaderAnimation}>
+                                    <DialogHeader className="p-6 pb-3">
+                                        <DialogTitle className="text-xl">{title}</DialogTitle>
+                                        <DialogDescription>{description}</DialogDescription>
+                                    </DialogHeader>
+                                </motion.div>
+
+                                <motion.div
+                                    {...dialogContentItemAnimation}
+                                    className="px-6 pb-6"
+                                >
+                                    <ModelForm
+                                        mode={mode}
+                                        modelId={model?.model}
+                                        defaultValues={defaultValues}
+                                        onSuccess={() => onOpenChange(false)}
+                                    />
+                                </motion.div>
+                            </motion.div>
+                        </DialogContent>
+                    </motion.div>
+                )}
+            </AnimatePresence>
+        </Dialog>
+    )
+}

+ 158 - 0
web/src/feature/model/components/ModelForm.tsx

@@ -0,0 +1,158 @@
+// src/feature/model/components/ModelForm.tsx
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { Input } from '@/components/ui/input'
+import { Button } from '@/components/ui/button'
+import {
+    Form,
+    FormControl,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from '@/components/ui/form'
+import {
+    Select,
+    SelectContent,
+    SelectItem,
+    SelectTrigger,
+    SelectValue,
+} from '@/components/ui/select'
+import { modelCreateSchema } from '@/validation/model'
+import { useCreateModel } from '../hooks'
+import { useTranslation } from 'react-i18next'
+import { ModelCreateForm } from '@/validation/model'
+import { AdvancedErrorDisplay } from '@/components/common/error/errorDisplay'
+import { AnimatedButton } from '@/components/ui/animation/components/animated-button'
+
+interface ModelFormProps {
+    mode?: 'create' | 'update'
+    modelId?: string
+    onSuccess?: () => void
+    defaultValues?: {
+        model: string
+        type: number
+    }
+}
+
+export function ModelForm({
+    mode = 'create',
+    // @ts-expect-error 忽略未使用参数
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    modelId,
+    onSuccess,
+    defaultValues = {
+        model: '',
+        type: 1,
+    },
+}: ModelFormProps) {
+    const { t } = useTranslation()
+
+    // API hooks
+    const {
+        createModel,
+        isLoading,
+        error,
+        clearError
+    } = useCreateModel()
+
+    // Form setup
+    const form = useForm<ModelCreateForm>({
+        resolver: zodResolver(modelCreateSchema),
+        defaultValues,
+    })
+
+    // Form submission handler
+    const handleFormSubmit = (data: ModelCreateForm) => {
+        // Clear previous errors
+        if (clearError) clearError()
+
+        // Prepare data for API
+        const formData = {
+            model: data.model,
+            type: Number(data.type)
+        }
+
+        if (mode === 'create') {
+            createModel(formData, {
+                onSuccess: () => {
+                    // Reset form
+                    form.reset()
+                    // Notify parent component
+                    if (onSuccess) onSuccess()
+                }
+            })
+        }
+    }
+
+    return (
+        <div>
+            <Form {...form}>
+                <form onSubmit={form.handleSubmit(handleFormSubmit)} className="space-y-6">
+                    {/* API error alert */}
+                    {error && (
+                        <AdvancedErrorDisplay error={error} />
+                    )}
+
+                    {/* Model name field */}
+                    <FormField
+                        control={form.control}
+                        name="model"
+                        render={({ field }) => (
+                            <FormItem>
+                                <FormLabel>{t("model.dialog.modelName")}</FormLabel>
+                                <FormControl>
+                                    <Input placeholder={t("model.dialog.modelNamePlaceholder")} {...field} />
+                                </FormControl>
+                                <FormMessage />
+                            </FormItem>
+                        )}
+                    />
+
+                    {/* Model type field */}
+                    <FormField
+                        control={form.control}
+                        name="type"
+                        render={({ field }) => (
+                            <FormItem>
+                                <FormLabel>{t("model.dialog.modelType")}</FormLabel>
+                                <Select
+                                    onValueChange={(value) => field.onChange(Number(value))}
+                                    defaultValue={String(field.value)}
+                                >
+                                    <FormControl>
+                                        <SelectTrigger>
+                                            <SelectValue placeholder={t("model.dialog.selectType")} />
+                                        </SelectTrigger>
+                                    </FormControl>
+                                    <SelectContent>
+                                        {Array.from({ length: 11 }, (_, i) => i + 1).map((type) => (
+                                            <SelectItem key={type} value={String(type)}>
+                                                {t(`modeType.${type}` as never)}
+                                            </SelectItem>
+                                        ))}
+                                    </SelectContent>
+                                </Select>
+                                <FormMessage />
+                            </FormItem>
+                        )}
+                    />
+
+                    {/* Submit button */}
+                    <div className="flex justify-end">
+                        <AnimatedButton >
+                            <Button type="submit" disabled={isLoading}>
+                                {isLoading
+                                    ? t("model.dialog.submitting")
+                                    : mode === 'create'
+                                        ? t("model.dialog.create")
+                                        : t("model.dialog.update")
+                                }
+                            </Button>
+                        </AnimatedButton>
+                    </div>
+                </form>
+            </Form>
+        </div>
+    )
+}

+ 235 - 0
web/src/feature/model/components/ModelTable.tsx

@@ -0,0 +1,235 @@
+// src/feature/model/components/ModelTable.tsx
+import { useState } from 'react'
+import { useModels } from '../hooks'
+import { ModelConfig } from '@/types/model'
+import { Button } from '@/components/ui/button'
+import {
+    // @ts-expect-error 忽略未使用参数
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    MoreHorizontal, Plus, Trash2, RefreshCcw, Pencil, FileText,
+} from 'lucide-react'
+import {
+    DropdownMenu, DropdownMenuContent,
+    DropdownMenuItem, DropdownMenuTrigger
+} from '@/components/ui/dropdown-menu'
+import { Card } from '@/components/ui/card'
+import { ModelDialog } from './ModelDialog'
+import { DeleteModelDialog } from './DeleteModelDialog'
+import { useTranslation } from 'react-i18next'
+import { DataTable } from '@/components/table/motion-data-table'
+import { ColumnDef } from '@tanstack/react-table'
+import { useReactTable, getCoreRowModel } from '@tanstack/react-table'
+import { AdvancedErrorDisplay } from '@/components/common/error/errorDisplay'
+import { AnimatedButton } from '@/components/ui/animation/components/animated-button'
+import { AnimatedIcon } from '@/components/ui/animation/components/animated-icon'
+import ApiDocDrawer from './api-doc/ApiDoc'
+
+export function ModelTable() {
+    const { t } = useTranslation()
+
+    // State management
+    const [modelDialogOpen, setModelDialogOpen] = useState(false)
+    const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
+    const [selectedModelId, setSelectedModelId] = useState<string | null>(null)
+    const [dialogMode, setDialogMode] = useState<'create' | 'update'>('create')
+    const [selectedModel, setSelectedModel] = useState<ModelConfig | null>(null)
+    const [isRefreshAnimating, setIsRefreshAnimating] = useState(false)
+
+    // API Doc drawer state
+    const [apiDocOpen, setApiDocOpen] = useState(false)
+
+    // Get models list
+    const {
+        data: models,
+        isLoading,
+        error,
+        isError,
+        refetch
+    } = useModels()
+
+    // Create table columns
+    const columns: ColumnDef<ModelConfig>[] = [
+        {
+            accessorKey: 'model',
+            header: () => <div className="font-medium py-3.5">{t("model.modelName")}</div>,
+            cell: ({ row }) => <div className="font-medium">{row.original.model}</div>,
+        },
+        {
+            accessorKey: 'type',
+            header: () => <div className="font-medium py-3.5">{t("model.modelType")}</div>,
+            cell: ({ row }) => (
+                <div className="font-medium">
+                    {/* @ts-expect-error 动态翻译键 */}
+                    {t(`modeType.${row.original.type}`)}
+                </div>
+            ),
+        },
+        // {
+        //     accessorKey: 'owner',
+        //     header: () => <div className="font-medium py-3.5">{t("model.owner")}</div>,
+        //     cell: ({ row }) => <div>{row.original.owner}</div>,
+        // },
+        // {
+        //     accessorKey: 'rpm',
+        //     header: () => <div className="font-medium py-3.5">{t("model.rpm")}</div>,
+        //     cell: ({ row }) => <div>{row.original.rpm}</div>,
+        // },
+        {
+            id: 'actions',
+            cell: ({ row }) => (
+                <DropdownMenu>
+                    <DropdownMenuTrigger asChild>
+                        <Button variant="ghost" size="icon">
+                            <MoreHorizontal className="h-4 w-4" />
+                        </Button>
+                    </DropdownMenuTrigger>
+                    <DropdownMenuContent align="end">
+                        <DropdownMenuItem
+                            onClick={() => openApiDoc(row.original)}
+                        >
+                            <FileText className="mr-2 h-4 w-4" />
+                            {t("model.apiDetails")}
+                        </DropdownMenuItem>
+                        {/* <DropdownMenuItem
+                            onClick={() => openUpdateDialog(row.original)}
+                        >
+                            <Pencil className="mr-2 h-4 w-4" />
+                            {t("model.edit")}
+                        </DropdownMenuItem> */}
+                        <DropdownMenuItem
+                            onClick={() => openDeleteDialog(row.original.model)}
+                        >
+                            <Trash2 className="mr-2 h-4 w-4 text-red-600 dark:text-red-500" />
+                            {t("model.delete")}
+                        </DropdownMenuItem>
+                    </DropdownMenuContent>
+                </DropdownMenu>
+            ),
+        },
+    ]
+
+    // Initialize table
+    const table = useReactTable({
+        data: models || [],
+        columns,
+        getCoreRowModel: getCoreRowModel(),
+    })
+
+    // Open create model dialog
+    const openCreateDialog = () => {
+        setDialogMode('create')
+        setSelectedModel(null)
+        setModelDialogOpen(true)
+    }
+
+    // Open update model dialog
+    // const openUpdateDialog = (model: ModelConfig) => {
+    //     setDialogMode('update')
+    //     setSelectedModel(model)
+    //     setModelDialogOpen(true)
+    // }
+
+    // Open delete dialog
+    const openDeleteDialog = (id: string) => {
+        setSelectedModelId(id)
+        setDeleteDialogOpen(true)
+    }
+
+    // Open API documentation drawer
+    const openApiDoc = (model: ModelConfig) => {
+        setSelectedModel(model)
+        setApiDocOpen(true)
+    }
+
+    // Refresh models
+    const refreshModels = () => {
+        setIsRefreshAnimating(true)
+        refetch()
+
+        // Stop animation after 1 second
+        setTimeout(() => {
+            setIsRefreshAnimating(false)
+        }, 1000)
+    }
+
+    return (
+        <>
+            <Card className="border-none shadow-none p-6 flex flex-col h-full">
+                {/* Title and action buttons */}
+                <div className="flex items-center justify-between mb-6">
+                    <h2 className="text-xl font-semibold text-primary">{t("model.management")}</h2>
+                    <div className="flex gap-2">
+                        <AnimatedButton >
+                            <Button
+                                variant="outline"
+                                size="sm"
+                                onClick={refreshModels}
+                                className="flex items-center gap-2 justify-center"
+                            >
+                                <AnimatedIcon animationVariant="continuous-spin" isAnimating={isRefreshAnimating} className="h-4 w-4">
+                                    <RefreshCcw className="h-4 w-4" />
+                                </AnimatedIcon>
+                                {t("model.refresh")}
+                            </Button>
+                        </AnimatedButton>
+                        <AnimatedButton >
+                            <Button
+                                size="sm"
+                                onClick={openCreateDialog}
+                                className="flex items-center gap-1"
+                            >
+                                <Plus className="h-4 w-4" />
+                                {t("model.add")}
+                            </Button>
+                        </AnimatedButton>
+                    </div>
+                </div>
+
+                {/* Table container */}
+                <div className="flex-1 overflow-hidden flex flex-col">
+                    <div className="overflow-auto h-full">
+                        {isError ? (
+                            <AdvancedErrorDisplay error={error} onRetry={refetch} />
+                        ) : (
+                            <DataTable
+                                table={table}
+                                columns={columns}
+                                isLoading={isLoading}
+                                loadingStyle="skeleton"
+                                fixedHeader={true}
+                                animatedRows={true}
+                                showScrollShadows={true}
+                            />
+                        )}
+                    </div>
+                </div>
+            </Card>
+
+            {/* Model Dialog */}
+            <ModelDialog
+                open={modelDialogOpen}
+                onOpenChange={setModelDialogOpen}
+                mode={dialogMode}
+                model={selectedModel}
+            />
+
+            {/* Delete Model Dialog */}
+            <DeleteModelDialog
+                open={deleteDialogOpen}
+                onOpenChange={setDeleteDialogOpen}
+                modelId={selectedModelId}
+                onDeleted={() => setSelectedModelId(null)}
+            />
+
+            {/* API Documentation Drawer */}
+
+            {selectedModel && (
+                <ApiDocDrawer
+                    isOpen={apiDocOpen}
+                    onClose={() => setApiDocOpen(false)}
+                    modelConfig={selectedModel}
+                />
+            )}
+        </>
+    )
+}

+ 480 - 0
web/src/feature/model/components/api-doc/ApiDoc.tsx

@@ -0,0 +1,480 @@
+import React from 'react'
+import {
+    Sheet,
+    SheetContent,
+} from "@/components/ui/sheet"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { ModelConfig } from '@/types/model'
+import { useTranslation } from 'react-i18next'
+import CodeBlock from './CodeHight'
+import { toast } from 'sonner'
+import { TFunction } from 'i18next'
+
+interface ApiDocContent {
+    title: string
+    endpoint: string
+    method: string
+    requestExample: string
+    responseExample: string
+    responseFormat: string
+    requestAdditionalInfo?: {
+        voices?: string[]
+        formats?: string[]
+    }
+    responseAdditionalInfo?: {
+        voices?: string[]
+        formats?: string[]
+    }
+}
+
+interface ApiDocDrawerProps {
+    isOpen: boolean
+    onClose: () => void
+    modelConfig: ModelConfig
+}
+
+const getApiDocContent = (
+    modelConfig: ModelConfig,
+    apiEndpoint: string,
+    t: TFunction
+): ApiDocContent => {
+    switch (modelConfig.type) {
+        case 1:
+            return {
+                title: t('modeType.1'),
+                endpoint: '/chat/completions',
+                method: 'POST',
+                responseFormat: 'json',
+                requestExample: `curl --request POST \\
+--url ${apiEndpoint}/v1/chat/completions \\
+--header "Authorization: Bearer $token" \\
+--header 'Content-Type: application/json' \\
+--data '{
+  "model": "${modelConfig.model}",
+  "messages": [
+    {
+      "role": "user",
+      "content": "What is Sealos"
+    }
+  ],
+  "stream": false,
+  "max_tokens": 512,
+  "temperature": 0.7
+}'`,
+                responseExample: `{
+  "object": "chat.completion",
+  "created": 1729672480,
+  "model": "${modelConfig.model}",
+  "choices": [
+    {
+      "index": 0,
+      "message": {
+        "role": "assistant",
+        "content": "Sealos is a cloud operating system based on Kubernetes, designed to provide users with a simple, efficient, and scalable cloud-native application deployment and management experience."
+      },
+      "finish_reason": "stop"
+    }
+  ],
+  "usage": {
+    "prompt_tokens": 18,
+    "completion_tokens": 52,
+    "total_tokens": 70
+  }
+}`
+            }
+        case 3:
+            return {
+                title: t('modeType.3'),
+                endpoint: '/embeddings',
+                method: 'POST',
+                responseFormat: 'json',
+                requestExample: `curl --request POST \\
+--url ${apiEndpoint}/v1/embeddings \\
+--header "Authorization: Bearer $token" \\
+--header 'Content-Type: application/json' \\
+--data '{
+  "model": "${modelConfig.model}",
+  "input": "Your text string goes here",
+  "encoding_format": "float"
+}'`,
+                responseExample: `{
+  "object": "list",
+  "model": "${modelConfig.model}",
+  "data": [
+    {
+      "object": "embedding",
+      "embedding": [
+        -0.1082494854927063,
+        0.022976752370595932
+        ...
+      ],
+      "index": 0
+    }
+  ],
+  "usage": {
+    "prompt_tokens": 4,
+    "completion_tokens": 0,
+    "total_tokens": 4
+  }
+}`
+            }
+        case 7:
+            return {
+                title: t('modeType.7'),
+                endpoint: '/audio/speech',
+                method: 'POST',
+                responseFormat: 'binary',
+                requestExample: `curl --request POST \\
+--url ${apiEndpoint}/v1/audio/speech \\
+--header "Authorization: Bearer $token" \\
+--header 'Content-Type: application/json' \\
+--data '{
+  "model": "${modelConfig.model}",
+  "input": "The text to generate audio for",
+${modelConfig?.config?.support_voices?.length
+                        ? `  "voice": "${modelConfig.config.support_voices[0]}",\n`
+                        : ''
+                    }${modelConfig?.config?.support_formats?.length
+                        ? `  "response_format": "${modelConfig.config.support_formats[0]}",\n`
+                        : ''
+                    }  "stream": true,
+  "speed": 1
+}' > audio.mp3`,
+                responseExample: 'Binary audio data',
+                requestAdditionalInfo: {
+                    voices: modelConfig?.config?.support_voices,
+                    formats: modelConfig?.config?.support_formats
+                }
+            }
+        case 8:
+            return {
+                title: t('modeType.8'),
+                endpoint: '/audio/transcriptions',
+                method: 'POST',
+                responseFormat: 'json',
+                requestExample: `curl --request POST \\
+--url ${apiEndpoint}/v1/audio/transcriptions \\
+--header "Authorization: Bearer $token" \\
+--header 'Content-Type: multipart/form-data' \\
+--form model=${modelConfig.model} \\
+--form 'file=@"audio.mp3"'`,
+                responseExample: `{
+  "text": "<string>"
+}`
+            }
+        case 10:
+            return {
+                title: t('modeType.10'),
+                endpoint: '/rerank',
+                method: 'POST',
+                responseFormat: 'json',
+                requestExample: `curl --request POST \\
+--url ${apiEndpoint}/v1/rerank \\
+--header "Authorization: Bearer $token" \\
+--header 'Content-Type: application/json' \\
+--data '{
+  "model": "${modelConfig.model}",
+  "query": "Apple",
+  "documents": [
+    "Apple",
+    "Banana",
+    "Fruit",
+    "Vegetable"
+  ],
+  "top_n": 4,
+  "return_documents": false,
+  "max_chunks_per_doc": 1024,
+  "overlap_tokens": 80
+}'`,
+                responseExample: `{
+  "results": [
+    {
+      "index": 0,
+      "relevance_score": 0.9953725
+    },
+    {
+      "index": 2,
+      "relevance_score": 0.002157342
+    },
+    {
+      "index": 1,
+      "relevance_score": 0.00046371284
+    },
+    {
+      "index": 3,
+      "relevance_score": 0.000017502925
+    }
+  ],
+  "meta": {
+    "tokens": {
+      "input_tokens": 28
+    }
+  }
+}`
+            }
+        case 11:
+            return {
+                title: t('modeType.11'),
+                endpoint: '/parse/pdf',
+                method: 'POST',
+                responseFormat: 'json',
+                requestExample: `curl --request POST \\
+--url ${apiEndpoint}/v1/parse/pdf \\
+--header "Authorization: Bearer $token" \\
+--header 'Content-Type: multipart/form-data' \\
+--form model=${modelConfig.model} \\
+--form 'file=@"test.pdf"'`,
+                responseExample: `{
+  "pages": 1,
+  "markdown": "sf ad fda daf da \\\\( f \\\\) ds f sd fs d afdas fsd asfad f\\n\\n\\n\\n![img](data:image/jpeg;base64,/9...)\\n\\n| sadsa |  |  |\\n| --- | --- | --- |\\n|  | sadasdsa | sad |\\n|  |  | dsadsadsa |\\n|  |  |  |\\n\\n\\n\\na fda"
+}`
+            }
+        default:
+            return {
+                title: t('modeType.0'),
+                endpoint: '',
+                method: '',
+                responseFormat: 'json',
+                requestExample: 'unknown',
+                responseExample: 'unknown'
+            }
+    }
+}
+
+const ApiDocDrawer: React.FC<ApiDocDrawerProps> = ({ isOpen, onClose, modelConfig }) => {
+    const { t } = useTranslation()
+    const apiDoc = getApiDocContent(modelConfig, "", t)
+
+    return (
+        <Sheet open={isOpen} onOpenChange={(open) => {
+            if (!open) onClose()
+        }}>
+            <SheetContent 
+                side="right"
+                className="p-0 overflow-hidden w-[400px] max-w-md"
+            >
+                <div className="flex flex-col gap-3 overflow-y-auto p-6 pb-3 h-[calc(100%-48px)]">
+                    {/* method */}
+                    <div className="flex gap-2.5 items-center">
+                        <Badge className="flex px-2 py-0.5 justify-center items-center gap-0.5 rounded bg-blue-100 dark:bg-blue-900">
+                            <span className="text-blue-600 dark:text-blue-300 font-medium text-xs leading-4 tracking-wide">
+                                {apiDoc.method}
+                            </span>
+                        </Badge>
+                        <svg
+                            xmlns="http://www.w3.org/2000/svg"
+                            width="2"
+                            height="18"
+                            viewBox="0 0 2 18"
+                            fill="none">
+                            <path d="M1 1L1 17" stroke="#F0F1F6" strokeLinecap="round" className="dark:stroke-zinc-700" />
+                        </svg>
+
+                        <div className="flex gap-1 items-center justify-start">
+                            {apiDoc.endpoint
+                                .split('/')
+                                .filter(Boolean)
+                                .map((segment, index) => (
+                                    <React.Fragment key={index}>
+                                        <svg
+                                            xmlns="http://www.w3.org/2000/svg"
+                                            width="5"
+                                            height="12"
+                                            viewBox="0 0 5 12"
+                                            fill="none">
+                                            <path
+                                                d="M4.42017 1.30151L0.999965 10.6984"
+                                                stroke="#C4CBD7"
+                                                strokeLinecap="round"
+                                                className="dark:stroke-zinc-600"
+                                            />
+                                        </svg>
+                                        <span className="text-gray-600 dark:text-gray-400 font-medium text-xs leading-4 tracking-wide">
+                                            {segment}
+                                        </span>
+                                    </React.Fragment>
+                                ))}
+                        </div>
+                    </div>
+
+                    {/* request example and response example */}
+                    <div className="flex flex-col gap-4 items-start w-full">
+                        {/* request example */}
+                        <div className="flex flex-col gap-2 items-start w-full">
+                            <span className="text-gray-900 dark:text-gray-100 font-medium text-xs leading-4 tracking-wide">
+                                {t('apiDoc.requestExample')}
+                            </span>
+
+                            {/* code */}
+                            <div className="flex flex-col items-start justify-center w-full rounded-md overflow-hidden">
+                                <div className="flex w-full p-2.5 justify-between items-center bg-[#232833]">
+                                    <span className="text-white font-medium text-xs leading-4 tracking-wide">
+                                        {'bash'}
+                                    </span>
+
+                                    <Button
+                                        onClick={() => {
+                                            navigator.clipboard.writeText(apiDoc.requestExample).then(
+                                                () => {
+                                                    toast.success(t('common.copied'))
+                                                },
+                                                (err) => {
+                                                    toast.error(err?.message || t('common.copyFailed'))
+                                                }
+                                            )
+                                        }}
+                                        variant="ghost"
+                                        size="icon"
+                                        className="inline-flex p-1 min-w-0 h-[22px] w-[22px] justify-center items-center rounded bg-transparent hover:bg-white/10">
+                                        <svg
+                                            xmlns="http://www.w3.org/2000/svg"
+                                            width="14"
+                                            height="14"
+                                            viewBox="0 0 14 14"
+                                            fill="none">
+                                            <path
+                                                fillRule="evenodd"
+                                                clipRule="evenodd"
+                                                d="M2.86483 2.30131C2.73937 2.30131 2.61904 2.35115 2.53032 2.43987C2.44161 2.52859 2.39176 2.64891 2.39176 2.77438V7.5282C2.39176 7.65366 2.44161 7.77399 2.53032 7.86271C2.61904 7.95142 2.73937 8.00127 2.86483 8.00127H3.39304C3.7152 8.00127 3.97637 8.26243 3.97637 8.5846C3.97637 8.90676 3.7152 9.16793 3.39304 9.16793H2.86483C2.42995 9.16793 2.01288 8.99517 1.70537 8.68766C1.39786 8.38015 1.2251 7.96308 1.2251 7.5282V2.77438C1.2251 2.3395 1.39786 1.92242 1.70537 1.61491C2.01288 1.3074 2.42995 1.13464 2.86483 1.13464H7.61865C8.05354 1.13464 8.47061 1.3074 8.77812 1.61491C9.08563 1.92242 9.25839 2.33949 9.25839 2.77438V3.30258C9.25839 3.62475 8.99722 3.88592 8.67505 3.88592C8.35289 3.88592 8.09172 3.62475 8.09172 3.30258V2.77438C8.09172 2.64891 8.04188 2.52859 7.95316 2.43987C7.86444 2.35115 7.74412 2.30131 7.61865 2.30131H2.86483ZM6.56225 5.99872C6.30098 5.99872 6.08918 6.21052 6.08918 6.47179V11.2256C6.08918 11.4869 6.30098 11.6987 6.56225 11.6987H11.3161C11.5773 11.6987 11.7891 11.4869 11.7891 11.2256V6.47179C11.7891 6.21052 11.5773 5.99872 11.3161 5.99872H6.56225ZM4.92251 6.47179C4.92251 5.56619 5.65664 4.83206 6.56225 4.83206H11.3161C12.2217 4.83206 12.9558 5.56619 12.9558 6.47179V11.2256C12.9558 12.1312 12.2217 12.8653 11.3161 12.8653H6.56225C5.65664 12.8653 4.92251 12.1312 4.92251 11.2256V6.47179Z"
+                                                fill="white"
+                                                fillOpacity="0.8"
+                                            />
+                                        </svg>
+                                    </Button>
+                                </div>
+                                <div className="p-3 bg-[#14181E] w-full">
+                                    <CodeBlock code={apiDoc.requestExample} language="bash" />
+                                </div>
+                            </div>
+
+                            {/* additional info */}
+                            {apiDoc?.requestAdditionalInfo?.voices &&
+                                apiDoc?.requestAdditionalInfo?.voices?.length > 0 && (
+                                    <div className="flex flex-col p-2.5 w-full gap-2 items-start rounded-md border border-gray-200 dark:border-gray-700">
+                                        <div className="flex gap-2">
+                                            <span className="text-blue-600 dark:text-blue-400 font-medium text-xs leading-4 tracking-wide">
+                                                {'voice'}
+                                            </span>
+                                            <div className="flex gap-1">
+                                                <Badge variant="outline" className="bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 font-medium text-xs">
+                                                    {'enum<string>'}
+                                                </Badge>
+                                                <Badge className="bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 font-medium text-xs border-0">
+                                                    {t('apiDoc.voice')}
+                                                </Badge>
+                                            </div>
+                                        </div>
+
+                                        <div className="flex flex-col gap-1 items-start w-full">
+                                            <span className="text-gray-500 dark:text-gray-400 font-medium text-xs leading-4 tracking-wide">
+                                                {t('apiDoc.voiceValues')}
+                                            </span>
+
+                                            <div className="flex flex-wrap gap-2">
+                                                {apiDoc?.requestAdditionalInfo?.voices?.map((voice) => (
+                                                    <Badge
+                                                        key={voice}
+                                                        variant="outline"
+                                                        className="bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-300 font-medium text-xs">
+                                                        {voice}
+                                                    </Badge>
+                                                ))}
+                                            </div>
+                                        </div>
+                                    </div>
+                                )}
+
+                            {apiDoc?.requestAdditionalInfo?.formats &&
+                                apiDoc?.requestAdditionalInfo?.formats?.length > 0 && (
+                                    <div className="flex flex-col p-2.5 w-full gap-2 items-start rounded-md border border-gray-200 dark:border-gray-700">
+                                        <div className="flex gap-2">
+                                            <span className="text-blue-600 dark:text-blue-400 font-medium text-xs leading-4 tracking-wide">
+                                                {'response_format'}
+                                            </span>
+                                            <div className="flex gap-1">
+                                                <Badge variant="outline" className="bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 font-medium text-xs">
+                                                    {'enum<string>'}
+                                                </Badge>
+                                                <Badge variant="outline" className="bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 font-medium text-xs">
+                                                    {'default:mp3'}
+                                                </Badge>
+                                            </div>
+                                        </div>
+
+                                        <div className="flex flex-col gap-1 items-start w-full">
+                                            <span className="text-gray-500 dark:text-gray-400 font-medium text-xs leading-4 tracking-wide">
+                                                {t('apiDoc.responseFormatValues')}
+                                            </span>
+
+                                            <div className="flex flex-wrap gap-2">
+                                                {apiDoc?.requestAdditionalInfo?.formats?.map((format) => (
+                                                    <Badge
+                                                        key={format}
+                                                        variant="outline"
+                                                        className="bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-300 font-medium text-xs">
+                                                        {format}
+                                                    </Badge>
+                                                ))}
+                                            </div>
+                                        </div>
+                                    </div>
+                                )}
+                        </div>
+
+                        {/* response example */}
+                        <div className="flex flex-col gap-2 items-start w-full">
+                            <span className="text-gray-900 dark:text-gray-100 font-medium text-xs leading-4 tracking-wide">
+                                {t('apiDoc.responseExample')}
+                            </span>
+
+                            {/* code */}
+                            <div className="flex flex-col items-start justify-center w-full rounded-md overflow-hidden">
+                                <div className="flex w-full p-2.5 justify-between items-center bg-[#232833]">
+                                    <span className="text-white font-medium text-xs leading-4 tracking-wide">
+                                        {apiDoc.responseFormat}
+                                    </span>
+
+                                    <Button
+                                        onClick={() => {
+                                            navigator.clipboard.writeText(apiDoc.responseExample).then(
+                                                () => {
+                                                    toast.success(t('common.copied'))
+                                                },
+                                                (err) => {
+                                                    toast.error(err?.message || t('common.copyFailed'))
+                                                }
+                                            )
+                                        }}
+                                        variant="ghost"
+                                        size="icon"
+                                        className="inline-flex p-1 min-w-0 h-[22px] w-[22px] justify-center items-center rounded bg-transparent hover:bg-white/10">
+                                        <svg
+                                            xmlns="http://www.w3.org/2000/svg"
+                                            width="14"
+                                            height="14"
+                                            viewBox="0 0 14 14"
+                                            fill="none">
+                                            <path
+                                                fillRule="evenodd"
+                                                clipRule="evenodd"
+                                                d="M2.86483 2.30131C2.73937 2.30131 2.61904 2.35115 2.53032 2.43987C2.44161 2.52859 2.39176 2.64891 2.39176 2.77438V7.5282C2.39176 7.65366 2.44161 7.77399 2.53032 7.86271C2.61904 7.95142 2.73937 8.00127 2.86483 8.00127H3.39304C3.7152 8.00127 3.97637 8.26243 3.97637 8.5846C3.97637 8.90676 3.7152 9.16793 3.39304 9.16793H2.86483C2.42995 9.16793 2.01288 8.99517 1.70537 8.68766C1.39786 8.38015 1.2251 7.96308 1.2251 7.5282V2.77438C1.2251 2.3395 1.39786 1.92242 1.70537 1.61491C2.01288 1.3074 2.42995 1.13464 2.86483 1.13464H7.61865C8.05354 1.13464 8.47061 1.3074 8.77812 1.61491C9.08563 1.92242 9.25839 2.33949 9.25839 2.77438V3.30258C9.25839 3.62475 8.99722 3.88592 8.67505 3.88592C8.35289 3.88592 8.09172 3.62475 8.09172 3.30258V2.77438C8.09172 2.64891 8.04188 2.52859 7.95316 2.43987C7.86444 2.35115 7.74412 2.30131 7.61865 2.30131H2.86483ZM6.56225 5.99872C6.30098 5.99872 6.08918 6.21052 6.08918 6.47179V11.2256C6.08918 11.4869 6.30098 11.6987 6.56225 11.6987H11.3161C11.5773 11.6987 11.7891 11.4869 11.7891 11.2256V6.47179C11.7891 6.21052 11.5773 5.99872 11.3161 5.99872H6.56225ZM4.92251 6.47179C4.92251 5.56619 5.65664 4.83206 6.56225 4.83206H11.3161C12.2217 4.83206 12.9558 5.56619 12.9558 6.47179V11.2256C12.9558 12.1312 12.2217 12.8653 11.3161 12.8653H6.56225C5.65664 12.8653 4.92251 12.1312 4.92251 11.2256V6.47179Z"
+                                                fill="white"
+                                                fillOpacity="0.8"
+                                            />
+                                        </svg>
+                                    </Button>
+                                </div>
+                                <div className="p-3 bg-[#14181E] w-full">
+                                    <CodeBlock code={apiDoc.responseExample} language={apiDoc.responseFormat === 'json' ? 'json' : 'text'} />
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </SheetContent>
+        </Sheet>
+    )
+}
+
+export default ApiDocDrawer

+ 59 - 0
web/src/feature/model/components/api-doc/CodeHight.tsx

@@ -0,0 +1,59 @@
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
+import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
+
+const CodeBlock = ({ code, language = 'bash' }: { code: string; language?: string }) => {
+    const customizedStyle = {
+        ...atomDark,
+        'pre[class*="language-"]': {
+            ...atomDark['pre[class*="language-"]'],
+            backgroundColor: 'transparent',
+            margin: 0,
+            padding: 0
+        }
+    }
+
+    return (
+        <div
+            className="overflow-x-auto"
+            style={{
+                msOverflowStyle: 'none',
+                scrollbarWidth: 'none',
+            }}>
+            <style dangerouslySetInnerHTML={{
+                __html: `
+                div::-webkit-scrollbar {
+                    width: 0;
+                    height: 0;
+                }
+                div pre::-webkit-scrollbar {
+                    width: 0;
+                    height: 0;
+                }
+                div code::-webkit-scrollbar {
+                    width: 0;
+                    height: 0;
+                }
+            `}} />
+            <SyntaxHighlighter
+                language={language}
+                style={customizedStyle}
+                customStyle={{
+                    fontSize: '12px',
+                    overflowX: 'auto',
+                    msOverflowStyle: 'none',
+                    scrollbarWidth: 'none'
+                }}
+                codeTagProps={{
+                    style: {
+                        color: 'white'
+                    }
+                }}
+                wrapLines={false}
+                lineProps={{ style: { whiteSpace: 'pre' } }}>
+                {code}
+            </SyntaxHighlighter>
+        </div>
+    )
+}
+
+export default CodeBlock

+ 85 - 0
web/src/feature/model/hooks.ts

@@ -0,0 +1,85 @@
+// src/feature/model/hooks.ts
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import { modelApi } from '@/api/model'
+import { useState } from 'react'
+import { ModelCreateRequest } from '@/types/model'
+import { toast } from 'sonner'
+
+// Get all models
+export const useModels = () => {
+    const query = useQuery({
+        queryKey: ['models'],
+        queryFn: modelApi.getModels,
+    })
+
+    return {
+        ...query,
+    }
+}
+
+// Get a specific model
+export const useModel = (model: string) => {
+    const query = useQuery({
+        queryKey: ['model', model],
+        queryFn: () => modelApi.getModel(model),
+        enabled: !!model,
+    })
+
+    return {
+        ...query,
+    }
+}
+
+// Create a new model
+export const useCreateModel = () => {
+    const queryClient = useQueryClient()
+    const [error, setError] = useState<ApiError | null>(null)
+
+    const mutation = useMutation({
+        mutationFn: (data: ModelCreateRequest) => {
+            return modelApi.createModel(data)
+        },
+        onSuccess: () => {
+            queryClient.invalidateQueries({ queryKey: ['models'] })
+            setError(null)
+        },
+        onError: (err: ApiError) => {
+            setError(err)
+            toast.error(err.message)
+        },
+    })
+
+    return {
+        createModel: mutation.mutate,
+        isLoading: mutation.isPending,
+        error,
+        clearError: () => setError(null),
+    }
+}
+
+// Delete a model
+export const useDeleteModel = () => {
+    const queryClient = useQueryClient()
+    const [error, setError] = useState<ApiError | null>(null)
+
+    const mutation = useMutation({
+        mutationFn: (model: string) => {
+            return modelApi.deleteModel(model)
+        },
+        onSuccess: () => {
+            queryClient.invalidateQueries({ queryKey: ['models'] })
+            setError(null)
+        },
+        onError: (err: ApiError) => {
+            setError(err)
+            toast.error(err.message)
+        },
+    })
+
+    return {
+        deleteModel: mutation.mutate,
+        isLoading: mutation.isPending,
+        error,
+        clearError: () => setError(null),
+    }
+}

+ 93 - 0
web/src/feature/token/components/DeleteTokenDialog.tsx

@@ -0,0 +1,93 @@
+// src/feature/token/components/DeleteTokenDialog.tsx
+import {
+    AlertDialog,
+    AlertDialogAction,
+    AlertDialogCancel,
+    AlertDialogContent,
+    AlertDialogDescription,
+    AlertDialogFooter,
+    AlertDialogHeader,
+    AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+import { useDeleteToken } from '../hooks'
+import { AnimatePresence, motion } from "motion/react"
+import { useTranslation } from 'react-i18next'
+import {
+    dialogEnterExitAnimation,
+    dialogContentAnimation,
+    dialogHeaderAnimation,
+    dialogContentItemAnimation
+} from '@/components/ui/animation/dialog-animation'
+import { AnimatedButton } from "@/components/ui/animation/components/animated-button"
+
+interface DeleteTokenDialogProps {
+    open: boolean
+    onOpenChange: (open: boolean) => void
+    tokenId: number | null
+    onDeleted?: () => void
+}
+
+export function DeleteTokenDialog({
+    open,
+    onOpenChange,
+    tokenId,
+    onDeleted
+}: DeleteTokenDialogProps) {
+    const { t } = useTranslation()
+    const { deleteToken, isLoading } = useDeleteToken()
+
+    // 处理删除token
+    const handleDeleteToken = () => {
+        if (!tokenId) return
+
+        deleteToken(tokenId, {
+            onSettled: () => {
+                onOpenChange(false)
+                onDeleted?.()
+            }
+        })
+    }
+
+    return (
+        <AlertDialog open={open} onOpenChange={onOpenChange}>
+            <AnimatePresence mode="wait">
+                {open && (
+                    <motion.div {...dialogEnterExitAnimation}>
+                        <AlertDialogContent className="p-0 overflow-hidden">
+                            <motion.div {...dialogContentAnimation}>
+                                <motion.div {...dialogHeaderAnimation}>
+                                    <AlertDialogHeader className="p-6 pb-3">
+                                        <AlertDialogTitle className="text-xl">{t("token.deleteDialog.confirmTitle")}</AlertDialogTitle>
+                                        <AlertDialogDescription>
+                                            {t("token.deleteDialog.confirmDescription")}
+                                        </AlertDialogDescription>
+                                    </AlertDialogHeader>
+                                </motion.div>
+
+                                <motion.div
+                                    {...dialogContentItemAnimation}
+                                    className="px-6 pb-6"
+                                >
+                                    <AlertDialogFooter className="mt-2 flex justify-end space-x-2">
+                                        <AnimatedButton>
+                                            <AlertDialogCancel>{t("token.deleteDialog.cancel")}</AlertDialogCancel>
+                                        </AnimatedButton>
+                                        <AnimatedButton>
+                                            <AlertDialogAction
+                                                onClick={handleDeleteToken}
+                                                disabled={isLoading}
+                                                className="bg-red-600 hover:bg-red-700"
+                                            >
+                                                {isLoading ? t("token.deleteDialog.deleting") : t("token.deleteDialog.delete")}
+                                            </AlertDialogAction>
+                                        </AnimatedButton>
+                                    </AlertDialogFooter>
+                                </motion.div>
+                            </motion.div>
+                        </AlertDialogContent>
+                    </motion.div>
+                )}
+            </AnimatePresence>
+        </AlertDialog>
+    )
+}

Some files were not shown because too many files changed in this diff