This commit is contained in:
Cledwyn Lew
2026-07-22 11:25:10 +08:00
parent 362f82fc3e
commit b63cd1d916
13 changed files with 325 additions and 126 deletions

View File

@@ -18,16 +18,18 @@
"dependencies": { "dependencies": {
"@heroui/react": "2.8.3", "@heroui/react": "2.8.3",
"@heroui/theme": "2.4.21", "@heroui/theme": "2.4.21",
"@mujian/js-sdk": "0.0.6-beta.69", "@mujian/js-sdk": "0.0.6-beta.88",
"@tailwindcss/vite": "4.1.12", "@tailwindcss/vite": "4.1.12",
"ahooks": "3.9.5", "ahooks": "3.9.5",
"axios": "1.11.0", "axios": "1.11.0",
"dayjs": "1.11.18", "dayjs": "1.11.18",
"i18next": "^25.8.4",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"lucide-react": "^0.546.0", "lucide-react": "^0.546.0",
"motion": "12.23.12", "motion": "12.23.12",
"react": "19.1.1", "react": "19.1.1",
"react-dom": "19.1.1", "react-dom": "19.1.1",
"react-i18next": "^16.5.4",
"react-icons": "5.5.0", "react-icons": "5.5.0",
"react-markdown": "10.1.0", "react-markdown": "10.1.0",
"react-router": "7.8.2", "react-router": "7.8.2",

114
pnpm-lock.yaml generated
View File

@@ -15,8 +15,8 @@ importers:
specifier: 2.4.21 specifier: 2.4.21
version: 2.4.21(tailwindcss@4.1.12) version: 2.4.21(tailwindcss@4.1.12)
'@mujian/js-sdk': '@mujian/js-sdk':
specifier: 0.0.6-beta.69 specifier: 0.0.6-beta.88
version: 0.0.6-beta.69(react-dom@19.1.1(react@19.1.1))(react@19.1.1) version: 0.0.6-beta.88(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: 4.1.12 specifier: 4.1.12
version: 4.1.12(vite@7.1.2(@types/node@22.13.9)(jiti@2.5.1)(lightningcss@1.30.1)) version: 4.1.12(vite@7.1.2(@types/node@22.13.9)(jiti@2.5.1)(lightningcss@1.30.1))
@@ -29,6 +29,9 @@ importers:
dayjs: dayjs:
specifier: 1.11.18 specifier: 1.11.18
version: 1.11.18 version: 1.11.18
i18next:
specifier: ^25.8.4
version: 25.8.4(typescript@5.8.3)
lodash-es: lodash-es:
specifier: 4.17.21 specifier: 4.17.21
version: 4.17.21 version: 4.17.21
@@ -44,6 +47,9 @@ importers:
react-dom: react-dom:
specifier: 19.1.1 specifier: 19.1.1
version: 19.1.1(react@19.1.1) version: 19.1.1(react@19.1.1)
react-i18next:
specifier: ^16.5.4
version: 16.5.4(i18next@25.8.4(typescript@5.8.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.8.3)
react-icons: react-icons:
specifier: 5.5.0 specifier: 5.5.0
version: 5.5.0(react@19.1.1) version: 5.5.0(react@19.1.1)
@@ -61,7 +67,7 @@ importers:
version: 1.0.7(tailwindcss@4.1.12) version: 1.0.7(tailwindcss@4.1.12)
zustand: zustand:
specifier: 5.0.8 specifier: 5.0.8
version: 5.0.8(@types/react@19.1.10)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) version: 5.0.8(@types/react@19.1.10)(immer@11.1.8)(react@19.1.1)(use-sync-external-store@1.6.0(react@19.1.1))
devDependencies: devDependencies:
'@biomejs/biome': '@biomejs/biome':
specifier: ^2.3.10 specifier: ^2.3.10
@@ -175,6 +181,10 @@ packages:
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/runtime@7.28.6':
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2': '@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -996,8 +1006,8 @@ packages:
'@jridgewell/trace-mapping@0.3.30': '@jridgewell/trace-mapping@0.3.30':
resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
'@mujian/js-sdk@0.0.6-beta.69': '@mujian/js-sdk@0.0.6-beta.88':
resolution: {integrity: sha512-hdiKHrbq+AngnV9e7woBTqnPhROJaejDgkFhh4IYpgAloIrarWuNbH1ZUvZU2G6Gqn1o4MwkymrXSmDXe9rMQA==} resolution: {integrity: sha512-i7LqTQd+wyz4RGMZrLjnDrK7wHUzJz7idPXM87IiJtBLin+hZD7z5tLol3mFlLg5QTGwARBSjflpLHaPxD6jKA==}
peerDependencies: peerDependencies:
react: ~19.1.1 react: ~19.1.1
react-dom: ~19.1.1 react-dom: ~19.1.1
@@ -1877,8 +1887,8 @@ packages:
devlop@1.1.0: devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
dompurify@3.3.1: dompurify@3.4.2:
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} resolution: {integrity: sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==}
dunder-proto@1.0.1: dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
@@ -2011,9 +2021,23 @@ packages:
hast-util-whitespace@3.0.0: hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
html-url-attributes@3.0.1: html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
i18next@25.8.4:
resolution: {integrity: sha512-a9A0MnUjKvzjEN/26ZY1okpra9kA8MEwzYEz1BNm+IyxUKPRH6ihf0p7vj8YvULwZHKHl3zkJ6KOt4hewxBecQ==}
peerDependencies:
typescript: ^5
peerDependenciesMeta:
typescript:
optional: true
immer@11.1.8:
resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==}
inline-style-parser@0.2.4: inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
@@ -2327,6 +2351,22 @@ packages:
react-fast-compare@3.2.2: react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
react-i18next@16.5.4:
resolution: {integrity: sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==}
peerDependencies:
i18next: '>= 25.6.2'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
typescript: ^5
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
typescript:
optional: true
react-icons@5.5.0: react-icons@5.5.0:
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
peerDependencies: peerDependencies:
@@ -2522,8 +2562,13 @@ packages:
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
uuid@13.0.0: use-sync-external-store@1.6.0:
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
uuid@13.0.2:
resolution: {integrity: sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==}
hasBin: true hasBin: true
vfile-message@4.0.3: vfile-message@4.0.3:
@@ -2592,6 +2637,10 @@ packages:
yaml: yaml:
optional: true optional: true
void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
yallist@3.1.1: yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@@ -2720,6 +2769,8 @@ snapshots:
'@babel/runtime@7.28.3': {} '@babel/runtime@7.28.3': {}
'@babel/runtime@7.28.6': {}
'@babel/template@7.27.2': '@babel/template@7.27.2':
dependencies: dependencies:
'@babel/code-frame': 7.27.1 '@babel/code-frame': 7.27.1
@@ -3942,17 +3993,18 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@mujian/js-sdk@0.0.6-beta.69(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': '@mujian/js-sdk@0.0.6-beta.88(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies: dependencies:
'@adobe/css-tools': 4.4.4 '@adobe/css-tools': 4.4.4
ahooks: 3.9.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) ahooks: 3.9.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
dompurify: 3.3.1 dompurify: 3.4.2
immer: 11.1.8
lodash-es: 4.17.21 lodash-es: 4.17.21
postmate: 1.5.2 postmate: 1.5.2
react: 19.1.1 react: 19.1.1
react-dom: 19.1.1(react@19.1.1) react-dom: 19.1.1(react@19.1.1)
showdown: 2.1.0 showdown: 2.1.0
uuid: 13.0.0 uuid: 13.0.2
virtua: 0.45.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) virtua: 0.45.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
transitivePeerDependencies: transitivePeerDependencies:
- solid-js - solid-js
@@ -5124,7 +5176,7 @@ snapshots:
dependencies: dependencies:
dequal: 2.0.3 dequal: 2.0.3
dompurify@3.3.1: dompurify@3.4.2:
optionalDependencies: optionalDependencies:
'@types/trusted-types': 2.0.7 '@types/trusted-types': 2.0.7
@@ -5281,8 +5333,20 @@ snapshots:
dependencies: dependencies:
'@types/hast': 3.0.4 '@types/hast': 3.0.4
html-parse-stringify@3.0.1:
dependencies:
void-elements: 3.1.0
html-url-attributes@3.0.1: {} html-url-attributes@3.0.1: {}
i18next@25.8.4(typescript@5.8.3):
dependencies:
'@babel/runtime': 7.28.6
optionalDependencies:
typescript: 5.8.3
immer@11.1.8: {}
inline-style-parser@0.2.4: {} inline-style-parser@0.2.4: {}
input-otp@1.4.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1): input-otp@1.4.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
@@ -5678,6 +5742,17 @@ snapshots:
react-fast-compare@3.2.2: {} react-fast-compare@3.2.2: {}
react-i18next@16.5.4(i18next@25.8.4(typescript@5.8.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.8.3):
dependencies:
'@babel/runtime': 7.28.6
html-parse-stringify: 3.0.1
i18next: 25.8.4(typescript@5.8.3)
react: 19.1.1
use-sync-external-store: 1.6.0(react@19.1.1)
optionalDependencies:
react-dom: 19.1.1(react@19.1.1)
typescript: 5.8.3
react-icons@5.5.0(react@19.1.1): react-icons@5.5.0(react@19.1.1):
dependencies: dependencies:
react: 19.1.1 react: 19.1.1
@@ -5905,7 +5980,11 @@ snapshots:
dependencies: dependencies:
react: 19.1.1 react: 19.1.1
uuid@13.0.0: {} use-sync-external-store@1.6.0(react@19.1.1):
dependencies:
react: 19.1.1
uuid@13.0.2: {}
vfile-message@4.0.3: vfile-message@4.0.3:
dependencies: dependencies:
@@ -5936,14 +6015,17 @@ snapshots:
jiti: 2.5.1 jiti: 2.5.1
lightningcss: 1.30.1 lightningcss: 1.30.1
void-elements@3.1.0: {}
yallist@3.1.1: {} yallist@3.1.1: {}
yallist@5.0.0: {} yallist@5.0.0: {}
zustand@5.0.8(@types/react@19.1.10)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): zustand@5.0.8(@types/react@19.1.10)(immer@11.1.8)(react@19.1.1)(use-sync-external-store@1.6.0(react@19.1.1)):
optionalDependencies: optionalDependencies:
'@types/react': 19.1.10 '@types/react': 19.1.10
immer: 11.1.8
react: 19.1.1 react: 19.1.1
use-sync-external-store: 1.5.0(react@19.1.1) use-sync-external-store: 1.6.0(react@19.1.1)
zwitch@2.0.4: {} zwitch@2.0.4: {}

35
src/i18n.ts Normal file
View File

@@ -0,0 +1,35 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import enUs from './locales/en-us.json';
import zhCn from './locales/zh-cn.json';
const resources = {
'en-US': { translation: enUs },
'zh-CN': { translation: zhCn },
};
const defaultLng = (() => {
const paramsLang = new URLSearchParams(window.location.search).get('lang');
const browserLang = navigator.language.toLowerCase();
if (paramsLang) {
const langSection = paramsLang.toLowerCase().replace('_', '-').split('-');
const normalized = langSection[0] + '-' + langSection[1].toUpperCase();
return normalized;
}
if (browserLang.startsWith('zh')) return 'zh-CN';
return 'en-US';
})();
i18n.use(initReactI18next).init({
resources,
lng: defaultLng,
fallbackLng: 'zh-CN',
nonExplicitSupportedLngs: true,
load: 'currentOnly',
interpolation: {
escapeValue: false,
},
});
export default i18n;

18
src/locales/en-us.json Normal file
View File

@@ -0,0 +1,18 @@
{
"edit": "Edit",
"copy": "Copy",
"refresh": "Refresh",
"continue": "Continue",
"delete": "Delete",
"save": "Save",
"cancel": "Cancel",
"copySuccess": "Copy Success",
"copyFailed": "Copy Failed, Please Try Again.",
"generating": "Generating...",
"sendWithEnter": "Send with Enter",
"newMessageExists": "New Message Exists, Cannot Switch Opening Message",
"assertionFailed": "Assertion Failed: No Opening Message, Cannot Switch Opening Message",
"unknownError": "Oops, Something Went Wrong, Please Refresh the Page and Try Again~",
"saveFailed": "Last Save Message Failed, Please Refresh the Page and Try Again",
"error": "Error, Please Refresh the Page and Try Again"
}

18
src/locales/zh-cn.json Normal file
View File

@@ -0,0 +1,18 @@
{
"edit": "编辑",
"copy": "复制",
"refresh": "重说",
"continue": "继续",
"delete": "删除",
"save": "保存",
"cancel": "取消",
"copySuccess": "复制成功",
"copyFailed": "复制失败,请重试。",
"generating": "正在生成...",
"sendWithEnter": "按 Enter 发送",
"newMessageExists": "已有新消息,不允许切换开场白",
"assertionFailed": "断言失败:没有开场消息,无法切换开场白",
"unknownError": "哎呀,发生了未知错误,刷新页面再试试吧~",
"saveFailed": "最近一次保存消息失败,请刷新页面后重试",
"error": "出错啦,请刷新页面后重试"
}

View File

@@ -4,6 +4,7 @@ import '@/styles/global.css';
import { MujianProvider } from '@mujian/js-sdk/react'; import { MujianProvider } from '@mujian/js-sdk/react';
import { ReactRouterProvider } from './providers/RouterProvider.tsx'; import { ReactRouterProvider } from './providers/RouterProvider.tsx';
import '@mujian/js-sdk/react.css'; import '@mujian/js-sdk/react.css';
import './i18n';
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<MujianProvider> <MujianProvider>

View File

@@ -1,4 +1,5 @@
import { Button, ButtonGroup, cn } from '@heroui/react'; import { Button, ButtonGroup, cn } from '@heroui/react';
import { useTranslation } from 'react-i18next';
import { mjChatCls } from '@/utils/cls'; import { mjChatCls } from '@/utils/cls';
export type EditActionsProps = { export type EditActionsProps = {
@@ -14,6 +15,7 @@ export const EditActions = ({
onSaveEdit, onSaveEdit,
onCancelEdit, onCancelEdit,
}: EditActionsProps) => { }: EditActionsProps) => {
const { t } = useTranslation();
if (!isEditing) return null; if (!isEditing) return null;
return ( return (
@@ -43,7 +45,7 @@ export const EditActions = ({
size="sm" size="sm"
onPress={onSaveEdit} onPress={onSaveEdit}
> >
保存 {t('save')}
</Button> </Button>
<Button <Button
className={mjChatCls( className={mjChatCls(
@@ -53,7 +55,7 @@ export const EditActions = ({
size="sm" size="sm"
onPress={onCancelEdit} onPress={onCancelEdit}
> >
取消 {t('cancel')}
</Button> </Button>
</ButtonGroup> </ButtonGroup>
</div> </div>

View File

@@ -33,7 +33,7 @@ export type LastMessageActionsProps = {
}; };
export const MessageActions = ({ export const MessageActions = ({
// message, message,
isUser, isUser,
isLastMsg, isLastMsg,
isFirstMsg, isFirstMsg,
@@ -64,7 +64,7 @@ export const MessageActions = ({
<div <div
className={mjChatCls( className={mjChatCls(
['msg-actions-wrapper'], ['msg-actions-wrapper'],
cn('flex justify-between row-start-2 col-start-1 col-end-4'), cn('flex justify-between row-start-2 col-start-1 col-end-4 flex-wrap'),
)} )}
role="presentation" role="presentation"
onClick={onMessageTextClick} /* Stop propagation */ onClick={onMessageTextClick} /* Stop propagation */
@@ -76,6 +76,7 @@ export const MessageActions = ({
)} )}
> >
{!isFirstMsg && ( {!isFirstMsg && (
<div className="flex gap-2">
<Dropdown <Dropdown
placement="top-start" placement="top-start"
className={mjChatCls(['msg-actions-dropdown'], 'dark')} className={mjChatCls(['msg-actions-dropdown'], 'dark')}
@@ -98,12 +99,10 @@ export const MessageActions = ({
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu <DropdownMenu
items={dropdownItems} items={dropdownItems}
className={mjChatCls(['msg-actions-dropdown-menu'], 'text-white')} className={mjChatCls(
// disabledKeys={ ['msg-actions-dropdown-menu'],
// message.id.startsWith('not_saved') 'text-white',
// ? ['edit', 'regenerate', 'continue'] )}
// : []
// }
> >
{(item) => ( {(item) => (
<DropdownItem <DropdownItem
@@ -147,6 +146,25 @@ export const MessageActions = ({
)} )}
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
{message.flags.emptyResponse &&
message.flags.saved &&
!message.flags.contentFilter &&
isLastMsg && (
<Button
variant="light"
size="sm"
className={mjChatCls(
['msg-actions-dropdown-button'],
'bg-white/15 text-default min-w-10',
)}
onPress={() => {
onRegenerate?.();
}}
>
重试
</Button>
)}
</div>
)} )}
</div> </div>
<div className={mjChatCls(['swipe-action-container'], 'flex items-end')}> <div className={mjChatCls(['swipe-action-container'], 'flex items-end')}>
@@ -199,6 +217,26 @@ export const MessageActions = ({
</ButtonGroup> </ButtonGroup>
)} )}
</div> </div>
{message.flags.contentFilter && isLastMsg && (
<div className="w-full flex items-center">
<div className="opacity-30 min-h-4 ml-0 text-white text-sm">
检测到截断,是否需要重新生成?
</div>
<Button
variant="light"
size="sm"
className={mjChatCls(
['msg-actions-dropdown-button'],
'bg-white/15 text-default min-w-8 h-6 px-0',
)}
onPress={() => {
onRegenerate?.();
}}
>
是
</Button>
</div>
)}
</div> </div>
); );
}; };

View File

@@ -1,4 +1,5 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { PanicContext } from '../../PanicContext'; import { PanicContext } from '../../PanicContext';
export type DropdownItem = { export type DropdownItem = {
@@ -34,30 +35,30 @@ export const useMessageActions = ({
onDelete, onDelete,
}: UseDropdownItemsProps): DropdownItem[] => { }: UseDropdownItemsProps): DropdownItem[] => {
const { panicked } = useContext(PanicContext); const { panicked } = useContext(PanicContext);
const { t } = useTranslation();
const dropdownItems: DropdownItem[] = [ const dropdownItems: DropdownItem[] = [
{ {
key: 'edit', key: 'edit',
label: '编辑', label: t('edit'),
onPress: onEditButtonClick, onPress: onEditButtonClick,
disabled: panicked, disabled: panicked,
}, },
{ {
key: 'copy', key: 'copy',
label: '复制', label: t('copy'),
onPress: onCopyClick, onPress: onCopyClick,
}, },
...(isLastMsg ...(isLastMsg
? [ ? [
{ {
key: 'regenerate', key: 'regenerate',
label: '重说', label: t('refresh'),
onPress: async () => await onRegenerate?.(), onPress: async () => await onRegenerate?.(),
disabled: panicked, disabled: panicked,
}, },
{ {
key: 'continue', key: 'continue',
label: '继续', label: t('continue'),
onPress: async () => await onContinue?.(), onPress: async () => await onContinue?.(),
disabled: panicked, disabled: panicked,
}, },
@@ -65,7 +66,7 @@ export const useMessageActions = ({
: []), : []),
{ {
key: 'delete', key: 'delete',
label: '删除', label: t('delete'),
onPress: onDelete, onPress: onDelete,
color: 'danger', color: 'danger',
className: 'text-danger', className: 'text-danger',

View File

@@ -1,6 +1,7 @@
import { addToast, cn } from '@heroui/react'; import { addToast, cn } from '@heroui/react';
import { type Message as BaseMessage, useMujian } from '@mujian/js-sdk/react'; import { type Message as BaseMessage, useMujian } from '@mujian/js-sdk/react';
import React, { type MouseEventHandler } from 'react'; import React, { type MouseEventHandler } from 'react';
import { useTranslation } from 'react-i18next';
import { mjChatCls } from '@/utils/cls'; import { mjChatCls } from '@/utils/cls';
import { EditActions } from './components/EditActions'; import { EditActions } from './components/EditActions';
import { MessageActions } from './components/MessageActions'; import { MessageActions } from './components/MessageActions';
@@ -83,10 +84,11 @@ export const MessageItem = React.memo((props: MessageItemProps) => {
swipes = [], swipes = [],
activeSwipeId, activeSwipeId,
mjv, // mjv,
// sendAt, // sendAt,
} = message; } = message;
const { t } = useTranslation();
const isUser = role === 'user'; const isUser = role === 'user';
const messageRef = React.useRef<HTMLDivElement>(null); // 使用 useRef 获取当前组件的引用 const messageRef = React.useRef<HTMLDivElement>(null); // 使用 useRef 获取当前组件的引用
@@ -123,13 +125,13 @@ export const MessageItem = React.memo((props: MessageItemProps) => {
await mujian.utils.clipboard.writeText(originalMessage.content); await mujian.utils.clipboard.writeText(originalMessage.content);
addToast({ addToast({
color: 'success', color: 'success',
description: '复制成功', description: t('copySuccess'),
timeout: 2000, timeout: 2000,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
addToast({ addToast({
description: '复制失败,请重试。', description: t('copyFailed'),
}); });
} }
}; };
@@ -185,7 +187,7 @@ export const MessageItem = React.memo((props: MessageItemProps) => {
// 按ctrl点击时打印ID // 按ctrl点击时打印ID
if (event.ctrlKey) { if (event.ctrlKey) {
event.preventDefault(); event.preventDefault();
console.log('Official Chat Message ID:', id, mjv); console.log('Official Chat Message ID:', id, message);
} }
}} }}
> >

View File

@@ -60,7 +60,7 @@ export const MessageList = React.memo(
prevDataLength.current === data.length && prevDataLength.current === data.length &&
prevLastMsgId.current !== lastMessage?.id; prevLastMsgId.current !== lastMessage?.id;
if (shouldRecoverScroll) { if (shouldRecoverScroll) {
vl.scrollTo(vl.scrollOffset); vl.scrollTo(vl.scrollOffset + 9999);
} }
prevDataLength.current = data.length; prevDataLength.current = data.length;

View File

@@ -1,7 +1,8 @@
import { Button, Textarea } from '@heroui/react'; import { Button, Textarea } from '@heroui/react';
import { CircleStopIcon, SendIcon } from 'lucide-react'; import { CircleStopIcon, SendIcon } from 'lucide-react';
import { mjChatCls } from '@/utils/cls';
import { useContext } from 'react'; import { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { mjChatCls } from '@/utils/cls';
import { PanicContext } from './PanicContext'; import { PanicContext } from './PanicContext';
interface Props { interface Props {
@@ -31,7 +32,7 @@ export const MsgSend = ({
onChange, onChange,
}: Props) => { }: Props) => {
const { panicked } = useContext(PanicContext); const { panicked } = useContext(PanicContext);
const { t } = useTranslation();
const isEmptyInput = value.trim().length === 0; const isEmptyInput = value.trim().length === 0;
const handleSend = () => { const handleSend = () => {
@@ -114,7 +115,7 @@ export const MsgSend = ({
maxLength={50000} maxLength={50000}
maxRows={4} maxRows={4}
minRows={1} minRows={1}
placeholder={running ? '正在生成...' : '按 Enter 发送'} placeholder={running ? t('generating') : t('sendWithEnter')}
radius="lg" radius="lg"
size="lg" size="lg"
value={value} value={value}

View File

@@ -1,6 +1,7 @@
import { Alert, ScrollShadow, Spinner, ToastProvider } from '@heroui/react'; import { Alert, ScrollShadow, Spinner, ToastProvider } from '@heroui/react';
import { useChat, useMjEngine, useMujian } from '@mujian/js-sdk/react'; import { useChat, useMjEngine, useMujian } from '@mujian/js-sdk/react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
import { mjChatCls } from '@/utils/cls'; import { mjChatCls } from '@/utils/cls';
import { MessageList } from './MessageList'; import { MessageList } from './MessageList';
@@ -11,7 +12,7 @@ import { QuickReply } from './QuickReply';
export const Chat = () => { export const Chat = () => {
const { init, projectInfo, activePersona } = useGlobalStore(); const { init, projectInfo, activePersona } = useGlobalStore();
const mujian = useMujian(); const mujian = useMujian();
const [inputValue, setInputValue] = useState(''); const { t } = useTranslation();
const { const {
messages, messages,
status, status,
@@ -24,6 +25,8 @@ export const Chat = () => {
editMessage, editMessage,
setSwipe, setSwipe,
loadMoreMessage, loadMoreMessage,
input: inputValue,
setInput: setInputValue,
} = useChat({ } = useChat({
onError: (e) => { onError: (e) => {
console.error(e); console.error(e);
@@ -110,15 +113,6 @@ export const Chat = () => {
); );
} }
const errorMessage =
Boolean(error) &&
(typeof error === 'object' &&
error &&
'message' in error &&
typeof error.message === 'string'
? error.message
: '哎呀,发生了未知错误,刷新页面再试试吧~');
const toastStyle = ` const toastStyle = `
.toast-region { .toast-region {
margin-top: ${paddingTop}px; margin-top: ${paddingTop}px;
@@ -128,9 +122,20 @@ export const Chat = () => {
status !== 'streaming' && status !== 'streaming' &&
messages.some((m) => m.id.startsWith('not_saved')); messages.some((m) => m.id.startsWith('not_saved'));
const errorMessage = [
Boolean(error) &&
(typeof error === 'object' &&
error &&
'message' in error &&
typeof error.message === 'string'
? error.message
: t('unknownError')),
saveFailed ? t('saveFailed') : '',
].filter(Boolean);
return ( return (
<PanicContext.Provider <PanicContext.Provider
value={{ panicked: saveFailed || Boolean(errorMessage) }} value={{ panicked: saveFailed || Boolean(errorMessage.length) }}
> >
<div className={mjChatCls(['container'], 'size-full relative')}> <div className={mjChatCls(['container'], 'size-full relative')}>
<style> {toastStyle} </style> <style> {toastStyle} </style>
@@ -151,7 +156,7 @@ export const Chat = () => {
)} )}
style={{ style={{
backgroundImage: projectInfo backgroundImage: projectInfo
? `url(${projectInfo.coverImageUrl})` ? `url(${projectInfo.backgroundImageUrl || projectInfo.coverImageUrl})`
: undefined, : undefined,
}} }}
/> />
@@ -184,19 +189,7 @@ export const Chat = () => {
onRegenerate={regenerate} onRegenerate={regenerate}
onNeedMore={loadMoreMessage} onNeedMore={loadMoreMessage}
/> />
{saveFailed && ( {errorMessage.length > 0 && (
<Alert
hideIcon
title="最近一次保存消息失败,请刷新页面后重试"
className="opacity-30"
classNames={{
mainWrapper: 'min-h-4 ml-0 text-white',
}}
color="default"
variant="bordered"
/>
)}
{errorMessage && (
<Alert <Alert
hideIcon hideIcon
className={mjChatCls(['error-message'], 'mt-2 mb-6')} className={mjChatCls(['error-message'], 'mt-2 mb-6')}
@@ -208,8 +201,14 @@ export const Chat = () => {
description: 'opacity-80 text-xs', description: 'opacity-80 text-xs',
}} }}
color="danger" color="danger"
title="出错啦,请刷新页面后重试" title={t('error')}
description={errorMessage} description={
<>
{errorMessage.map((m) => (
<div>{m}</div>
))}
</>
}
variant="bordered" variant="bordered"
/> />
)} )}