refactor: update chat ui (#13269)

### What problem does this PR solve?

Update **Chat** UI:
- Align to the design.
- Update `<AudioButton>` visualizer logic.
- Fix keyboard navigation issue.

### Type of change

- [x] Refactoring
This commit is contained in:
Jimmy Ben Klieve
2026-02-27 22:26:19 +08:00
committed by GitHub
parent 4e48aba5c4
commit c0823e8d6d
23 changed files with 785 additions and 602 deletions

163
web/package-lock.json generated
View File

@@ -34,9 +34,9 @@
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "2.1.4",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.6",
@@ -56,7 +56,7 @@
"ajv-formats": "^3.0.1",
"antd": "^5.12.7",
"axios": "^1.12.0",
"class-variance-authority": "^0.7.0",
"class-variance-authority": "^0.7.1",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
@@ -105,7 +105,7 @@
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"sonner": "^1.7.4",
"tailwind-merge": "^2.5.4",
"tailwind-merge": "^2.6.1",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss-animate": "^1.0.7",
"umi-request": "^1.4.0",
@@ -915,6 +915,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -1678,6 +1679,7 @@
"resolved": "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -1812,6 +1814,7 @@
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -7949,6 +7952,7 @@
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.25"
@@ -8212,6 +8216,7 @@
"resolved": "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.90.14.tgz",
"integrity": "sha512-JAMuULej09hrZ14W9+mxoRZ44rR2BuZfCd6oKTQVNfynQxCN3muH3jh3W46gqZNw5ZqY0ZVaS43Imb3dMr6tgw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/query-core": "5.90.14"
},
@@ -8879,6 +8884,7 @@
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.10.4.tgz",
"integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -8916,6 +8922,7 @@
"resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -8937,6 +8944,7 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -9089,6 +9097,7 @@
"integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.52.0",
"@typescript-eslint/types": "8.52.0",
@@ -9519,7 +9528,6 @@
"integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/parser": "^7.28.5",
"@vue/shared": "3.5.26",
@@ -9534,7 +9542,6 @@
"integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"engines": {
"node": ">=0.12"
},
@@ -9547,8 +9554,7 @@
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.26",
@@ -9556,7 +9562,6 @@
"integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-core": "3.5.26",
"@vue/shared": "3.5.26"
@@ -9568,7 +9573,6 @@
"integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/parser": "^7.28.5",
"@vue/compiler-core": "3.5.26",
@@ -9586,8 +9590,7 @@
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.26",
@@ -9595,7 +9598,6 @@
"integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.26",
"@vue/shared": "3.5.26"
@@ -9607,7 +9609,6 @@
"integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/shared": "3.5.26"
}
@@ -9618,7 +9619,6 @@
"integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.26",
"@vue/shared": "3.5.26"
@@ -9630,7 +9630,6 @@
"integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.26",
"@vue/runtime-core": "3.5.26",
@@ -9644,7 +9643,6 @@
"integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.5.26",
"@vue/shared": "3.5.26"
@@ -9658,8 +9656,7 @@
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.26.tgz",
"integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
@@ -9891,6 +9888,7 @@
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -10011,6 +10009,7 @@
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -10124,6 +10123,7 @@
"resolved": "https://registry.npmmirror.com/antd/-/antd-5.29.3.tgz",
"integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ant-design/colors": "^7.2.1",
"@ant-design/cssinjs": "^1.23.0",
@@ -10774,7 +10774,6 @@
"resolved": "https://registry.npmmirror.com/big.js/-/big.js-5.2.2.tgz",
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": "*"
}
@@ -10845,6 +10844,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -12226,6 +12226,7 @@
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -12394,6 +12395,7 @@
"resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
@@ -12952,7 +12954,8 @@
"version": "8.6.0",
"resolved": "https://registry.npmmirror.com/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
@@ -13001,7 +13004,6 @@
"resolved": "https://registry.npmmirror.com/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 4"
}
@@ -13011,6 +13013,7 @@
"resolved": "https://registry.npmmirror.com/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"license": "MIT",
"peer": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
@@ -13326,6 +13329,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -13436,6 +13440,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -15810,6 +15815,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.2"
}
@@ -15883,6 +15889,7 @@
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -16164,7 +16171,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=4"
}
@@ -16680,7 +16686,6 @@
"resolved": "https://registry.npmmirror.com/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
@@ -18189,7 +18194,6 @@
"resolved": "https://registry.npmmirror.com/lib0/-/lib0-0.2.117.tgz",
"integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==",
"license": "MIT",
"peer": true,
"dependencies": {
"isomorphic.js": "^0.2.4"
},
@@ -18837,7 +18841,6 @@
"resolved": "https://registry.npmmirror.com/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT",
"peer": true,
"bin": {
"marked": "bin/marked.js"
},
@@ -20098,23 +20101,11 @@
"ml-array-rescale": "^1.3.7"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": "*"
}
},
"node_modules/monaco-editor": {
"version": "0.55.1",
"resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT",
"peer": true,
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
@@ -20125,7 +20116,6 @@
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"peer": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
@@ -20136,7 +20126,6 @@
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=4"
}
@@ -21133,6 +21122,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -21406,6 +21396,7 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -21778,6 +21769,7 @@
"resolved": "https://registry.npmmirror.com/rc-field-form/-/rc-field-form-2.7.1.tgz",
"integrity": "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.0",
"@rc-component/async-validator": "^5.0.3",
@@ -22319,6 +22311,7 @@
"resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -22457,6 +22450,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -22768,6 +22762,7 @@
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -22846,6 +22841,7 @@
"resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.69.0.tgz",
"integrity": "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -23057,6 +23053,7 @@
"resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.11.0.tgz",
"integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
@@ -23824,7 +23821,6 @@
"integrity": "sha512-bfmJW1dmR2LvaMJuAnE88pZP9DktIFYXazkTfOIKZzi3Knk9lT0roItIA24ydOucI3bV/g/tXBA6hzqq3FV9Ew==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/mdast": "^3.0.0",
"remark-parse": "^10.0.0",
@@ -23974,7 +23970,6 @@
"integrity": "sha512-koyOzCMYoUHudypbj4XpnAKFbkddRMYZHwghnxd7ue5210WzGw6kOBwauJTRUMq16jsovXx8dYNvSSWP89kZ3A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/mdast": "^3.0.0",
"mdast-util-to-markdown": "^1.0.0",
@@ -23991,7 +23986,6 @@
"integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^2"
}
@@ -24001,8 +23995,7 @@
"resolved": "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz",
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/remark-stringify/node_modules/mdast-util-phrasing": {
"version": "3.0.1",
@@ -24010,7 +24003,6 @@
"integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/mdast": "^3.0.0",
"unist-util-is": "^5.0.0"
@@ -24026,7 +24018,6 @@
"integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/mdast": "^3.0.0",
"@types/unist": "^2.0.0",
@@ -24048,7 +24039,6 @@
"integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/mdast": "^3.0.0"
},
@@ -24073,7 +24063,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0"
@@ -24095,7 +24084,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-symbol": "^1.0.0"
}
@@ -24116,7 +24104,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"decode-named-character-reference": "^1.0.0",
"micromark-util-character": "^1.0.0",
@@ -24139,8 +24126,7 @@
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/remark-stringify/node_modules/micromark-util-types": {
"version": "1.1.0",
@@ -24157,8 +24143,7 @@
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/remark-stringify/node_modules/unified": {
"version": "10.1.2",
@@ -24166,7 +24151,6 @@
"integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^2.0.0",
"bail": "^2.0.0",
@@ -24187,7 +24171,6 @@
"integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^2.0.0"
},
@@ -24202,7 +24185,6 @@
"integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^2.0.0"
},
@@ -24217,7 +24199,6 @@
"integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0",
@@ -24234,7 +24215,6 @@
"integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0"
@@ -24250,7 +24230,6 @@
"integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^2.0.0",
"is-buffer": "^2.0.0",
@@ -24268,7 +24247,6 @@
"integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^2.0.0",
"unist-util-stringify-position": "^3.0.0"
@@ -24284,7 +24262,6 @@
"integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^2"
}
@@ -24294,8 +24271,7 @@
"resolved": "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz",
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/remark/node_modules/mdast-util-from-markdown": {
"version": "1.3.1",
@@ -24303,7 +24279,6 @@
"integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/mdast": "^3.0.0",
"@types/unist": "^2.0.0",
@@ -24329,7 +24304,6 @@
"integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/mdast": "^3.0.0"
},
@@ -24354,7 +24328,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@types/debug": "^4.0.0",
"debug": "^4.0.0",
@@ -24391,7 +24364,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"decode-named-character-reference": "^1.0.0",
"micromark-factory-destination": "^1.0.0",
@@ -24427,7 +24399,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-character": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
@@ -24450,7 +24421,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-character": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
@@ -24474,7 +24444,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-character": "^1.0.0",
"micromark-util-types": "^1.0.0"
@@ -24496,7 +24465,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-factory-space": "^1.0.0",
"micromark-util-character": "^1.0.0",
@@ -24520,7 +24488,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-factory-space": "^1.0.0",
"micromark-util-character": "^1.0.0",
@@ -24544,7 +24511,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0"
@@ -24566,7 +24532,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-symbol": "^1.0.0"
}
@@ -24587,7 +24552,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-character": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
@@ -24610,7 +24574,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-chunked": "^1.0.0",
"micromark-util-types": "^1.0.0"
@@ -24632,7 +24595,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-symbol": "^1.0.0"
}
@@ -24653,7 +24615,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"decode-named-character-reference": "^1.0.0",
"micromark-util-character": "^1.0.0",
@@ -24676,8 +24637,7 @@
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/remark/node_modules/micromark-util-html-tag-name": {
"version": "1.2.0",
@@ -24694,8 +24654,7 @@
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/remark/node_modules/micromark-util-normalize-identifier": {
"version": "1.1.0",
@@ -24713,7 +24672,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-symbol": "^1.0.0"
}
@@ -24734,7 +24692,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-types": "^1.0.0"
}
@@ -24755,7 +24712,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-character": "^1.0.0",
"micromark-util-encode": "^1.0.0",
@@ -24778,7 +24734,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"micromark-util-chunked": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
@@ -24801,8 +24756,7 @@
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/remark/node_modules/micromark-util-types": {
"version": "1.1.0",
@@ -24819,8 +24773,7 @@
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/remark/node_modules/remark-parse": {
"version": "10.0.2",
@@ -24828,7 +24781,6 @@
"integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/mdast": "^3.0.0",
"mdast-util-from-markdown": "^1.0.0",
@@ -24845,7 +24797,6 @@
"integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^2.0.0",
"bail": "^2.0.0",
@@ -24866,7 +24817,6 @@
"integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^2.0.0"
},
@@ -24881,7 +24831,6 @@
"integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^2.0.0",
"is-buffer": "^2.0.0",
@@ -24899,7 +24848,6 @@
"integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^2.0.0",
"unist-util-stringify-position": "^3.0.0"
@@ -25110,6 +25058,7 @@
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.54.0.tgz",
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -25181,7 +25130,6 @@
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"mri": "^1.1.0"
},
@@ -25334,6 +25282,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -25818,6 +25767,7 @@
"integrity": "sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@storybook/global": "^5.0.0",
"@testing-library/jest-dom": "^6.6.3",
@@ -26369,9 +26319,9 @@
}
},
"node_modules/tailwind-merge": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
"integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
"version": "2.6.1",
"resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.1.tgz",
"integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==",
"license": "MIT",
"funding": {
"type": "github",
@@ -26395,6 +26345,7 @@
"resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -26477,6 +26428,7 @@
"resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"license": "MIT",
"peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -26973,6 +26925,7 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -27188,6 +27141,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -27574,7 +27528,6 @@
"integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"dequal": "^2.0.0",
"diff": "^5.0.0",
@@ -27594,7 +27547,6 @@
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true,
"engines": {
"node": ">=0.3.1"
}
@@ -27605,7 +27557,6 @@
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
@@ -27733,6 +27684,7 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -28620,6 +28572,7 @@
"resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.104.1.tgz",
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -29010,7 +28963,6 @@
"resolved": "https://registry.npmmirror.com/workerize-loader/-/workerize-loader-2.0.2.tgz",
"integrity": "sha512-HoZ6XY4sHWxA2w0WpzgBwUiR3dv1oo7bS+oCwIpb6n54MclQ/7KXdXsVIChTCygyuHtVuGBO1+i3HzTt699UJQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loader-utils": "^2.0.0"
},
@@ -29023,7 +28975,6 @@
"resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"license": "MIT",
"peer": true,
"dependencies": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",

View File

@@ -50,9 +50,9 @@
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "2.1.4",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.6",
@@ -72,7 +72,7 @@
"ajv-formats": "^3.0.1",
"antd": "^5.12.7",
"axios": "^1.12.0",
"class-variance-authority": "^0.7.0",
"class-variance-authority": "^0.7.1",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
@@ -121,7 +121,7 @@
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"sonner": "^1.7.4",
"tailwind-merge": "^2.5.4",
"tailwind-merge": "^2.6.1",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss-animate": "^1.0.7",
"umi-request": "^1.4.0",

View File

@@ -1,5 +1,5 @@
import { transformFile2Base64 } from '@/utils/file-util';
import { Pencil, Plus, XIcon } from 'lucide-react';
import { LucidePencil, LucidePlus, LucideX } from 'lucide-react';
import {
ChangeEventHandler,
forwardRef,
@@ -45,7 +45,7 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
}
ev.target.value = '';
},
[onChange],
[],
);
const handleRemove = useCallback(() => {
@@ -249,30 +249,36 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
};
}
}, 100);
}, [handleWheel, containerRef.current]);
}, [handleWheel, imageToCrop, isCropModalOpen]);
return (
<div className="flex justify-start items-end space-x-2">
<div className="relative group">
{!avatarBase64Str ? (
<div className="w-[64px] h-[64px] grid place-content-center border border-dashed bg-bg-input rounded-md">
<div className="flex flex-col items-center">
<Plus />
<p>{t('common.upload')}</p>
</div>
<div
className="
border border-dashed border-borer-button rounded-md size-16
flex flex-col gap-1 justify-center items-center text-sm text-text-secondary transition-colors
group-has-[input:focus-visible]:border-accent-primary group-has-[input:focus-visible]:text-text-primary"
>
<LucidePlus className="size-4" />
<span>{t('common.upload')}</span>
</div>
) : (
<div className="w-[64px] h-[64px] relative grid place-content-center">
<Avatar className="w-[64px] h-[64px] rounded-md">
<div className="size-16 relative grid place-content-center">
<Avatar className="size-16 rounded-md">
<AvatarImage className="block" src={avatarBase64Str} alt="" />
<AvatarFallback></AvatarFallback>
</Avatar>
<div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60">
<Pencil
size={20}
className="absolute right-2 bottom-0 opacity-50 hidden group-hover:block"
/>
<div
className="
absolute inset-0 bg-black/50 flex items-center justify-center
transition-opacity opacity-0 group-hover:opacity-100 group-has-[input:focus-visible]:opacity-100"
>
<LucidePencil className="size-5 opacity-75" />
</div>
<Button
onClick={handleRemove}
size="icon"
@@ -280,10 +286,11 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
aria-label="Remove image"
type="button"
>
<XIcon className="size-3.5" />
<LucideX className="size-3" />
</Button>
</div>
)}
<Input
placeholder=""
type="file"
@@ -294,7 +301,7 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
ref={ref}
/>
</div>
<div className="margin-1 text-text-secondary">
<div className="margin-1 text-sm text-text-secondary">
{tips ?? t('knowledgeConfiguration.photoTip')}
</div>

View File

@@ -9,7 +9,7 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { AlertDialogOverlay } from '@radix-ui/react-alert-dialog';
import { DialogProps } from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -55,53 +55,48 @@ export function ConfirmDeleteDialog({
defaultOpen={defaultOpen}
>
{children && <AlertDialogTrigger asChild>{children}</AlertDialogTrigger>}
<AlertDialogOverlay
onClick={(e) => {
e.stopPropagation();
}}
<AlertDialogContent
onSelect={(e) => e.preventDefault()}
onClick={(e) => e.stopPropagation()}
className="bg-bg-base "
>
<AlertDialogContent
onSelect={(e) => e.preventDefault()}
onClick={(e) => e.stopPropagation()}
className="bg-bg-base "
>
<AlertDialogHeader className="space-y-5">
<AlertDialogTitle>
{title ?? t('common.deleteModalTitle')}
<AlertDialogCancel
onClick={onCancel}
className="border-none bg-transparent hover:border-none hover:bg-transparent absolute right-3 top-3 hover:text-text-primary"
>
<X size={16} />
</AlertDialogCancel>
</AlertDialogTitle>
{content && (
<>
<Separator className="w-[calc(100%+48px)] -translate-x-6"></Separator>
<AlertDialogDescription className="mt-5">
<div className="flex flex-col gap-5 text-base mb-10 px-5">
<div className="text-text-primary">
{content.title || t('common.deleteModalTitle')}
</div>
{content.node}
</div>
</AlertDialogDescription>
</>
)}
</AlertDialogHeader>
<AlertDialogFooter className="px-5 flex items-center gap-2">
<AlertDialogCancel onClick={onCancel}>
{cancelButtonText || t('common.cancel')}
</AlertDialogCancel>
<AlertDialogAction
className="bg-state-error text-text-primary hover:text-text-primary hover:bg-state-error"
onClick={onOk}
<AlertDialogHeader className="space-y-5">
<AlertDialogTitle>
{title ?? t('common.deleteModalTitle')}
<AlertDialogCancel
onClick={onCancel}
className="border-none bg-transparent hover:border-none hover:bg-transparent absolute right-3 top-3 hover:text-text-primary"
>
{okButtonText || t('common.delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
<X size={16} />
</AlertDialogCancel>
</AlertDialogTitle>
{content && (
<>
<Separator className="w-[calc(100%+48px)] -translate-x-6"></Separator>
<AlertDialogDescription className="mt-5">
<div className="flex flex-col gap-5 text-base mb-10 px-5">
<div className="text-text-primary">
{content.title || t('common.deleteModalTitle')}
</div>
{content.node}
</div>
</AlertDialogDescription>
</>
)}
</AlertDialogHeader>
<AlertDialogFooter className="px-5 flex items-center gap-2">
<AlertDialogCancel onClick={onCancel}>
{cancelButtonText || t('common.cancel')}
</AlertDialogCancel>
<AlertDialogAction
className="bg-state-error text-text-primary hover:text-text-primary hover:bg-state-error"
onClick={onOk}
>
{okButtonText || t('common.delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -1,10 +1,16 @@
import { useTranslate } from '@/hooks/common-hooks';
import { CheckOutlined, CopyOutlined } from '@ant-design/icons';
import { cn } from '@/lib/utils';
import { LucideCheck, LucideCopy } from 'lucide-react';
import { useState } from 'react';
import { CopyToClipboard as Clipboard, Props } from 'react-copy-to-clipboard';
import { CopyToClipboard as Clipboard } from 'react-copy-to-clipboard';
import { Button, ButtonProps } from './ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
const CopyToClipboard = ({ text }: Props) => {
const CopyToClipboard = ({
text,
className,
...buttonProps
}: { text: string } & ButtonProps) => {
const [copied, setCopied] = useState(false);
const { t } = useTranslate('common');
@@ -16,12 +22,19 @@ const CopyToClipboard = ({ text }: Props) => {
};
return (
<Tooltip>
<TooltipTrigger>
<Clipboard text={text} onCopy={handleCopy}>
{copied ? <CheckOutlined /> : <CopyOutlined />}
</Clipboard>
</TooltipTrigger>
<Tooltip open={copied ? true : undefined}>
<Clipboard text={text} onCopy={handleCopy}>
<TooltipTrigger asChild>
<Button
variant="transparent"
size="icon-sm"
{...buttonProps}
className={cn(className, copied && '!text-state-success')}
>
{copied ? <LucideCheck /> : <LucideCopy />}
</Button>
</TooltipTrigger>
</Clipboard>
<TooltipContent>{copied ? t('copied') : t('copy')}</TooltipContent>
</Tooltip>
);

View File

@@ -14,7 +14,6 @@ import {
} from '@/components/file-upload';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { t } from 'i18next';
import {
Atom,
@@ -159,7 +158,7 @@ export function NextMessageInput({
onValueChange={setFiles}
onUpload={onUpload}
onFileReject={onFileReject}
className="relative w-full items-center "
className="relative w-full items-center"
disabled={isUploading || disabled}
>
<FileUploadDropzone
@@ -178,9 +177,14 @@ export function NextMessageInput({
</p>
</div>
</FileUploadDropzone>
<form
onSubmit={onSubmit}
className="relative flex w-full flex-col gap-2.5 rounded-md border border-input px-3 py-2 outline-none focus-within:ring-1 focus-within:ring-ring/50"
className="
relative flex w-full flex-col gap-2.5 rounded-md
border-0.5 border-border-default bg-bg-card p-2 outline-none
has-[textarea:focus]:outline-accent-primary has-[textarea:focus]:outline-1 has-[textarea:focus]:outline-offset-2
"
>
<FileUploadList
orientation="horizontal"
@@ -205,25 +209,29 @@ export function NextMessageInput({
</FileUploadItem>
))}
</FileUploadList>
<Textarea
value={value}
onChange={onInputChange}
placeholder={t('chat.messagePlaceholder')}
className="min-h-10 max-h-40 w-full border-0 bg-transparent p-0 shadow-none focus-visible:ring-0 dark:bg-transparent"
className="
min-h-10 max-h-40 w-full p-0 overflow-auto
!outline-none !border-transparent !bg-transparent !shadow-none !ring-transparent !ring-offset-transparent
"
disabled={isUploading || disabled || sendLoading}
onKeyDown={handleKeyDown}
autoSize={{ minRows: 1, maxRows: 8 }}
resize={resize}
autoSize={{ minRows: 2, maxRows: 8 }}
/>
<div className={cn('flex items-center justify-between gap-1.5')}>
<div className="flex items-center gap-3">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
{showUploadIcon && (
<FileUploadTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className="size-7 rounded-sm"
size="icon-xs"
variant="transparent"
className="rounded-sm border-0"
disabled={isUploading || sendLoading}
>
<Paperclip className="size-3.5" />
@@ -231,40 +239,35 @@ export function NextMessageInput({
</Button>
</FileUploadTrigger>
)}
{showReasoning && (
<Button
type="button"
variant="ghost"
className={cn(
'rounded-sm h-7 focus-visible:bg-none! hover:bg-none!',
{
'bg-accent-primary text-white': enableThinking,
},
)}
size="sm"
variant={enableThinking ? 'accent' : 'transparent'}
className="border-0 h-7 text-sm"
onClick={handleThinkingToggle}
>
<Atom />
<span>Thinking</span>
</Button>
)}
{showInternet && (
<Button
type="button"
variant="ghost"
className={cn(
'rounded-sm h-7 focus-visible:bg-none! hover:bg-none!',
{
'bg-accent-primary text-white': enableInternet,
},
)}
variant={enableInternet ? 'accent' : 'transparent'}
size="icon-xs"
className="border-0"
onClick={handleInternetToggle}
>
<Globe />
</Button>
)}
</div>
{sendLoading ? (
<Button onClick={stopOutputMessage} className="size-5 rounded-sm">
<Button onClick={stopOutputMessage} size="icon-xs">
<CircleStop />
</Button>
) : (
@@ -274,8 +277,9 @@ export function NextMessageInput({
setAudioInputValue(value);
}}
/>
<Button
className="size-5 rounded-sm"
size="icon-xs"
disabled={
sendDisabled || isUploading || sendLoading || !value.trim()
}

View File

@@ -1,6 +1,5 @@
import { PromptIcon } from '@/assets/icon/next-icon';
import CopyToClipboard from '@/components/copy-to-clipboard';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import {
Tooltip,
TooltipContent,
@@ -8,18 +7,20 @@ import {
} from '@/components/ui/tooltip';
import { useSetModalState } from '@/hooks/common-hooks';
import { IRemoveMessageById } from '@/hooks/logic-hooks';
import { cn } from '@/lib/utils';
import {
DeleteOutlined,
DislikeOutlined,
LikeOutlined,
PauseCircleOutlined,
SoundOutlined,
SyncOutlined,
} from '@ant-design/icons';
LucidePauseCircle,
LucideRefreshCw,
LucideThumbsDown,
LucideThumbsUp,
LucideTrash2,
LucideVolume2,
} from 'lucide-react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import FeedbackDialog from '../feedback-dialog';
import { PromptDialog } from '../prompt-dialog';
import { Button } from '../ui/button';
import { useRemoveMessage, useSendFeedback, useSpeech } from './hooks';
interface IProps {
@@ -55,44 +56,57 @@ export const AssistantGroupButton = ({
return (
<>
<ToggleGroup
type="single"
size="sm"
variant="outline"
className="space-x-1"
>
<ToggleGroupItem value="a">
<CopyToClipboard text={content}></CopyToClipboard>
</ToggleGroupItem>
<div className="flex gap-1" role="toolbar">
<CopyToClipboard text={content} className="border-0" size="icon-xs" />
{showLoudspeaker && (
<ToggleGroupItem value="b" onClick={handleRead}>
<>
<Tooltip>
<TooltipTrigger asChild>
<span>
{isPlaying ? <PauseCircleOutlined /> : <SoundOutlined />}
</span>
<Button
variant="transparent"
size="icon-xs"
className="border-0"
onClick={handleRead}
>
<span>
{isPlaying ? <LucidePauseCircle /> : <LucideVolume2 />}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t('chat.read')}</TooltipContent>
</Tooltip>
<audio src="" ref={ref}></audio>
</ToggleGroupItem>
</>
)}
{showLikeButton && (
<>
<ToggleGroupItem value="c" onClick={handleLike}>
<LikeOutlined />
</ToggleGroupItem>
<ToggleGroupItem value="d" onClick={showModal}>
<DislikeOutlined />
</ToggleGroupItem>
<Button
variant="transparent"
size="icon-xs"
className="border-0"
onClick={handleLike}
>
<LucideThumbsUp />
</Button>
<Button
variant="transparent"
size="icon-xs"
className="border-0"
onClick={showModal}
>
<LucideThumbsDown />
</Button>
</>
)}
{prompt && (
<ToggleGroupItem value="e" onClick={showPromptModal}>
<Button onClick={showPromptModal}>
<PromptIcon style={{ fontSize: '16px' }} />
</ToggleGroupItem>
</Button>
)}
</ToggleGroup>
</div>
{visible && (
<FeedbackDialog
visible={visible}
@@ -133,39 +147,41 @@ export const UserGroupButton = ({
const { t } = useTranslation();
return (
<ToggleGroup
type="single"
size="sm"
variant="outline"
className="space-x-1"
>
<ToggleGroupItem value="a">
<CopyToClipboard text={content}></CopyToClipboard>
</ToggleGroupItem>
<div className="flex gap-1">
<CopyToClipboard text={content} className="border-0" size="icon-xs" />
{regenerateMessage && (
<ToggleGroupItem
value="b"
onClick={regenerateMessage}
disabled={sendLoading}
>
<Tooltip>
<TooltipTrigger asChild>
<SyncOutlined spin={sendLoading} />
</TooltipTrigger>
<TooltipContent>{t('chat.regenerate')}</TooltipContent>
</Tooltip>
</ToggleGroupItem>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="transparent"
size="icon-xs"
className="border-0"
onClick={regenerateMessage}
disabled={sendLoading}
>
<LucideRefreshCw className={cn(sendLoading && 'animate-spin')} />
</Button>
</TooltipTrigger>
<TooltipContent>{t('chat.regenerate')}</TooltipContent>
</Tooltip>
)}
{removeMessageById && (
<ToggleGroupItem value="c" onClick={onRemoveMessage} disabled={loading}>
<Tooltip>
<TooltipTrigger asChild>
<DeleteOutlined spin={loading} />
</TooltipTrigger>
<TooltipContent>{t('common.delete')}</TooltipContent>
</Tooltip>
</ToggleGroupItem>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="transparent"
size="icon-xs"
className="border-0"
onClick={onRemoveMessage}
disabled={loading}
>
<LucideTrash2 />
</Button>
</TooltipTrigger>
<TooltipContent>{t('common.delete')}</TooltipContent>
</Tooltip>
)}
</ToggleGroup>
</div>
);
};

View File

@@ -32,6 +32,7 @@
.messageTextDark {
.chunkText();
.messageTextBase();
padding: 0;
word-break: break-word;
:global(section.think) {
color: rgb(166, 166, 166);

View File

@@ -4,14 +4,15 @@ import React from 'react';
import { Button, ButtonProps } from './ui/button';
export const MoreButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, size, ...props }, ref) => {
function MoreButton({ className, size, ...props }, ref) {
return (
<Button
ref={ref}
variant="ghost"
size={size || 'icon'}
className={cn(
'invisible group-hover:visible size-3.5 bg-transparent group-hover:bg-transparent',
'invisible size-3.5 bg-transparent group-hover:bg-transparent',
'group-focus-within:visible group-hover:visible aria-expanded:visible',
className,
)}
{...props}

View File

@@ -5,11 +5,13 @@ import { Authorization } from '@/constants/authorization';
import { cn } from '@/lib/utils';
import api from '@/utils/api';
import { getAuthorization } from '@/utils/authorization-util';
import { chain, sum } from 'lodash';
import { Loader2, Mic, Square } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useIsDarkTheme } from '../theme-provider';
import { Input } from './input';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
const VoiceVisualizer = ({ isRecording }: { isRecording: boolean }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const audioContextRef = useRef<AudioContext | null>(null);
@@ -18,7 +20,79 @@ const VoiceVisualizer = ({ isRecording }: { isRecording: boolean }) => {
const streamRef = useRef<MediaStream | null>(null);
const isDark = useIsDarkTheme();
const startVisualization = async () => {
const draw = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const analyser = analyserRef.current;
if (!analyser) return;
// Set canvas dimensions
const width = canvas.clientWidth * window.devicePixelRatio;
const height = canvas.clientHeight * window.devicePixelRatio;
const centerY = height / 2;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Get frequency data
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
/**
* The frequencies are spread linearly from 0Hz to 1/2 of the sample rate
* @see https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/getByteFrequencyData
*/
analyser.getByteFrequencyData(dataArray);
const desiredBarCount = 6;
const freqSpanPerPole =
analyser.context.sampleRate / 2 / analyser.frequencyBinCount;
const freqMinIndex = Math.max(1, Math.floor(100 / freqSpanPerPole));
const freqMaxIndex =
Math.max(freqMinIndex, Math.ceil(3500 / freqSpanPerPole)) + 1;
const freqData = dataArray.slice(
freqMinIndex,
Math.max(freqMinIndex + desiredBarCount, freqMaxIndex),
);
const reducedFreqData = chain(freqData)
.chunk(Math.floor(freqData.length / desiredBarCount))
.take(desiredBarCount)
.map(sum)
.value();
// Draw waveform
const barGap = 1 * window.devicePixelRatio;
const barWidth = (width - barGap * (desiredBarCount - 1)) / desiredBarCount;
// Create gradient
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, '#3ba05c'); // Blue
gradient.addColorStop(1, '#3ba05c'); // Light blue
// gradient.addColorStop(0, isDark ? '#fff' : '#000'); // Blue
// gradient.addColorStop(1, isDark ? '#eee' : '#eee'); // Light blue
for (let i = 0, x = 0; i < desiredBarCount; i++, x += barWidth + barGap) {
const barHeight = (reducedFreqData[i] / 255) * centerY;
ctx.fillStyle = gradient;
ctx.fillRect(x, centerY - barHeight, barWidth, barHeight * 2);
}
animationFrameRef.current = requestAnimationFrame(draw);
}, []);
const startVisualization = useCallback(async () => {
try {
// Check if the browser supports getUserMedia
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
@@ -30,13 +104,14 @@ const VoiceVisualizer = ({ isRecording }: { isRecording: boolean }) => {
streamRef.current = stream;
// Create audio context and analyzer
const audioContext = new (window.AudioContext ||
(window as any).webkitAudioContext)();
const audioContext = new (
window.AudioContext || (window as any).webkitAudioContext
)();
audioContextRef.current = audioContext;
const analyser = audioContext.createAnalyser();
analyserRef.current = analyser;
analyser.fftSize = 32;
analyser.fftSize = 256;
// Connect audio nodes
const source = audioContext.createMediaStreamSource(stream);
@@ -50,9 +125,9 @@ const VoiceVisualizer = ({ isRecording }: { isRecording: boolean }) => {
error,
);
}
};
}, [draw]);
const stopVisualization = () => {
const stopVisualization = useCallback(() => {
// Stop animation frame
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
@@ -76,7 +151,8 @@ const VoiceVisualizer = ({ isRecording }: { isRecording: boolean }) => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
};
}, []);
useEffect(() => {
if (isRecording) {
startVisualization();
@@ -87,68 +163,9 @@ const VoiceVisualizer = ({ isRecording }: { isRecording: boolean }) => {
return () => {
stopVisualization();
};
}, [isRecording]);
const draw = () => {
const canvas = canvasRef.current;
if (!canvas) return;
}, [isRecording, startVisualization, stopVisualization]);
const ctx = canvas.getContext('2d');
if (!ctx) return;
const analyser = analyserRef.current;
if (!analyser) return;
// Set canvas dimensions
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const centerY = height / 2;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Get frequency data
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
// Draw waveform
const barWidth = (width / bufferLength) * 1.5;
let x = 0;
for (let i = 0; i < bufferLength; i = i + 2) {
const barHeight = (dataArray[i] / 255) * centerY;
// Create gradient
const gradient = ctx.createLinearGradient(
0,
centerY - barHeight,
0,
centerY + barHeight,
);
gradient.addColorStop(0, '#3ba05c'); // Blue
gradient.addColorStop(1, '#3ba05c'); // Light blue
// gradient.addColorStop(0, isDark ? '#fff' : '#000'); // Blue
// gradient.addColorStop(1, isDark ? '#eee' : '#eee'); // Light blue
ctx.fillStyle = gradient;
ctx.fillRect(x, centerY - barHeight, barWidth, barHeight * 2);
x += barWidth + 2;
}
animationFrameRef.current = requestAnimationFrame(draw);
};
return (
<div className="w-full h-6 bg-transparent flex items-center justify-center overflow-hidden ">
<canvas ref={canvasRef} className="w-full h-full" />
</div>
);
return <canvas ref={canvasRef} className="block size-full" />;
};
const VoiceInputBox = ({
@@ -363,21 +380,22 @@ export const AudioButton = ({
<div className=" relative w-6 h-6 flex items-center justify-center">
{isRecording && (
<div
className={cn(
'absolute inset-0 w-full h-6 rounded-full overflow-hidden flex items-center justify-center p-1',
{ 'bg-state-success-5': isRecording },
)}
>
<div className="absolute inset-0 size-full overflow-hidden flex items-center justify-center p-1">
<VoiceVisualizer isRecording={isRecording} />
</div>
)}
{isRecording && (
<div className="absolute inset-0 rounded-full border-2 border-state-success animate-ping opacity-75"></div>
<div
className="
absolute inset-0 rounded-full border-2 border-state-success
animate-ping duration-[4s] opacity-75 pointer-events-none"
/>
)}
<Button
variant="outline"
size="sm"
variant="transparent"
size="icon-xs"
// onMouseDown={() => {
// startRecording();
// }}
@@ -391,11 +409,11 @@ export const AudioButton = ({
startRecording();
}
}}
className={`w-6 h-6 p-2 rounded-md border-none bg-transparent hover:bg-state-success-5 ${
isRecording
? 'animate-pulse bg-state-success-5 text-state-success'
: ''
}`}
className={cn(
'border-0 p-2 rounded-md border-none bg-transparent hover:bg-state-success-5',
isRecording &&
'animate-pulse !bg-state-success/20 text-state-success rounded-full',
)}
disabled={isProcessing}
>
{isProcessing ? (

View File

@@ -53,7 +53,10 @@ const BreadcrumbLink = React.forwardRef<
return (
<Comp
ref={ref}
className={cn('transition-colors hover:text-foreground', className)}
className={cn(
'transition-colors hover:text-foreground focus-visible:text-foreground',
className,
)}
{...props}
/>
);

View File

@@ -0,0 +1,83 @@
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { Separator } from '@/components/ui/separator';
import { cn } from '@/lib/utils';
const buttonGroupVariants = cva(
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
{
variants: {
orientation: {
horizontal:
'[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none',
vertical:
'flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
},
},
defaultVariants: {
orientation: 'horizontal',
},
},
);
function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<'div'> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
);
}
function ButtonGroupText({
className,
asChild = false,
...props
}: React.ComponentProps<'div'> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : 'div';
return (
<Comp
className={cn(
"bg-muted shadow-xs flex items-center gap-2 rounded-md border px-4 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
className,
)}
{...props}
/>
);
}
function ButtonGroupSeparator({
className,
orientation = 'vertical',
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
'bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto',
className,
)}
{...props}
/>
);
}
export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
};

View File

@@ -36,7 +36,7 @@ const buttonVariants = cva(
link: 'text-primary underline-offset-4 hover:underline',
icon: 'bg-colors-background-inverse-standard text-foreground hover:bg-colors-background-inverse-standard/80',
dashed: 'border border-dashed border-input hover:bg-accent',
dashed: 'border border-dashed border-border-button hover:bg-accent',
transparent: `
text-text-secondary bg-transparent border-0.5 border-border-button
@@ -49,10 +49,16 @@ const buttonVariants = cva(
hover:bg-state-error/10 focus-visible:bg-state-error/10
`,
accent: `
bg-accent-primary text-white
hover:bg-accent-primary/90 focus-visible:bg-accent-primary/90
`,
highlighted: `
bg-text-primary text-bg-base border-b-4 border-b-accent-primary
hover:bg-text-primary/90 focus-visible:bg-text-primary/90
`,
delete: `
text-text-secondary
hover:bg-state-error-5 hover:text-state-error
@@ -65,6 +71,8 @@ const buttonVariants = cva(
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
auto: 'h-full px-1',
'icon-sm': 'size-8',
'icon-xs': 'size-7',
},
},
defaultVariants: {
@@ -75,7 +83,8 @@ const buttonVariants = cva(
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
@@ -126,7 +135,7 @@ ButtonLoading.displayName = 'ButtonLoading';
export { Button, buttonVariants };
export const BlockButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ children, className, ...props }, ref) => {
function BlockButton({ children, className, ...props }, ref) {
return (
<Button
variant={'outline'}

View File

@@ -5,8 +5,9 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
interface DualRangeSliderProps
extends React.ComponentProps<typeof SliderPrimitive.Root> {
interface DualRangeSliderProps extends React.ComponentProps<
typeof SliderPrimitive.Root
> {
labelPosition?: 'top' | 'bottom';
label?: (value: number | undefined) => React.ReactNode;
}
@@ -33,7 +34,12 @@ const DualRangeSlider = React.forwardRef<
</SliderPrimitive.Track>
{initialValue.map((value, index) => (
<React.Fragment key={index}>
<SliderPrimitive.Thumb className="relative block h-2.5 w-2.5 rounded-full border-2 border-accent-primary bg-white ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 cursor-pointer">
<SliderPrimitive.Thumb
className="
relative block h-2.5 w-2.5 rounded-full border-2 border-accent-primary bg-white ring-offset-background transition-colors
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2
disabled:pointer-events-none disabled:opacity-50 cursor-pointer"
>
{label && (
<span
className={cn(

View File

@@ -20,7 +20,13 @@ const Slider = React.forwardRef<
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-colors-background-inverse-strong">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-colors-text-core-standard ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
<SliderPrimitive.Thumb
className="
block h-5 w-5 rounded-full border-2 border-primary bg-colors-text-core-standard transition-colors ring-offset-background
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2
disabled:pointer-events-none disabled:opacity-50"
/>
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
@@ -33,16 +39,18 @@ type SliderProps = Omit<
const FormSlider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
SliderProps
>(({ onChange, value, ...props }, ref) => (
<Slider
ref={ref}
{...props}
value={[value]}
onValueChange={(vals) => {
onChange(vals[0]);
}}
></Slider>
));
>(function FormSlider({ onChange, value, ...props }, ref) {
return (
<Slider
ref={ref}
{...props}
value={[value]}
onValueChange={(vals) => {
onChange(vals[0]);
}}
/>
);
});
Slider.displayName = SliderPrimitive.Root.displayName;

View File

@@ -891,7 +891,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
tocEnhance: 'PageIndex',
tocEnhanceTip: ` During the parsing of the document, table of contents information was generated (see the 'Enable Table of Contents Extraction' option in the General method). This allows the large model to return table of contents items relevant to the user's query, thereby using these items to retrieve related chunks and apply weighting to these chunks during the sorting process. This approach mimics human information-searching behavior in books.`,
batchDeleteSessions: 'Batch delete',
deleteSelectedConfirm: 'Delete the selected {count} session(s)?',
deleteSelectedConfirm: 'Delete the selected {{count}} session(s)?',
},
setting: {
Verify: 'Verify',

View File

@@ -1,5 +1,6 @@
import { Button } from '@/components/ui/button';
import { Form } from '@/components/ui/form';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
import { DatasetMetadata } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
@@ -10,7 +11,7 @@ import {
} from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { isEmpty, omit } from 'lodash';
import { PanelRightClose, Settings } from 'lucide-react';
import { LucidePanelRightClose, LucideSettings } from 'lucide-react';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
@@ -103,39 +104,51 @@ export function ChatSettings({ hasSingleChatBox }: ChatSettingsProps) {
return (
<div className="p-5">
<Button
className="w-full"
onClick={switchSettingVisible}
disabled={!hasSingleChatBox}
variant={'ghost'}
size="icon-sm"
>
<Settings />
<LucideSettings />
</Button>
</div>
);
}
return (
<section className="p-5 w-[440px] flex flex-col">
<div className="flex justify-between items-center text-base pb-2">
<section className="w-[440px] flex flex-col">
<div className="p-5 pb-2 flex justify-between items-center text-base">
{t('chat.chatSetting')}
<PanelRightClose
className="size-4 cursor-pointer"
<Button
variant="transparent"
size="icon-sm"
className="border-0"
onClick={switchSettingVisible}
/>
>
<LucidePanelRightClose
className="size-4 cursor-pointer"
onClick={switchSettingVisible}
/>
</Button>
</div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit, onInvalid)}
className="flex-1 flex flex-col min-h-0"
>
<section className="space-y-6 overflow-auto flex-1 pr-4 min-h-0">
<ChatBasicSetting></ChatBasicSetting>
<Separator />
<ChatPromptEngine></ChatPromptEngine>
<Separator />
<ChatModelSettings></ChatModelSettings>
</section>
<div className="space-x-5 text-right pt-4">
<ScrollArea>
<section className="p-5 space-y-6 overflow-auto flex-1 min-h-0">
<ChatBasicSetting></ChatBasicSetting>
<Separator />
<ChatPromptEngine></ChatPromptEngine>
<Separator />
<ChatModelSettings></ChatModelSettings>
</section>
</ScrollArea>
<div className="p-5 pt-4 space-x-5 text-right">
<Button variant={'outline'} onClick={switchSettingVisible}>
{t('chat.cancel')}
</Button>

View File

@@ -38,25 +38,33 @@ export function DynamicVariableForm() {
<FormLabel tooltip={t('chat.variableTip')}>
{t('chat.variable')}
</FormLabel>
<Button variant={'ghost'} type="button" onClick={add}>
<Button
variant="ghost"
size="icon-sm"
className="border-0"
type="button"
onClick={add}
>
<Plus />
</Button>
</div>
<div className="flex gap-2 pr-12 text-text-secondary text-xs">
<span className="flex-1">{t('chat.key')}</span>
<span className="w-3"></span>
<span className="flex-1">{t('chat.optional')}</span>
</div>
<div className="space-y-5">
{fields.map((field, index) => {
const typeField = `${name}.${index}.key`;
return (
<div key={field.id} className="flex w-full items-center gap-2">
<div className="grid grid-cols-[1fr_auto_auto_auto] items-center gap-2">
<div className="contents text-text-secondary text-xs">
<span>{t('chat.key')}</span>
<span />
<span>{t('chat.optional')}</span>
<span />
</div>
<div className="grid grid-cols-subgrid items-center col-span-4 gap-y-4">
{fields.map((field, index) => (
<div key={field.id} className="contents">
<FormField
control={form.control}
name={typeField}
name={`${name}.${index}.key`}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormItem className="flex-1">
<FormControl>
<BlurInput
{...field}
@@ -67,12 +75,14 @@ export function DynamicVariableForm() {
</FormItem>
)}
/>
<Separator className="w-3 text-text-secondary" />
<FormField
control={form.control}
name={`${name}.${index}.optional`}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormItem className="flex-1">
<FormControl>
<Switch
checked={field.value}
@@ -83,12 +93,19 @@ export function DynamicVariableForm() {
</FormItem>
)}
/>
<Button variant={'ghost'} onClick={() => remove(index)}>
<Button
variant="ghost"
size="icon-sm"
className="border-0"
type="button"
onClick={() => remove(index)}
>
<X className="text-text-sub-title-invert " />
</Button>
</div>
);
})}
))}
</div>
</div>
</section>
);

View File

@@ -163,7 +163,7 @@ const ChatCard = forwardRef(function ChatCard(
return (
<Card className="bg-transparent border flex-1 flex flex-col">
<CardHeader className="border-b px-5 py-3">
<CardHeader className="border-b-0.5 px-5 py-3">
<CardTitle className="flex justify-between items-center">
<div className="flex items-center gap-3">
<span className="text-base">{idx + 1}</span>
@@ -175,7 +175,8 @@ const ChatCard = forwardRef(function ChatCard(
<Tooltip>
<TooltipTrigger>
<Button
variant={'ghost'}
variant="ghost"
size="icon-sm"
disabled={isEmpty(llmId)}
onClick={handleApplyConfig}
>
@@ -187,12 +188,16 @@ const ChatCard = forwardRef(function ChatCard(
</TooltipContent>
</Tooltip>
{!isLatestChat || chatBoxIds.length === 3 ? (
<Button variant={'ghost'} onClick={handleRemoveChatBox}>
<Button
variant="ghost"
size="icon-sm"
onClick={handleRemoveChatBox}
>
<Trash2 />
</Button>
) : (
<Button variant={'ghost'} onClick={addChatBox}>
<Plus></Plus>
<Button variant="ghost" size="icon-sm" onClick={addChatBox}>
<Plus />
</Button>
)}
</div>

View File

@@ -73,67 +73,71 @@ export function SingleChatBox({
}, [conversationId, setDerivedMessages]);
return (
<section className="flex flex-col p-5 h-full">
<div ref={messageContainerRef} className="flex-1 overflow-auto min-h-0">
<section className="flex flex-col h-full gap-4">
<div
ref={messageContainerRef}
className="p-5 flex-1 overflow-auto min-h-0 scrollbar-auto"
>
<div className="w-full pr-5">
{derivedMessages?.map((message, i) => {
return (
<MessageItem
loading={
message.role === MessageType.Assistant &&
sendLoading &&
derivedMessages.length - 1 === i
}
key={buildMessageUuidWithRole(message)}
item={message}
nickname={userInfo.nickname}
avatar={userInfo.avatar}
avatarDialog={currentDialog.icon}
reference={buildMessageItemReference(
{
message: derivedMessages,
reference: conversation.reference,
},
message,
)}
clickDocumentButton={clickDocumentButton}
index={i}
removeMessageById={removeMessageById}
regenerateMessage={regenerateMessage}
sendLoading={sendLoading}
></MessageItem>
);
})}
{derivedMessages?.map((message, i) => (
<MessageItem
loading={
message.role === MessageType.Assistant &&
sendLoading &&
derivedMessages.length - 1 === i
}
key={buildMessageUuidWithRole(message)}
item={message}
nickname={userInfo.nickname}
avatar={userInfo.avatar}
avatarDialog={currentDialog.icon}
reference={buildMessageItemReference(
{
message: derivedMessages,
reference: conversation.reference,
},
message,
)}
clickDocumentButton={clickDocumentButton}
index={i}
removeMessageById={removeMessageById}
regenerateMessage={regenerateMessage}
sendLoading={sendLoading}
/>
))}
</div>
<div ref={scrollRef} />
</div>
<NextMessageInput
disabled={disabled}
sendDisabled={sendDisabled}
sendLoading={sendLoading}
value={value}
resize="vertical"
onInputChange={handleInputChange}
onPressEnter={handlePressEnter}
conversationId={conversationId}
createConversationBeforeUploadDocument={
createConversationBeforeUploadDocument
}
stopOutputMessage={stopOutputMessage}
onUpload={handleUploadFile}
isUploading={isUploading}
removeFile={removeFile}
showReasoning
showInternet={showInternet}
/>
{visible && (
<PdfSheet
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
></PdfSheet>
)}
<div className="p-5 pt-0">
<NextMessageInput
disabled={disabled}
sendDisabled={sendDisabled}
sendLoading={sendLoading}
value={value}
resize="vertical"
onInputChange={handleInputChange}
onPressEnter={handlePressEnter}
conversationId={conversationId}
createConversationBeforeUploadDocument={
createConversationBeforeUploadDocument
}
stopOutputMessage={stopOutputMessage}
onUpload={handleUploadFile}
isUploading={isUploading}
removeFile={removeFile}
showReasoning
showInternet={showInternet}
/>
{visible && (
<PdfSheet
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
></PdfSheet>
)}
</div>
</section>
);
}

View File

@@ -12,7 +12,6 @@ import {
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { SharedFrom } from '@/constants/chat';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import {
useFetchConversationList,
useFetchConversationManually,
@@ -21,9 +20,14 @@ import {
} from '@/hooks/use-chat-request';
import { IClientConversation } from '@/interfaces/database/chat';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { useMount } from 'ahooks';
import { isEmpty } from 'lodash';
import { ArrowUpRight, LogOut, Send } from 'lucide-react';
import {
LucideArrowBigLeft,
LucideArrowUpRight,
LucideSend,
} from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
@@ -37,7 +41,6 @@ import { useSwitchDebugMode } from './use-switch-debug-mode';
export default function Chat() {
const { id } = useParams();
const { navigateToChatList } = useNavigatePage();
const { data } = useFetchDialog();
const { t } = useTranslation();
const [currentConversation, setCurrentConversation] =
@@ -89,15 +92,20 @@ export default function Chat() {
if (isDebugMode) {
return (
<section className="pt-14 h-[100vh] pb-24">
<div className="flex items-center justify-between px-10 pb-5">
<section className="pt-5 pb-16 h-[100vh] flex flex-col">
<header className="px-10 pb-5">
<div className="mb-5">
<Button variant="outline" onClick={switchDebugMode}>
<LucideArrowBigLeft />
<span>{t('common.back')}</span>
</Button>
</div>
<span className="text-2xl">
{t('chat.multipleModels')} ({chatBoxIds.length}/3)
</span>
<Button variant={'ghost'} onClick={switchDebugMode}>
{t('chat.exit')} <LogOut />
</Button>
</div>
</header>
<MultipleChatBox
chatBoxIds={chatBoxIds}
controller={controller}
@@ -116,7 +124,11 @@ export default function Chat() {
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink onClick={navigateToChatList}>
<BreadcrumbLink
// Not friendly for keyboard navigation
// onClick={navigateToChatList}
href={Routes.Chats}
>
{t('chat.chat')}
</BreadcrumbLink>
</BreadcrumbItem>
@@ -127,23 +139,26 @@ export default function Chat() {
</BreadcrumbList>
</Breadcrumb>
<Button onClick={showEmbedModal}>
<Send />
<LucideSend />
{t('common.embedIntoSite')}
</Button>
</PageHeader>
<div className="flex flex-1 min-h-0 pb-9">
<article className="flex flex-1 min-h-0 pb-9">
<Sessions handleConversationCardClick={handleSessionClick}></Sessions>
<Card className="flex-1 min-w-0 bg-transparent border-none shadow-none h-full">
<CardContent className="flex p-0 h-full">
<Card className="flex flex-col flex-1 bg-transparent min-w-0">
<CardHeader
className={cn('p-5', { 'border-b': hasSingleChatBox })}
className={cn('p-5', {
'border-b-0.5 border-border-button': hasSingleChatBox,
})}
>
<CardTitle className="flex justify-between items-center text-base">
<CardTitle className="flex justify-between items-center text-base gap-2">
<div className="truncate">{currentConversationName}</div>
<Button variant={'ghost'} onClick={switchDebugMode}>
<ArrowUpRight /> {t('chat.multipleModels')}
<LucideArrowUpRight /> {t('chat.multipleModels')}
</Button>
</CardTitle>
</CardHeader>
@@ -152,13 +167,15 @@ export default function Chat() {
controller={controller}
stopOutputMessage={stopOutputMessage}
conversation={currentConversation}
></SingleChatBox>
/>
</CardContent>
</Card>
<ChatSettings hasSingleChatBox={hasSingleChatBox}></ChatSettings>
</CardContent>
</Card>
</div>
</article>
{embedVisible && (
<EmbedDialog
visible={embedVisible}

View File

@@ -2,7 +2,6 @@ import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import { MoreButton } from '@/components/more-button';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { SearchInput } from '@/components/ui/input';
import { useSetModalState } from '@/hooks/common-hooks';
@@ -11,14 +10,13 @@ import {
useGetChatSearchParams,
useRemoveConversation,
} from '@/hooks/use-chat-request';
import { cn } from '@/lib/utils';
import {
Check,
CopyX,
PanelLeftClose,
Plus,
Trash2,
Undo2,
LucideCopyX,
LucideListChecks,
LucidePanelLeftClose,
LucidePlus,
LucideTrash2,
LucideUndo2,
} from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -91,72 +89,80 @@ export function Sessions({ handleConversationCardClick }: SessionProps) {
}, [selectedIds, removeConversation, exitSelectionMode]);
const selectedCount = useMemo(() => selectedIds.size, [selectedIds]);
const allSelected = useMemo(
() =>
selectedCount === conversationList.length && conversationList.length > 0,
[selectedCount, conversationList.length],
);
const handleCardClick = useCallback(
(conversationId: string, isNew: boolean) => () => {
if (selectionMode) {
toggleSelection(conversationId);
} else {
handleConversationCardClick(conversationId, isNew);
}
},
[handleConversationCardClick, selectionMode, toggleSelection],
);
const { conversationId } = useGetChatSearchParams();
if (!visible) {
return (
<div className="p-5">
<RAGFlowAvatar
avatar={data.icon}
name={data.name}
className="size-8 cursor-pointer"
<Button
variant="transparent"
size="icon-sm"
className="border-0"
onClick={switchVisible}
></RAGFlowAvatar>
>
<RAGFlowAvatar
avatar={data.icon}
name={data.name}
className="size-8 cursor-pointer"
/>
</Button>
</div>
);
}
return (
<section className="p-5 w-[296px] flex flex-col">
<section className="flex items-center text-base justify-between gap-2">
<aside className="p-5 w-[296px] flex flex-col" role="complementary">
<header className="flex items-center text-base justify-between gap-2">
<div className="flex gap-3 items-center min-w-0">
<RAGFlowAvatar
avatar={data.icon}
name={data.name}
className="size-8"
></RAGFlowAvatar>
/>
<span className="flex-1 truncate">{data.name}</span>
</div>
<PanelLeftClose
className="cursor-pointer size-4"
<Button
variant="transparent"
size="icon-sm"
className="border-0"
onClick={switchVisible}
/>
</section>
>
<LucidePanelLeftClose />
</Button>
</header>
<div className="flex justify-between items-center mb-4 pt-10">
<div className="flex items-center gap-3">
<span className="text-base font-bold">{t('chat.conversations')}</span>
<span className="text-text-secondary text-xs">
<data
className="text-text-secondary text-xs"
value={conversationList.length}
>
{conversationList.length}
</span>
</data>
</div>
{selectionMode && selectedCount > 0 ? (
// Selection mode with items selected: show return and delete
<div className="flex items-center gap-1">
<div className="flex items-center gap-2">
{selectionMode ? (
// Exit selection mode
<Button variant="ghost" size="icon-xs" onClick={exitSelectionMode}>
<LucideUndo2 size={16} />
</Button>
) : (
// New conversation
<Button
variant="ghost"
size="icon"
className="size-6"
onClick={exitSelectionMode}
size="icon-xs"
onClick={addTemporaryConversation}
>
<Undo2 size={16} />
<LucidePlus className="h-4 w-4" />
</Button>
)}
{selectionMode && selectedCount > 0 ? (
// Delete selected items
<ConfirmDeleteDialog
onOk={handleBatchDelete}
title={t('chat.batchDeleteSessions')}
@@ -166,89 +172,82 @@ export function Sessions({ handleConversationCardClick }: SessionProps) {
}),
}}
>
<Button
variant="ghost"
size="icon"
className="size-6 text-state-error"
>
<Trash2 className="h-4 w-4" />
<Button variant="delete" size="icon-xs">
<LucideTrash2 />
</Button>
</ConfirmDeleteDialog>
</div>
) : (
// Default or selection mode without selection: show plus and batch delete
<div className="flex items-center gap-1">
) : (
<Button
variant="ghost"
size="icon"
className="size-6"
onClick={addTemporaryConversation}
>
<Plus className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="size-6"
size="icon-xs"
onClick={selectionMode ? toggleSelectAll : toggleSelectionMode}
>
{selectionMode && allSelected ? (
<Check className="h-4 w-4" />
) : (
<CopyX className="h-4 w-4" />
// <img
// src="/batch_delete2.png"
// alt="批量删除"
// className="h-4 w-4"
// />
)}
{selectionMode ? <LucideListChecks /> : <LucideCopyX />}
</Button>
</div>
)}
)}
</div>
</div>
<div className="pb-4">
<div className="pb-4" role="search">
<SearchInput
onChange={handleInputChange}
value={searchString}
></SearchInput>
</div>
<div className="space-y-4 flex-1 overflow-auto">
{conversationList.map((x) => (
<Card
key={x.id}
onClick={handleCardClick(x.id, x.is_new)}
className={cn('cursor-pointer bg-transparent relative', {
'bg-bg-card': conversationId === x.id && !selectionMode,
})}
>
<CardContent className="px-3 py-2 flex justify-between items-center group gap-1">
<div className="flex items-center gap-2 flex-1 min-w-0">
{selectionMode && (
<span
className="flex-shrink-0"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>
<Checkbox
checked={selectedIds.has(x.id)}
onCheckedChange={() => toggleSelection(x.id)}
/>
</span>
)}
<div className="truncate">{x.name}</div>
</div>
{!selectionMode && (
<ConversationDropdown
conversation={x}
removeTemporaryConversation={removeTemporaryConversation}
<div className="flex-1 overflow-auto">
{selectionMode ? (
<ul className="space-y-2" role="listbox" aria-multiselectable>
{conversationList.map((x) => (
<li
key={x.id}
className="py-2"
role="option"
aria-selected={selectedIds.has(x.id)}
>
<label className="flex items-center gap-2">
<Checkbox
checked={selectedIds.has(x.id)}
onCheckedChange={() => toggleSelection(x.id)}
/>
<span className="truncate">{x.name}</span>
</label>
</li>
))}
</ul>
) : (
<nav aria-label={t('chat.conversations')}>
<ul className="space-y-2">
{conversationList.map((x) => (
<li
key={x.id}
className="
group pr-3 flex items-center gap-1 rounded-lg
aria-selected:bg-bg-card has-[>button:focus-visible]:bg-bg-card
"
aria-selected={conversationId === x.id}
>
<MoreButton></MoreButton>
</ConversationDropdown>
)}
</CardContent>
</Card>
))}
<button
type="button"
className="focus-visible:outline-none px-3 py-2 text-left flex-1 truncate"
onClick={() => handleConversationCardClick(x.id, x.is_new)}
>
{x.name}
</button>
<ConversationDropdown
conversation={x}
removeTemporaryConversation={removeTemporaryConversation}
>
<MoreButton></MoreButton>
</ConversationDropdown>
</li>
))}
</ul>
</nav>
)}
</div>
</section>
</aside>
);
}

View File

@@ -172,14 +172,26 @@ export const useSendMessage = (controller: AbortController) => {
id,
content: value.trim(),
role: MessageType.User,
files: files,
files,
conversationId: targetConversationId,
},
enableInternet,
enableThinking,
});
}
clearFiles();
// Auto scroll to bottom when sending new message
if (messageContainerRef.current) {
const el = messageContainerRef.current;
requestAnimationFrame(() => {
el.scrollTo({
top: el.scrollHeight,
});
});
}
},
[
value,
@@ -190,6 +202,7 @@ export const useSendMessage = (controller: AbortController) => {
clearFiles,
setValue,
sendMessage,
messageContainerRef,
],
);