update 优化录音功能模块的代码和火山模块

This commit is contained in:
2025-10-22 03:24:08 +08:00
parent c1072c3896
commit edace856ab
26 changed files with 1634 additions and 101 deletions

552
pnpm-lock.yaml generated
View File

@@ -75,6 +75,15 @@ importers:
'@kevisual/registry':
specifier: ^0.0.1
version: 0.0.1(typescript@5.9.3)
'@radix-ui/react-alert-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-slot':
specifier: ^1.2.3
version: 1.2.3(@types/react@19.2.2)(react@19.2.0)
'@ricky0123/vad-web':
specifier: ^0.0.28
version: 0.0.28
@@ -84,6 +93,12 @@ importers:
'@tailwindcss/vite':
specifier: ^4.1.14
version: 4.1.14(vite@6.3.7(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1))
'@tanstack/react-form':
specifier: ^1.23.7
version: 1.23.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@tanstack/react-query':
specifier: ^5.90.5
version: 5.90.5(react@19.2.0)
astro:
specifier: ^5.14.4
version: 5.14.5(@types/node@24.7.2)(idb-keyval@6.2.2)(jiti@2.6.1)(lightningcss@1.30.1)(rollup@4.52.4)(typescript@5.9.3)
@@ -161,7 +176,7 @@ importers:
version: 7.11.0
zustand:
specifier: ^5.0.8
version: 5.0.8(@types/react@19.2.2)(react@19.2.0)
version: 5.0.8(@types/react@19.2.2)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0))
devDependencies:
'@kevisual/types':
specifier: ^0.0.10
@@ -760,6 +775,190 @@ packages:
'@protobufjs/utf8@1.1.0':
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
'@radix-ui/react-alert-dialog@1.1.15':
resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==}
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-dialog@1.1.15':
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
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-dismissable-layer@1.1.11':
resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
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-focus-guards@1.1.3':
resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-focus-scope@1.1.7':
resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
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-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.5':
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
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-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
'@ricky0123/vad-web@0.0.28':
resolution: {integrity: sha512-Hvw8jN3r1SBxmjJa89HITxRcwlT6dc7CQPVtVQLrqfY8EeQcx41QeqKUol4lw8ZCeAIHKwYndHnB1K/4SAQJgQ==}
@@ -1051,6 +1250,39 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7
'@tanstack/devtools-event-client@0.3.3':
resolution: {integrity: sha512-RfV+OPV/M3CGryYqTue684u10jUt55PEqeBOnOtCe6tAmHI9Iqyc8nHeDhWPEV9715gShuauFVaMc9RiUVNdwg==}
engines: {node: '>=18'}
'@tanstack/form-core@1.24.3':
resolution: {integrity: sha512-e+HzSD49NWr4aIqJWtPPzmi+/phBJAP3nSPN8dvxwmJWqAxuB/cH138EcmCFf3+oA7j3BXvwvTY0I+8UweGPjQ==}
'@tanstack/query-core@5.90.5':
resolution: {integrity: sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==}
'@tanstack/react-form@1.23.7':
resolution: {integrity: sha512-p/j9Gi2+s135sOjj48RjM+6xZQr1FVpliQlETLYBEGmmmxWHgYYs2b62mTDSnuv7AqtuZhpQ+t0CRFVfbQLsFA==}
peerDependencies:
'@tanstack/react-start': ^1.130.10
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@tanstack/react-start':
optional: true
'@tanstack/react-query@5.90.5':
resolution: {integrity: sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==}
peerDependencies:
react: ^18 || ^19
'@tanstack/react-store@0.7.7':
resolution: {integrity: sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/store@0.7.7':
resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==}
'@tweenjs/tween.js@23.1.3':
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
@@ -1263,6 +1495,10 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
aria-hidden@1.2.6:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'}
aria-query@5.3.2:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
@@ -1439,6 +1675,9 @@ packages:
supports-color:
optional: true
decode-formdata@0.9.0:
resolution: {integrity: sha512-q5uwOjR3Um5YD+ZWPOF/1sGHVW9A5rCrRwITQChRXlmPkxDFBqCm4jNTIVdGHNH9OnR+V9MoZVgRhsFb+ARbUw==}
decode-named-character-reference@1.2.0:
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
@@ -1469,6 +1708,9 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
deterministic-object-hash@2.0.2:
resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==}
engines: {node: '>=18'}
@@ -1677,6 +1919,10 @@ packages:
resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
engines: {node: '>=18'}
get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
@@ -2555,12 +2801,42 @@ packages:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
react-remove-scroll-bar@2.3.8:
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
react-remove-scroll@2.7.1:
resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==}
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
react-resizable-panels@3.0.6:
resolution: {integrity: sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==}
peerDependencies:
react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-style-singleton@2.2.3:
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
react-toastify@11.0.5:
resolution: {integrity: sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==}
peerDependencies:
@@ -3058,6 +3334,31 @@ packages:
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
use-callback-ref@1.3.3:
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
use-sidecar@1.1.3:
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
use-sync-external-store@1.6.0:
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -3799,6 +4100,163 @@ snapshots:
'@protobufjs/utf8@1.1.0': {}
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.2)(react@19.2.0)':
dependencies:
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-context@1.1.2(@types/react@19.2.2)(react@19.2.0)':
dependencies:
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0)
aria-hidden: 1.2.6
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.2)(react@19.2.0)':
dependencies:
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-id@1.1.1(@types/react@19.2.2)(react@19.2.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-slot@1.2.3(@types/react@19.2.2)(react@19.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.2)(react@19.2.0)':
dependencies:
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.2)(react@19.2.0)':
dependencies:
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.2)(react@19.2.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.2)(react@19.2.0)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.2)(react@19.2.0)':
dependencies:
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.2
'@ricky0123/vad-web@0.0.28':
dependencies:
onnxruntime-web: 1.23.0
@@ -4024,6 +4482,39 @@ snapshots:
tailwindcss: 4.1.14
vite: 6.3.7(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1)
'@tanstack/devtools-event-client@0.3.3': {}
'@tanstack/form-core@1.24.3':
dependencies:
'@tanstack/devtools-event-client': 0.3.3
'@tanstack/store': 0.7.7
'@tanstack/query-core@5.90.5': {}
'@tanstack/react-form@1.23.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@tanstack/form-core': 1.24.3
'@tanstack/react-store': 0.7.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
decode-formdata: 0.9.0
devalue: 5.3.2
react: 19.2.0
transitivePeerDependencies:
- react-dom
'@tanstack/react-query@5.90.5(react@19.2.0)':
dependencies:
'@tanstack/query-core': 5.90.5
react: 19.2.0
'@tanstack/react-store@0.7.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@tanstack/store': 0.7.7
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
use-sync-external-store: 1.6.0(react@19.2.0)
'@tanstack/store@0.7.7': {}
'@tweenjs/tween.js@23.1.3': {}
'@types/babel__core@7.20.5':
@@ -4301,6 +4792,10 @@ snapshots:
argparse@2.0.1: {}
aria-hidden@1.2.6:
dependencies:
tslib: 2.8.1
aria-query@5.3.2: {}
array-iterate@2.0.1: {}
@@ -4539,6 +5034,8 @@ snapshots:
dependencies:
ms: 2.1.3
decode-formdata@0.9.0: {}
decode-named-character-reference@1.2.0:
dependencies:
character-entities: 2.0.2
@@ -4560,6 +5057,8 @@ snapshots:
detect-libc@2.1.2: {}
detect-node-es@1.1.0: {}
deterministic-object-hash@2.0.2:
dependencies:
base-64: 1.0.0
@@ -4782,6 +5281,8 @@ snapshots:
get-east-asian-width@1.4.0: {}
get-nonce@1.0.1: {}
github-slugger@2.0.0: {}
glob-parent@5.1.2:
@@ -6002,11 +6503,38 @@ snapshots:
react-refresh@0.17.0: {}
react-remove-scroll-bar@2.3.8(@types/react@19.2.2)(react@19.2.0):
dependencies:
react: 19.2.0
react-style-singleton: 2.2.3(@types/react@19.2.2)(react@19.2.0)
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.2
react-remove-scroll@2.7.1(@types/react@19.2.2)(react@19.2.0):
dependencies:
react: 19.2.0
react-remove-scroll-bar: 2.3.8(@types/react@19.2.2)(react@19.2.0)
react-style-singleton: 2.2.3(@types/react@19.2.2)(react@19.2.0)
tslib: 2.8.1
use-callback-ref: 1.3.3(@types/react@19.2.2)(react@19.2.0)
use-sidecar: 1.1.3(@types/react@19.2.2)(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.2
react-resizable-panels@3.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
react-style-singleton@2.2.3(@types/react@19.2.2)(react@19.2.0):
dependencies:
get-nonce: 1.0.1
react: 19.2.0
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.2
react-toastify@11.0.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
clsx: 2.1.1
@@ -6597,6 +7125,25 @@ snapshots:
querystringify: 2.2.0
requires-port: 1.0.0
use-callback-ref@1.3.3(@types/react@19.2.2)(react@19.2.0):
dependencies:
react: 19.2.0
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.2
use-sidecar@1.1.3(@types/react@19.2.2)(react@19.2.0):
dependencies:
detect-node-es: 1.1.0
react: 19.2.0
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.2
use-sync-external-store@1.6.0(react@19.2.0):
dependencies:
react: 19.2.0
util-deprecate@1.0.2: {}
uuid@8.3.2: {}
@@ -6698,9 +7245,10 @@ snapshots:
zod@3.25.76: {}
zustand@5.0.8(@types/react@19.2.2)(react@19.2.0):
zustand@5.0.8(@types/react@19.2.2)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)):
optionalDependencies:
'@types/react': 19.2.2
react: 19.2.0
use-sync-external-store: 1.6.0(react@19.2.0)
zwitch@2.0.4: {}

View File

@@ -9,6 +9,7 @@
"build": "astro build",
"preview": "astro preview",
"pub": "envision deploy ./dist -k light-code-center -v 0.0.1 -u",
"ui": "pnpm dlx shadcn@latest add ",
"sn": "pnpm dlx shadcn@latest add "
},
"keywords": [],
@@ -25,9 +26,14 @@
"@kevisual/query": "^0.0.29",
"@kevisual/query-login": "^0.0.6",
"@kevisual/registry": "^0.0.1",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-slot": "^1.2.3",
"@ricky0123/vad-web": "^0.0.28",
"@szhsin/react-menu": "^4.5.0",
"@tailwindcss/vite": "^4.1.14",
"@tanstack/react-form": "^1.23.7",
"@tanstack/react-query": "^5.90.5",
"astro": "^5.14.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",

View File

@@ -333,8 +333,8 @@ export const Table: React.FC<TableProps> = ({
// Ctrl/Cmd + A 全选
if ((event.ctrlKey || event.metaKey) && event.key === 'a') {
event.preventDefault();
handleSelectAll(true);
// event.preventDefault();
// handleSelectAll(true);
return;
}

View File

@@ -0,0 +1,46 @@
import React, { useState } from 'react';
import { Eye, EyeOff } from 'lucide-react';
interface PasswordInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}
export const PasswordInput: React.FC<PasswordInputProps> = ({
value,
onChange,
placeholder,
className = '',
}) => {
const [showPassword, setShowPassword] = useState(false);
const togglePasswordVisibility = () => {
setShowPassword(!showPassword);
};
return (
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className={`pr-10 ${className}`}
/>
<button
type="button"
onClick={togglePasswordVisibility}
className="absolute right-3 top-1/2 transform -translate-y-1/2 p-1 hover:bg-gray-100 rounded transition-colors"
title={showPassword ? '隐藏密码' : '显示密码'}
>
{showPassword ? (
<EyeOff className="w-4 h-4 text-gray-500" />
) : (
<Eye className="w-4 h-4 text-gray-500" />
)}
</button>
</div>
);
};

View File

@@ -3,7 +3,7 @@ import { AuthProvider } from '../login/AuthProvider';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { useState, useRef } from 'react';
import { App as Voice } from './videos/index.tsx';
import { App as Voice } from './voice/index.tsx';
import { ChatInterface } from './prompts/index.tsx';
import { BaseApp } from './base/index.tsx';
import { exampleUsage, markService } from './modules/mark-service.ts';
@@ -201,7 +201,7 @@ export const App: React.FC = () => {
<MuseApp />
<ToastContainer
position="top-right"
autoClose={5000}
autoClose={1000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick

View File

@@ -1,5 +0,0 @@
@import 'tailwindcss';
.low-energy-spin {
animation: 2.5s linear 0s infinite normal forwards running spin;
}

View File

@@ -0,0 +1,148 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "../../../../components/ui/dialog";
import { useSettingStore } from '../store/settingStore';
import { PasswordInput } from '../../components/PasswordInput';
import { X, RotateCcw } from 'lucide-react';
export const SettingModal: React.FC = () => {
const {
isOpen,
autoRecognize,
listen,
volcengineAucAppId,
volcengineAucToken,
closeModal,
setAutoRecognize,
setListen,
setVolcengineAucAppId,
setVolcengineAucToken,
resetToDefault,
} = useSettingStore();
const handleClose = () => {
closeModal();
};
const handleReset = () => {
if (window.confirm('确定要重置所有设置为默认值吗?')) {
resetToDefault();
}
};
return (
<Dialog open={isOpen} onOpenChange={handleClose} >
<DialogContent className="max-w-md max-h-[80vh] overflow-y-auto" showCloseButton={false}>
<DialogHeader>
<div className="flex items-center justify-between">
<DialogTitle></DialogTitle>
<div className="flex items-center space-x-2">
<button
onClick={handleClose}
className="p-1 hover:bg-gray-100 rounded transition-colors cursor-pointer"
>
<X className="w-4 h-4 text-gray-500" />
</button>
</div>
</div>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* 语音识别设置 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className="text-sm text-gray-700"></label>
<div className="relative">
<input
type="checkbox"
checked={autoRecognize}
onChange={(e) => setAutoRecognize(e.target.checked)}
className="sr-only"
/>
<div
onClick={() => setAutoRecognize(!autoRecognize)}
className={`w-11 h-6 rounded-full cursor-pointer transition-colors ${autoRecognize ? 'bg-blue-600' : 'bg-gray-200'
}`}
>
<div
className={`w-5 h-5 bg-white rounded-full shadow-md transform transition-transform ${autoRecognize ? 'translate-x-5' : 'translate-x-0.5'
} mt-0.5`}
/>
</div>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-sm text-gray-700"></label>
<div className="relative">
<input
type="checkbox"
checked={listen}
onChange={(e) => setListen(e.target.checked)}
className="sr-only"
/>
<div
onClick={() => setListen(!listen)}
className={`w-11 h-6 rounded-full cursor-pointer transition-colors ${listen ? 'bg-blue-600' : 'bg-gray-200'
}`}
>
<div
className={`w-5 h-5 bg-white rounded-full shadow-md transform transition-transform ${listen ? 'translate-x-5' : 'translate-x-0.5'
} mt-0.5`}
/>
</div>
</div>
</div>
</div>
{/* 火山引擎配置 */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-gray-900"></h3>
<div className="space-y-3">
<div>
<label className="block text-sm text-gray-700 mb-1">App ID</label>
<input
type="text"
value={volcengineAucAppId}
onChange={(e) => setVolcengineAucAppId(e.target.value)}
placeholder="请输入火山引擎 App ID"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Token</label>
<PasswordInput
value={volcengineAucToken}
onChange={setVolcengineAucToken}
placeholder="请输入火山引擎 Token"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
</div>
</div>
<div className="flex justify-end space-x-3 pt-4 border-t">
<button
onClick={handleClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
>
</button>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -2,13 +2,26 @@ import { MicVAD, utils } from "@ricky0123/vad-web"
import clsx from "clsx";
import { useState, useEffect, useRef } from "react";
import './style.css'
import { MoreHorizontal, Play, Pause } from "lucide-react";
import { MoreHorizontal, Play, Pause, Settings, FileAudio, StopCircle, Loader } from "lucide-react";
import { Menu, MenuItem, MenuButton, } from '@szhsin/react-menu';
import '@szhsin/react-menu/dist/index.css';
import { toast } from 'react-toastify';
import { Speak } from "./speak-db/speak";
import { useVoiceStore } from "../store/voiceStore";
import { useSettingStore } from "../store/settingStore";
import { SettingModal } from "./SettingModal";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "../../../../components/ui/alert-dialog";
type VadVoiceProps = {
data: Speak;
@@ -136,19 +149,51 @@ const VoicePlayer = ({ data }: VadVoiceProps) => {
<span></span>
</div>
</MenuItem>
<MenuItem onClick={handleDelete}>
<div className="flex items-center space-x-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1-1H8a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
<span></span>
</div>
</MenuItem>
{data.text && data.text.trim() ? (
<AlertDialog>
<AlertDialogTrigger asChild>
<MenuItem>
<div className="flex items-center space-x-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1-1H8a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
<span></span>
</div>
</MenuItem>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"{data.text}"
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={handleDelete}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
) : (
<MenuItem onClick={handleDelete}>
<div className="flex items-center space-x-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1-1H8a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
<span></span>
</div>
</MenuItem>
)}
</Menu>
{/* 播放/暂停按钮 */}
{!isPlaying ? (
@@ -187,11 +232,92 @@ export const ShowVoicePlayer = ({ data }: { data: Speak[] }) => {
<VoicePlayer data={item} />
</div>
<div className="flex-1 min-w-0">
<div className="text-xs text-gray-400 truncate">
{new Date(item.timestamp).toLocaleTimeString()}
</div>
<div className="text-xs text-gray-300">
#{item.no}
<div className="flex items-center justify-between space-x-2">
<div>
<div className="text-xs text-gray-400 truncate">
{new Date(item.timestamp).toLocaleTimeString()}
</div>
<div className="text-xs text-gray-300">
#{item.no}
</div>
</div>
<div className="flex items-center space-x-1">
{item.text && item.text.trim() ? (
<AlertDialog>
<AlertDialogTrigger asChild>
<button
onClick={(e) => e.stopPropagation()}
className="w-5 h-5 hover:bg-red-100 rounded flex items-center justify-center text-red-500 transition-colors cursor-pointer"
title="删除"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1-1H8a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"{item.text}"
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.stopPropagation();
const { deleteVoice } = useVoiceStore.getState();
deleteVoice(item.id)
.then(() => {
console.log('语音记录删除成功');
toast.success('删除成功', { autoClose: 200 });
})
.catch((error) => {
console.error('删除语音记录失败:', error);
toast.error('删除失败: ' + (error instanceof Error ? error.message : '未知错误'));
});
}}
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
) : (
<button
onClick={(e) => {
e.stopPropagation();
const { deleteVoice } = useVoiceStore.getState();
deleteVoice(item.id)
.then(() => {
console.log('语音记录删除成功');
toast.success('删除成功', { autoClose: 200 });
})
.catch((error) => {
console.error('删除语音记录失败:', error);
toast.error('删除失败: ' + (error instanceof Error ? error.message : '未知错误'));
});
}}
className="w-5 h-5 hover:bg-red-100 rounded flex items-center justify-center text-red-500 transition-colors cursor-pointer"
title="删除"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1-1H8a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
)}
</div>
</div>
{item.text && (
<div
@@ -230,7 +356,15 @@ export const VadVoice = () => {
setError: setStoreError
} = useVoiceStore();
const [listen, setListen] = useState<boolean>(false);
// 使用设置 store
const {
openModal: openSettingModal,
listen,
setListen,
autoRecognize,
setAutoRecognize
} = useSettingStore();
const [vadStatus, setVadStatus] = useState<'idle' | 'initializing' | 'ready' | 'error'>('idle');
const [realListen, setRealListen] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string>('');
@@ -238,7 +372,8 @@ export const VadVoice = () => {
const ref = useRef<MicVAD | null>(null);
const initializingRef = useRef<boolean>(false);
async function initializeVAD() {
async function initializeVAD(ls: boolean = true) {
if (!ls) { return }
if (ref.current || initializingRef.current) return;
initializingRef.current = true;
@@ -247,7 +382,6 @@ export const VadVoice = () => {
try {
console.log('Starting VAD initialization...');
// 添加延迟确保资源加载完成
await new Promise((resolve) => setTimeout(resolve, 500));
@@ -310,7 +444,7 @@ export const VadVoice = () => {
const handleUserInteraction = async () => {
if (!userInteracted) {
setUserInteracted(true);
await initializeVAD();
// await initializeVAD();
}
};
@@ -318,7 +452,6 @@ export const VadVoice = () => {
useEffect(() => {
initializeStore();
}, [initializeStore]);
useEffect(() => {
// 页面加载时不自动初始化,等待用户交互
const handleFirstClick = () => {
@@ -329,16 +462,23 @@ export const VadVoice = () => {
};
document.addEventListener('click', handleFirstClick);
// handleUserInteraction()
return () => {
document.removeEventListener('click', handleFirstClick);
// 清理 VAD 资源
if (ref.current) {
ref.current.destroy();
ref.current = null;
}
};
}, [])
useEffect(() => {
if (!userInteracted) {
return
}
console.log('VadVoice listen changed:', listen, userInteracted);
initializeVAD(listen);
return () => {
ref.current?.destroy?.();
ref.current = null;
setVadStatus('idle');
}
}, [listen, userInteracted]);
const close = () => {
if (ref.current) {
ref.current.destroy();
@@ -369,20 +509,26 @@ export const VadVoice = () => {
return <div className="h-full flex flex-col">
{/* Audio Recordings List */}
<div className="flex-1 overflow-y-auto px-2 py-3 min-h-0 max-h-200">
{!userInteracted && vadStatus === 'idle' ? (
<div className="text-center text-gray-400 text-sm py-8">
{
voiceList.length === 0 ? (
<div className="text-center text-gray-400 text-sm py-8">
<div className="mb-2">🎤</div>
<div>No recordings yet</div>
<div className="text-xs mt-1">Start talking to record</div>
</div>
) : (
<ShowVoicePlayer data={voiceList} />
)
}
{!userInteracted && vadStatus === 'idle' && listen && (
<div className="text-center text-gray-400 text-sm py-8" onClick={() => {
setUserInteracted(true);
initializeVAD(listen)
}}>
<div className="mb-2">🎤</div>
<div>Click anywhere to initialize microphone</div>
<div className="text-xs mt-1">Browser requires user interaction for microphone access</div>
</div>
) : voiceList.length === 0 ? (
<div className="text-center text-gray-400 text-sm py-8">
<div className="mb-2">🎤</div>
<div>No recordings yet</div>
<div className="text-xs mt-1">Start talking to record</div>
</div>
) : (
<ShowVoicePlayer data={voiceList} />
)}
</div>
@@ -401,7 +547,7 @@ export const VadVoice = () => {
<div className="absolute -top-1 -right-1 w-3 h-3 bg-blue-500 rounded-full animate-pulse"></div>
)}
{realListen && (
{listen && realListen && (
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
)}
</div>
@@ -433,17 +579,41 @@ export const VadVoice = () => {
onClick={handleStartStop}
disabled={vadStatus === 'initializing'}
className={clsx(
"px-3 py-1.5 text-xs font-medium rounded-md transition-colors",
"w-8 h-8 text-xs font-medium rounded-full flex items-center justify-center transition-colors cursor-pointer",
vadStatus === 'initializing' && "opacity-50 cursor-not-allowed",
listen
? "bg-red-100 text-red-700 hover:bg-red-200"
: "bg-green-100 text-green-700 hover:bg-green-200"
)}
>
{vadStatus === 'initializing' ? 'Initializing...' : (listen ? 'Stop' : 'Start')}
{vadStatus === 'initializing' ? <Loader className="w-4 h-4 inline-block animate-spin" /> : (listen ? <StopCircle className="w-4 h-4 inline-block" /> :
<Play className="w-4 h-4 inline-block" />
)}
</button>
<button onClick={() => {
const newStatus = !autoRecognize;
setAutoRecognize(newStatus);
}}
className={clsx(
"w-8 h-8 hover:bg-gray-200 rounded-full flex items-center justify-center text-gray-700 transition-colors cursor-pointer",
{ "bg-blue-200": autoRecognize }
)}
title={autoRecognize ? '自动转文字中' : '转文字禁用中'}>
<FileAudio className="w-4 h-4" />
</button>
<button
onClick={openSettingModal}
className="w-8 h-8 hover:bg-gray-200 rounded-full flex items-center justify-center text-gray-700 transition-colors cursor-pointer"
title="设置"
>
<Settings className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* 设置弹窗 */}
<SettingModal />
</div >
}

View File

@@ -10,7 +10,9 @@ export const getConfig = () => {
};
return {
// 火山引擎APPID
VOLCENGINE_AUC_APPID: getFromLocalStorage('VOLCENGINE_AUC_APPID', ''),
// 火山引擎Access Token
VOLCENGINE_AUC_TOKEN: getFromLocalStorage('VOLCENGINE_AUC_TOKEN', ''),
};
};

View File

@@ -0,0 +1,58 @@
@import 'tailwindcss';
.low-energy-spin {
animation: 2.5s linear 0s infinite normal forwards running spin;
}
/* 自定义滑块样式 */
.slider {
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
}
.slider::-webkit-slider-track {
background: #e5e7eb;
height: 8px;
border-radius: 4px;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
background: #3b82f6;
height: 20px;
width: 20px;
border-radius: 50%;
cursor: pointer;
margin-top: -6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: background-color 0.2s;
}
.slider::-webkit-slider-thumb:hover {
background: #2563eb;
}
.slider::-moz-range-track {
background: #e5e7eb;
height: 8px;
border-radius: 4px;
border: none;
}
.slider::-moz-range-thumb {
background: #3b82f6;
height: 20px;
width: 20px;
border-radius: 50%;
cursor: pointer;
border: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: background-color 0.2s;
}
.slider::-moz-range-thumb:hover {
background: #2563eb;
}

View File

@@ -0,0 +1,133 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface SettingState {
// 弹窗状态
isOpen: boolean;
mount: boolean;
// 语音设置
autoRecognize: boolean;
listen: boolean;
recognitionLanguage: string;
// 火山引擎配置
volcengineAucAppId: string;
volcengineAucToken: string;
// 操作方法
openModal: () => void;
closeModal: () => void;
setAutoRecognize: (value: boolean) => void;
setListen: (value: boolean) => void;
setRecognitionLanguage: (language: string) => void;
setVolcengineAucAppId: (appId: string) => void;
setVolcengineAucToken: (token: string) => void;
resetToDefault: () => void;
}
const defaultSettings = {
autoRecognize: false,
listen: false,
recognitionLanguage: 'zh-CN',
volcengineAucAppId: '',
volcengineAucToken: '',
};
// 从原有的 localStorage key 读取初始值
const getInitialVolcengineConfig = () => {
try {
return {
volcengineAucAppId: localStorage.getItem('VOLCENGINE_AUC_APPID') || '',
volcengineAucToken: localStorage.getItem('VOLCENGINE_AUC_TOKEN') || '',
};
} catch (error) {
console.warn('Failed to read volcengine config from localStorage:', error);
return {
volcengineAucAppId: '',
volcengineAucToken: '',
};
}
};
export const useSettingStore = create<SettingState>()(
persist(
(set, get) => ({
// 初始状态 - 合并默认设置和从 localStorage 读取的火山引擎配置
isOpen: false,
...defaultSettings,
...getInitialVolcengineConfig(),
mount: false,
// 弹窗控制方法
openModal: () => set({ isOpen: true }),
closeModal: () => set({ isOpen: false }),
// 设置更新方法
setAutoRecognize: (value: boolean) => set({ autoRecognize: value }),
setListen: (value: boolean) => set({ listen: value }),
setRecognitionLanguage: (language: string) => set({ recognitionLanguage: language }),
setVolcengineAucAppId: (appId: string) => {
// 同时更新 zustand 状态和原有的 localStorage key
try {
localStorage.setItem('VOLCENGINE_AUC_APPID', appId);
} catch (error) {
console.warn('Failed to save VOLCENGINE_AUC_APPID to localStorage:', error);
}
set({ volcengineAucAppId: appId });
},
setVolcengineAucToken: (token: string) => {
// 同时更新 zustand 状态和原有的 localStorage key
try {
localStorage.setItem('VOLCENGINE_AUC_TOKEN', token);
} catch (error) {
console.warn('Failed to save VOLCENGINE_AUC_TOKEN to localStorage:', error);
}
set({ volcengineAucToken: token });
},
// 重置为默认设置
resetToDefault: () => {
try {
localStorage.removeItem('VOLCENGINE_AUC_APPID');
localStorage.removeItem('VOLCENGINE_AUC_TOKEN');
} catch (error) {
console.warn('Failed to remove volcengine config from localStorage:', error);
}
set({
...defaultSettings,
volcengineAucAppId: '',
volcengineAucToken: '',
});
},
}),
{
name: 'voice-settings',
partialize: (state) => ({
autoRecognize: state.autoRecognize,
listen: state.listen,
recognitionLanguage: state.recognitionLanguage,
mount: true,
// 火山引擎配置不通过 zustand persist 保存,而是直接使用原有的 localStorage key
}),
}
)
);
// 兼容原有 config.ts 的 API
export const getConfig = () => {
const state = useSettingStore.getState();
return {
VOLCENGINE_AUC_APPID: state.volcengineAucAppId,
VOLCENGINE_AUC_TOKEN: state.volcengineAucToken,
};
};
export const setConfig = (config: { VOLCENGINE_AUC_APPID?: string; VOLCENGINE_AUC_TOKEN?: string }) => {
const { setVolcengineAucAppId, setVolcengineAucToken } = useSettingStore.getState();
if (config.VOLCENGINE_AUC_APPID !== undefined) {
setVolcengineAucAppId(config.VOLCENGINE_AUC_APPID);
}
if (config.VOLCENGINE_AUC_TOKEN !== undefined) {
setVolcengineAucToken(config.VOLCENGINE_AUC_TOKEN);
}
};

View File

@@ -3,6 +3,7 @@ import { devtools, persist } from 'zustand/middleware';
import { Speak, getDayOfYear, CreateSpeakData } from '../modules/speak-db/speak';
import { speakService } from '../modules/speak-db/speak-service';
import { getText } from '../modules/text';
import { useSettingStore } from './settingStore';
interface VoiceState {
// 状态数据
@@ -105,7 +106,7 @@ export const useVoiceStore = create<VoiceState>()(
// 添加新的语音记录
addVoice: async (url: string, duration: number, audioBlob?: Blob) => {
const { setError } = get();
const autoRecognize = useSettingStore.getState().autoRecognize;
try {
setError(null);
@@ -123,8 +124,12 @@ export const useVoiceStore = create<VoiceState>()(
day: getDayOfYear(),
no: 0, // 将由 service 自动生成
timestamp: Date.now(),
type: 'normal' as const
type: 'normal' as const,
text: '', // 初始为空
};
if (autoRecognize) {
speakData.text = await getText(fileData || '').then(res => res.text);
}
// 保存到 IndexedDB不包含 url
const newSpeak = await speakService.createSpeakAuto(speakData);

View File

@@ -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,
}

View File

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

View File

@@ -0,0 +1,60 @@
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-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white 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 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",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
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 }

View File

@@ -0,0 +1,141 @@
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,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
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}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-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,
}