feat ui: add tooltip

This commit is contained in:
musistudio
2025-08-04 09:48:15 +08:00
parent 5fd78a103b
commit e51d70caf2
10 changed files with 514 additions and 215 deletions

View File

@@ -441,6 +441,8 @@ If you find this project helpful, please consider sponsoring its development. Yo
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F31GN2GM) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F31GN2GM)
[Paypal](https://paypal.me/musistudio1999)
<table> <table>
<tr> <tr>
<td><img src="/blog/images/alipay.jpg" width="200" alt="Alipay" /></td> <td><img src="/blog/images/alipay.jpg" width="200" alt="Alipay" /></td>

View File

@@ -436,6 +436,8 @@ jobs:
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F31GN2GM) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F31GN2GM)
[Paypal](https://paypal.me/musistudio1999)
<table> <table>
<tr> <tr>
<td><img src="/blog/images/alipay.jpg" width="200" alt="Alipay" /></td> <td><img src="/blog/images/alipay.jpg" width="200" alt="Alipay" /></td>

View File

@@ -21,6 +21,7 @@
"dependencies": { "dependencies": {
"@fastify/static": "^8.2.0", "@fastify/static": "^8.2.0",
"@musistudio/llms": "^1.0.18", "@musistudio/llms": "^1.0.18",
"@radix-ui/react-tooltip": "^1.2.7",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"json5": "^2.2.3", "json5": "^2.2.3",
"openurl": "^1.1.1", "openurl": "^1.1.1",

395
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@musistudio/llms': '@musistudio/llms':
specifier: ^1.0.18 specifier: ^1.0.18
version: 1.0.18(ws@8.18.3)(zod@3.25.67) 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: dotenv:
specifier: ^16.4.7 specifier: ^16.4.7
version: 16.6.1 version: 16.6.1
@@ -232,6 +235,21 @@ packages:
'@fastify/static@8.2.0': '@fastify/static@8.2.0':
resolution: {integrity: sha512-PejC/DtT7p1yo3p+W7LiUtLMsV8fEvxAK15sozHy9t8kwo5r0uLYmhV/inURmGz1SkHZFz/8CNtHLPyhKcx4SQ==} 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': '@google/genai@1.8.0':
resolution: {integrity: sha512-n3KiMFesQCy2R9iSdBIuJ0JWYQ1HZBJJkmt4PPZMGZKvlgHhBAGw1kUMyX+vsAIzprN3lK45DI755lm70wPOOg==} resolution: {integrity: sha512-n3KiMFesQCy2R9iSdBIuJ0JWYQ1HZBJJkmt4PPZMGZKvlgHhBAGw1kUMyX+vsAIzprN3lK45DI755lm70wPOOg==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
@@ -272,6 +290,215 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'} 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': '@types/node@24.0.15':
resolution: {integrity: sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==} resolution: {integrity: sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==}
@@ -729,6 +956,15 @@ packages:
quick-format-unescaped@4.0.4: quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} 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: real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'} engines: {node: '>= 12.13.0'}
@@ -770,6 +1006,9 @@ packages:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'} engines: {node: '>=10'}
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
secure-json-parse@4.0.0: secure-json-parse@4.0.0:
resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==} resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==}
@@ -1073,6 +1312,23 @@ snapshots:
fastq: 1.19.1 fastq: 1.19.1
glob: 11.0.3 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': '@google/genai@1.8.0':
dependencies: dependencies:
google-auth-library: 9.15.1 google-auth-library: 9.15.1
@@ -1136,6 +1392,136 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5 '@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1 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': '@types/node@24.0.15':
dependencies: dependencies:
undici-types: 7.8.0 undici-types: 7.8.0
@@ -1633,6 +2019,13 @@ snapshots:
quick-format-unescaped@4.0.4: {} 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: {} real-require@0.2.0: {}
rechoir@0.6.2: rechoir@0.6.2:
@@ -1665,6 +2058,8 @@ snapshots:
safe-stable-stringify@2.5.0: {} safe-stable-stringify@2.5.0: {}
scheduler@0.26.0: {}
secure-json-parse@4.0.0: {} secure-json-parse@4.0.0: {}
semver@5.7.2: {} semver@5.7.2: {}

View File

@@ -2,12 +2,16 @@
import { run } from "./index"; import { run } from "./index";
import { showStatus } from "./utils/status"; import { showStatus } from "./utils/status";
import { executeCodeCommand } from "./utils/codeCommand"; 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 { version } from "../package.json";
import { spawn, exec } from "child_process"; import { spawn, exec } from "child_process";
import { PID_FILE, REFERENCE_COUNT_FILE } from "./constants"; import { PID_FILE, REFERENCE_COUNT_FILE } from "./constants";
import fs, { existsSync, readFileSync } from "fs"; import fs, { existsSync, readFileSync } from "fs";
import {join} from "path"; import { join } from "path";
const command = process.argv[2]; const command = process.argv[2];
@@ -108,7 +112,9 @@ async function main() {
startProcess.unref(); startProcess.unref();
if (await waitForService()) { 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 { } else {
console.error( console.error(
"Service startup timeout, please manually run `ccr start` to start the service" "Service startup timeout, please manually run `ccr start` to start the service"
@@ -116,7 +122,9 @@ async function main() {
process.exit(1); process.exit(1);
} }
} else { } 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; break;
case "ui": case "ui":
@@ -138,49 +146,68 @@ async function main() {
if (!(await waitForService())) { if (!(await waitForService())) {
// If service startup fails, try to start with default config // If service startup fails, try to start with default config
console.log("Service startup timeout, trying to start with default configuration..."); console.log(
const { initDir, writeConfigFile, backupConfigFile } = require("./utils"); "Service startup timeout, trying to start with default configuration..."
);
const {
initDir,
writeConfigFile,
backupConfigFile,
} = require("./utils");
try { try {
// Initialize directories // Initialize directories
await initDir(); await initDir();
// Backup existing config file if it exists // Backup existing config file if it exists
const backupPath = await backupConfigFile(); const backupPath = await backupConfigFile();
if (backupPath) { 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 // Create a minimal default config file
await writeConfigFile({ await writeConfigFile({
"PORT": 3456, PORT: 3456,
"Providers": [], Providers: [],
"Router": {} Router: {},
}); });
console.log("Created minimal default configuration file at ~/.claude-code-router/config.json"); console.log(
console.log("Please edit this file with your actual configuration."); "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 // Try starting the service again
const restartProcess = spawn("node", [cliPath, "start"], { const restartProcess = spawn("node", [cliPath, "start"], {
detached: true, detached: true,
stdio: "ignore", stdio: "ignore",
}); });
restartProcess.on("error", (error) => { 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); process.exit(1);
}); });
restartProcess.unref(); 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( console.error(
"Service startup still failing. Please manually run `ccr start` to start the service and check the logs." "Service startup still failing. Please manually run `ccr start` to start the service and check the logs."
); );
process.exit(1); process.exit(1);
} }
} catch (error: any) { } catch (error: any) {
console.error("Failed to create default configuration:", error.message); console.error(
"Failed to create default configuration:",
error.message
);
process.exit(1); process.exit(1);
} }
} }
@@ -190,11 +217,11 @@ async function main() {
const serviceInfo = await getServiceInfo(); const serviceInfo = await getServiceInfo();
const uiUrl = `${serviceInfo.endpoint}/ui/`; const uiUrl = `${serviceInfo.endpoint}/ui/`;
console.log(`Opening UI at ${uiUrl}`); console.log(`Opening UI at ${uiUrl}`);
// Open URL in browser based on platform // Open URL in browser based on platform
const platform = process.platform; const platform = process.platform;
let openCommand = ""; let openCommand = "";
if (platform === "win32") { if (platform === "win32") {
// Windows // Windows
openCommand = `start ${uiUrl}`; openCommand = `start ${uiUrl}`;
@@ -208,7 +235,7 @@ async function main() {
console.error("Unsupported platform for opening browser"); console.error("Unsupported platform for opening browser");
process.exit(1); process.exit(1);
} }
exec(openCommand, (error) => { exec(openCommand, (error) => {
if (error) { if (error) {
console.error("Failed to open browser:", error.message); console.error("Failed to open browser:", error.message);

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { SettingsDialog } from "@/components/SettingsDialog"; import { SettingsDialog } from "@/components/SettingsDialog";
@@ -9,30 +9,23 @@ import { JsonEditor } from "@/components/JsonEditor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useConfig } from "@/components/ConfigProvider"; import { useConfig } from "@/components/ConfigProvider";
import { api } from "@/lib/api"; 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 { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Toast } from "@/components/ui/toast"; import { Toast } from "@/components/ui/toast";
import "@/styles/animations.css"; import "@/styles/animations.css";
function App() { function App() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { config, setConfig, error } = useConfig(); const { config, error } = useConfig();
const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false); const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false);
const [isCheckingAuth, setIsCheckingAuth] = useState(true); const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
const checkAuth = async () => { 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<HTMLInputElement>) => {
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) { if (isCheckingAuth) {
return ( return (
@@ -224,93 +180,51 @@ function App() {
} }
return ( return (
<TooltipProvider> <div className="h-screen bg-gray-50 font-sans">
<div className="h-screen bg-gray-50 font-sans"> <header className="flex h-16 items-center justify-between border-b bg-white px-6">
<header className="flex h-16 items-center justify-between border-b bg-white px-6"> <h1 className="text-xl font-semibold text-gray-800">{t('app.title')}</h1>
<h1 className="text-xl font-semibold text-gray-800">{t('app.title')}</h1> <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <Button variant="ghost" size="icon" onClick={() => setIsSettingsOpen(true)} className="transition-all-ease hover:scale-110">
<Tooltip> <Settings className="h-5 w-5" />
<TooltipTrigger asChild> </Button>
<Button variant="ghost" size="icon" onClick={importConfig} className="transition-all-ease hover:scale-110"> <Button variant="ghost" size="icon" onClick={() => setIsJsonEditorOpen(true)} className="transition-all-ease hover:scale-110">
<Download className="h-5 w-5" /> <FileJson className="h-5 w-5" />
</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="transition-all-ease hover:scale-110">
<Languages className="h-5 w-5" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-32 p-2">
<div className="space-y-1">
<Button
variant={i18n.language.startsWith('en') ? 'default' : 'ghost'}
className="w-full justify-start transition-all-ease hover:scale-[1.02]"
onClick={() => i18n.changeLanguage('en')}
>
English
</Button> </Button>
</TooltipTrigger> <Button
<TooltipContent> variant={i18n.language.startsWith('zh') ? 'default' : 'ghost'}
<p>{t('app.import_config')}</p> className="w-full justify-start transition-all-ease hover:scale-[1.02]"
</TooltipContent> onClick={() => i18n.changeLanguage('zh')}
</Tooltip> >
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={exportConfig} className="transition-all-ease hover:scale-110">
<Upload className="h-5 w-5" />
</Button> </Button>
</TooltipTrigger> </div>
<TooltipContent> </PopoverContent>
<p>{t('app.export_config')}</p> </Popover>
</TooltipContent> <Button onClick={saveConfig} variant="outline" className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
</Tooltip> <Save className="mr-2 h-4 w-4" />
<Tooltip> {t('app.save')}
<TooltipTrigger asChild> </Button>
<Button variant="ghost" size="icon" onClick={() => setIsSettingsOpen(true)} className="transition-all-ease hover:scale-110"> <Button onClick={saveConfigAndRestart} className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
<Settings className="h-5 w-5" /> <RefreshCw className="mr-2 h-4 w-4" />
</Button> {t('app.save_and_restart')}
</TooltipTrigger> </Button>
<TooltipContent> </div>
<p>{t('app.settings')}</p> </header>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={() => setIsJsonEditorOpen(true)} className="transition-all-ease hover:scale-110">
<FileJson className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t('json_editor.title')}</p>
</TooltipContent>
</Tooltip>
<Popover>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="transition-all-ease hover:scale-110">
<Languages className="h-5 w-5" />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>
<p>{t('app.change_language')}</p>
</TooltipContent>
</Tooltip>
<PopoverContent className="w-32 p-2">
<div className="space-y-1">
<Button
variant={i18n.language.startsWith('en') ? 'default' : 'ghost'}
className="w-full justify-start transition-all-ease hover:scale-[1.02]"
onClick={() => i18n.changeLanguage('en')}
>
English
</Button>
<Button
variant={i18n.language.startsWith('zh') ? 'default' : 'ghost'}
className="w-full justify-start transition-all-ease hover:scale-[1.02]"
onClick={() => i18n.changeLanguage('zh')}
>
</Button>
</div>
</PopoverContent>
</Popover>
<Button onClick={saveConfig} variant="outline" className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
<Save className="mr-2 h-4 w-4" />
{t('app.save')}
</Button>
<Button onClick={saveConfigAndRestart} className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
<RefreshCw className="mr-2 h-4 w-4" />
{t('app.save_and_restart')}
</Button>
</div>
</header>
<main className="flex h-[calc(100vh-4rem)] gap-4 p-4"> <main className="flex h-[calc(100vh-4rem)] gap-4 p-4">
<div className="w-3/5"> <div className="w-3/5">
<Providers /> <Providers />
@@ -324,28 +238,20 @@ function App() {
</div> </div>
</div> </div>
</main> </main>
<SettingsDialog isOpen={isSettingsOpen} onOpenChange={setIsSettingsOpen} /> <SettingsDialog isOpen={isSettingsOpen} onOpenChange={setIsSettingsOpen} />
<JsonEditor <JsonEditor
open={isJsonEditorOpen} open={isJsonEditorOpen}
onOpenChange={setIsJsonEditorOpen} onOpenChange={setIsJsonEditorOpen}
showToast={(message, type) => setToast({ message, type })} showToast={(message, type) => setToast({ message, type })}
/>
{toast && (
<Toast
message={toast.message}
type={toast.type}
onClose={() => setToast(null)}
/> />
{toast && ( )}
<Toast </div>
message={toast.message}
type={toast.type}
onClose={() => setToast(null)}
/>
)}
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept=".json"
className="hidden"
/>
</div>
</TooltipProvider>
); );
} }

View File

@@ -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<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md 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",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@@ -12,10 +12,7 @@
"config_saved_success": "Config saved successfully", "config_saved_success": "Config saved successfully",
"config_saved_failed": "Failed to save config", "config_saved_failed": "Failed to save config",
"config_saved_restart_success": "Config saved and service restarted successfully", "config_saved_restart_success": "Config saved and service restarted successfully",
"config_saved_restart_failed": "Failed to save config and restart service", "config_saved_restart_failed": "Failed to save config and restart service"
"import_config": "Import Config",
"export_config": "Export Config",
"change_language": "Change Language"
}, },
"login": { "login": {
"title": "Sign in to your account", "title": "Sign in to your account",

View File

@@ -12,10 +12,7 @@
"config_saved_success": "配置保存成功", "config_saved_success": "配置保存成功",
"config_saved_failed": "配置保存失败", "config_saved_failed": "配置保存失败",
"config_saved_restart_success": "配置保存并服务重启成功", "config_saved_restart_success": "配置保存并服务重启成功",
"config_saved_restart_failed": "配置保存并服务重启失败", "config_saved_restart_failed": "配置保存并服务重启失败"
"import_config": "导入配置",
"export_config": "导出配置",
"change_language": "切换语言"
}, },
"login": { "login": {
"title": "登录到您的账户", "title": "登录到您的账户",

View File

@@ -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"} {"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"}