diff --git a/README.md b/README.md
index 8a16fc3..16cc42d 100644
--- a/README.md
+++ b/README.md
@@ -441,6 +441,8 @@ If you find this project helpful, please consider sponsoring its development. Yo
[](https://ko-fi.com/F1F31GN2GM)
+[Paypal](https://paypal.me/musistudio1999)
+
diff --git a/README_zh.md b/README_zh.md
index 9bef1ee..371fe2c 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -436,6 +436,8 @@ jobs:
[](https://ko-fi.com/F1F31GN2GM)
+[Paypal](https://paypal.me/musistudio1999)
+
diff --git a/package.json b/package.json
index 371980c..d5e845d 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"dependencies": {
"@fastify/static": "^8.2.0",
"@musistudio/llms": "^1.0.18",
+ "@radix-ui/react-tooltip": "^1.2.7",
"dotenv": "^16.4.7",
"json5": "^2.2.3",
"openurl": "^1.1.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cc6b93e..41d5783 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,9 @@ importers:
'@musistudio/llms':
specifier: ^1.0.18
version: 1.0.18(ws@8.18.3)(zod@3.25.67)
+ '@radix-ui/react-tooltip':
+ specifier: ^1.2.7
+ version: 1.2.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
dotenv:
specifier: ^16.4.7
version: 16.6.1
@@ -232,6 +235,21 @@ packages:
'@fastify/static@8.2.0':
resolution: {integrity: sha512-PejC/DtT7p1yo3p+W7LiUtLMsV8fEvxAK15sozHy9t8kwo5r0uLYmhV/inURmGz1SkHZFz/8CNtHLPyhKcx4SQ==}
+ '@floating-ui/core@1.7.3':
+ resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
+
+ '@floating-ui/dom@1.7.3':
+ resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==}
+
+ '@floating-ui/react-dom@2.1.5':
+ resolution: {integrity: sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@floating-ui/utils@0.2.10':
+ resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
+
'@google/genai@1.8.0':
resolution: {integrity: sha512-n3KiMFesQCy2R9iSdBIuJ0JWYQ1HZBJJkmt4PPZMGZKvlgHhBAGw1kUMyX+vsAIzprN3lK45DI755lm70wPOOg==}
engines: {node: '>=20.0.0'}
@@ -272,6 +290,215 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
+ '@radix-ui/primitive@1.1.2':
+ resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
+
+ '@radix-ui/react-arrow@1.1.7':
+ resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
+ 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/react-compose-refs@1.1.2':
+ 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/react-context@1.1.2':
+ 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/react-dismissable-layer@1.1.10':
+ resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==}
+ 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/react-id@1.1.1':
+ 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/react-popper@1.2.7':
+ resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==}
+ 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/react-portal@1.1.9':
+ resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
+ 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/react-presence@1.1.4':
+ 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/react-primitive@2.1.3':
+ resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
+ 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/react-slot@1.2.3':
+ resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-tooltip@1.2.7':
+ resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==}
+ 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/react-use-callback-ref@1.1.1':
+ 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/react-use-controllable-state@1.2.2':
+ 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/react-use-effect-event@0.0.2':
+ 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/react-use-escape-keydown@1.1.1':
+ 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/react-use-layout-effect@1.1.1':
+ 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/react-use-rect@1.1.1':
+ 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/react-use-size@1.1.1':
+ 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/react-visually-hidden@1.2.3':
+ resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
+ 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/rect@1.1.1':
+ resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+
'@types/node@24.0.15':
resolution: {integrity: sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==}
@@ -729,6 +956,15 @@ packages:
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
+ react-dom@19.1.1:
+ resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}
+ peerDependencies:
+ react: ^19.1.1
+
+ react@19.1.1:
+ resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
+ engines: {node: '>=0.10.0'}
+
real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
@@ -770,6 +1006,9 @@ packages:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
+ scheduler@0.26.0:
+ resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
+
secure-json-parse@4.0.0:
resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==}
@@ -1073,6 +1312,23 @@ snapshots:
fastq: 1.19.1
glob: 11.0.3
+ '@floating-ui/core@1.7.3':
+ dependencies:
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/dom@1.7.3':
+ dependencies:
+ '@floating-ui/core': 1.7.3
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/react-dom@2.1.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@floating-ui/dom': 1.7.3
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+
+ '@floating-ui/utils@0.2.10': {}
+
'@google/genai@1.8.0':
dependencies:
google-auth-library: 9.15.1
@@ -1136,6 +1392,136 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
+ '@radix-ui/primitive@1.1.2': {}
+
+ '@radix-ui/react-arrow@1.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+
+ '@radix-ui/react-compose-refs@1.1.2(react@19.1.1)':
+ dependencies:
+ react: 19.1.1
+
+ '@radix-ui/react-context@1.1.2(react@19.1.1)':
+ dependencies:
+ react: 19.1.1
+
+ '@radix-ui/react-dismissable-layer@1.1.10(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1)
+ '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(react@19.1.1)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+
+ '@radix-ui/react-id@1.1.1(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1)
+ react: 19.1.1
+
+ '@radix-ui/react-popper@1.2.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@floating-ui/react-dom': 2.1.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-arrow': 1.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1)
+ '@radix-ui/react-context': 1.1.2(react@19.1.1)
+ '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(react@19.1.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1)
+ '@radix-ui/react-use-rect': 1.1.1(react@19.1.1)
+ '@radix-ui/react-use-size': 1.1.1(react@19.1.1)
+ '@radix-ui/rect': 1.1.1
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+
+ '@radix-ui/react-portal@1.1.9(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+
+ '@radix-ui/react-presence@1.1.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+
+ '@radix-ui/react-primitive@2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.3(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+
+ '@radix-ui/react-slot@1.2.3(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1)
+ react: 19.1.1
+
+ '@radix-ui/react-tooltip@1.2.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1)
+ '@radix-ui/react-context': 1.1.2(react@19.1.1)
+ '@radix-ui/react-dismissable-layer': 1.1.10(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-id': 1.1.1(react@19.1.1)
+ '@radix-ui/react-popper': 1.2.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-portal': 1.1.9(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-presence': 1.1.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-slot': 1.2.3(react@19.1.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(react@19.1.1)
+ '@radix-ui/react-visually-hidden': 1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+
+ '@radix-ui/react-use-callback-ref@1.1.1(react@19.1.1)':
+ dependencies:
+ react: 19.1.1
+
+ '@radix-ui/react-use-controllable-state@1.2.2(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-use-effect-event': 0.0.2(react@19.1.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1)
+ react: 19.1.1
+
+ '@radix-ui/react-use-effect-event@0.0.2(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1)
+ react: 19.1.1
+
+ '@radix-ui/react-use-escape-keydown@1.1.1(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.1(react@19.1.1)
+ react: 19.1.1
+
+ '@radix-ui/react-use-layout-effect@1.1.1(react@19.1.1)':
+ dependencies:
+ react: 19.1.1
+
+ '@radix-ui/react-use-rect@1.1.1(react@19.1.1)':
+ dependencies:
+ '@radix-ui/rect': 1.1.1
+ react: 19.1.1
+
+ '@radix-ui/react-use-size@1.1.1(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1)
+ react: 19.1.1
+
+ '@radix-ui/react-visually-hidden@1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+
+ '@radix-ui/rect@1.1.1': {}
+
'@types/node@24.0.15':
dependencies:
undici-types: 7.8.0
@@ -1633,6 +2019,13 @@ snapshots:
quick-format-unescaped@4.0.4: {}
+ react-dom@19.1.1(react@19.1.1):
+ dependencies:
+ react: 19.1.1
+ scheduler: 0.26.0
+
+ react@19.1.1: {}
+
real-require@0.2.0: {}
rechoir@0.6.2:
@@ -1665,6 +2058,8 @@ snapshots:
safe-stable-stringify@2.5.0: {}
+ scheduler@0.26.0: {}
+
secure-json-parse@4.0.0: {}
semver@5.7.2: {}
diff --git a/src/cli.ts b/src/cli.ts
index b66a406..114e068 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -2,12 +2,16 @@
import { run } from "./index";
import { showStatus } from "./utils/status";
import { executeCodeCommand } from "./utils/codeCommand";
-import { cleanupPidFile, isServiceRunning, getServiceInfo } from "./utils/processCheck";
+import {
+ cleanupPidFile,
+ isServiceRunning,
+ getServiceInfo,
+} from "./utils/processCheck";
import { version } from "../package.json";
import { spawn, exec } from "child_process";
import { PID_FILE, REFERENCE_COUNT_FILE } from "./constants";
import fs, { existsSync, readFileSync } from "fs";
-import {join} from "path";
+import { join } from "path";
const command = process.argv[2];
@@ -108,7 +112,9 @@ async function main() {
startProcess.unref();
if (await waitForService()) {
- executeCodeCommand(process.argv.slice(3));
+ // Join all code arguments into a single string to preserve spaces within quotes
+ const codeArgs = process.argv.slice(3);
+ executeCodeCommand(codeArgs);
} else {
console.error(
"Service startup timeout, please manually run `ccr start` to start the service"
@@ -116,7 +122,9 @@ async function main() {
process.exit(1);
}
} else {
- executeCodeCommand(process.argv.slice(3));
+ // Join all code arguments into a single string to preserve spaces within quotes
+ const codeArgs = process.argv.slice(3);
+ executeCodeCommand(codeArgs);
}
break;
case "ui":
@@ -138,49 +146,68 @@ async function main() {
if (!(await waitForService())) {
// If service startup fails, try to start with default config
- console.log("Service startup timeout, trying to start with default configuration...");
- const { initDir, writeConfigFile, backupConfigFile } = require("./utils");
-
+ console.log(
+ "Service startup timeout, trying to start with default configuration..."
+ );
+ const {
+ initDir,
+ writeConfigFile,
+ backupConfigFile,
+ } = require("./utils");
+
try {
// Initialize directories
await initDir();
-
+
// Backup existing config file if it exists
const backupPath = await backupConfigFile();
if (backupPath) {
- console.log(`Backed up existing configuration file to ${backupPath}`);
+ console.log(
+ `Backed up existing configuration file to ${backupPath}`
+ );
}
-
+
// Create a minimal default config file
await writeConfigFile({
- "PORT": 3456,
- "Providers": [],
- "Router": {}
+ PORT: 3456,
+ Providers: [],
+ Router: {},
});
- console.log("Created minimal default configuration file at ~/.claude-code-router/config.json");
- console.log("Please edit this file with your actual configuration.");
-
+ console.log(
+ "Created minimal default configuration file at ~/.claude-code-router/config.json"
+ );
+ console.log(
+ "Please edit this file with your actual configuration."
+ );
+
// Try starting the service again
const restartProcess = spawn("node", [cliPath, "start"], {
detached: true,
stdio: "ignore",
});
-
+
restartProcess.on("error", (error) => {
- console.error("Failed to start service with default config:", error.message);
+ console.error(
+ "Failed to start service with default config:",
+ error.message
+ );
process.exit(1);
});
-
+
restartProcess.unref();
-
- if (!(await waitForService(15000))) { // Wait a bit longer for the first start
+
+ if (!(await waitForService(15000))) {
+ // Wait a bit longer for the first start
console.error(
"Service startup still failing. Please manually run `ccr start` to start the service and check the logs."
);
process.exit(1);
}
} catch (error: any) {
- console.error("Failed to create default configuration:", error.message);
+ console.error(
+ "Failed to create default configuration:",
+ error.message
+ );
process.exit(1);
}
}
@@ -190,11 +217,11 @@ async function main() {
const serviceInfo = await getServiceInfo();
const uiUrl = `${serviceInfo.endpoint}/ui/`;
console.log(`Opening UI at ${uiUrl}`);
-
+
// Open URL in browser based on platform
const platform = process.platform;
let openCommand = "";
-
+
if (platform === "win32") {
// Windows
openCommand = `start ${uiUrl}`;
@@ -208,7 +235,7 @@ async function main() {
console.error("Unsupported platform for opening browser");
process.exit(1);
}
-
+
exec(openCommand, (error) => {
if (error) {
console.error("Failed to open browser:", error.message);
diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index ffcb2b4..9a96f86 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useRef } from "react";
+import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { SettingsDialog } from "@/components/SettingsDialog";
@@ -9,30 +9,23 @@ import { JsonEditor } from "@/components/JsonEditor";
import { Button } from "@/components/ui/button";
import { useConfig } from "@/components/ConfigProvider";
import { api } from "@/lib/api";
-import { Settings, Languages, Save, RefreshCw, FileJson, Upload, Download } from "lucide-react";
+import { Settings, Languages, Save, RefreshCw, FileJson } from "lucide-react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
import { Toast } from "@/components/ui/toast";
import "@/styles/animations.css";
function App() {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
- const { config, setConfig, error } = useConfig();
+ const { config, error } = useConfig();
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false);
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
- const fileInputRef = useRef(null);
useEffect(() => {
const checkAuth = async () => {
@@ -160,43 +153,6 @@ function App() {
}
};
- const exportConfig = () => {
- if (!config) return;
-
- const configString = JSON.stringify(config, null, 2);
- const blob = new Blob([configString], { type: "application/json" });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = "claude-code-router-config.json";
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- };
-
- const importConfig = () => {
- fileInputRef.current?.click();
- };
-
- const handleFileChange = (e: React.ChangeEvent) => {
- const file = e.target.files?.[0];
- if (!file) return;
-
- const reader = new FileReader();
- reader.onload = (event) => {
- try {
- const configString = event.target?.result as string;
- const importedConfig = JSON.parse(configString);
- setConfig(importedConfig);
- } catch (error) {
- console.error("Failed to parse config file:", error);
- setToast({ message: t('settings.import_error'), type: 'error' });
- }
- };
- reader.readAsText(file);
- };
-
if (isCheckingAuth) {
return (
@@ -224,93 +180,51 @@ function App() {
}
return (
-
-
-
- {t('app.title')}
-
-
-
-
-
+
+
+ {t('app.title')}
+
+
setIsSettingsOpen(true)} className="transition-all-ease hover:scale-110">
+
+
+
setIsJsonEditorOpen(true)} className="transition-all-ease hover:scale-110">
+
+
+
+
+
+
+
+
+
+
+
i18n.changeLanguage('en')}
+ >
+ English
-
-
- {t('app.import_config')}
-
-
-
-
-
-
+ i18n.changeLanguage('zh')}
+ >
+ 中文
-
-
- {t('app.export_config')}
-
-
-
-
- setIsSettingsOpen(true)} className="transition-all-ease hover:scale-110">
-
-
-
-
- {t('app.settings')}
-
-
-
-
- setIsJsonEditorOpen(true)} className="transition-all-ease hover:scale-110">
-
-
-
-
- {t('json_editor.title')}
-
-
-
-
-
-
-
-
-
-
-
-
- {t('app.change_language')}
-
-
-
-
- i18n.changeLanguage('en')}
- >
- English
-
- i18n.changeLanguage('zh')}
- >
- 中文
-
-
-
-
-
-
- {t('app.save')}
-
-
-
- {t('app.save_and_restart')}
-
-
-
+
+
+
+
+
+ {t('app.save')}
+
+
+
+ {t('app.save_and_restart')}
+
+
+
@@ -324,28 +238,20 @@ function App() {
-
- setToast({ message, type })}
+
+ setToast({ message, type })}
+ />
+ {toast && (
+ setToast(null)}
/>
- {toast && (
- setToast(null)}
- />
- )}
-
-
-
+ )}
+
);
}
diff --git a/ui/src/components/ui/tooltip.tsx b/ui/src/components/ui/tooltip.tsx
deleted file mode 100644
index 61b717e..0000000
--- a/ui/src/components/ui/tooltip.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as React from "react"
-import * as TooltipPrimitive from "@radix-ui/react-tooltip"
-
-import { cn } from "@/lib/utils"
-
-const TooltipProvider = TooltipPrimitive.Provider
-
-const Tooltip = TooltipPrimitive.Root
-
-const TooltipTrigger = TooltipPrimitive.Trigger
-
-const TooltipContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, sideOffset = 4, ...props }, ref) => (
-
-))
-TooltipContent.displayName = TooltipPrimitive.Content.displayName
-
-export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
\ No newline at end of file
diff --git a/ui/src/locales/en.json b/ui/src/locales/en.json
index 6940066..9cf93fa 100644
--- a/ui/src/locales/en.json
+++ b/ui/src/locales/en.json
@@ -12,10 +12,7 @@
"config_saved_success": "Config saved successfully",
"config_saved_failed": "Failed to save config",
"config_saved_restart_success": "Config saved and service restarted successfully",
- "config_saved_restart_failed": "Failed to save config and restart service",
- "import_config": "Import Config",
- "export_config": "Export Config",
- "change_language": "Change Language"
+ "config_saved_restart_failed": "Failed to save config and restart service"
},
"login": {
"title": "Sign in to your account",
diff --git a/ui/src/locales/zh.json b/ui/src/locales/zh.json
index c0b057c..3a4828e 100644
--- a/ui/src/locales/zh.json
+++ b/ui/src/locales/zh.json
@@ -12,10 +12,7 @@
"config_saved_success": "配置保存成功",
"config_saved_failed": "配置保存失败",
"config_saved_restart_success": "配置保存并服务重启成功",
- "config_saved_restart_failed": "配置保存并服务重启失败",
- "import_config": "导入配置",
- "export_config": "导出配置",
- "change_language": "切换语言"
+ "config_saved_restart_failed": "配置保存并服务重启失败"
},
"login": {
"title": "登录到您的账户",
diff --git a/ui/tsconfig.tsbuildinfo b/ui/tsconfig.tsbuildinfo
index b42216f..62ad4f2 100644
--- a/ui/tsconfig.tsbuildinfo
+++ b/ui/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/components/ui/tooltip.tsx","./src/lib/api.ts","./src/lib/utils.ts"],"version":"5.8.3"}
\ No newline at end of file
+{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts"],"version":"5.8.3"}
\ No newline at end of file