@alicloud/appflow-chat 0.0.4 → 0.0.5-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/appflow-chat.cjs.js +857 -164
- package/dist/appflow-chat.esm.js +19844 -13557
- package/dist/types/index.d.ts +243 -2
- package/package.json +3 -1
- package/src/components/A2UIRenderer/A2UIRenderer.tsx +181 -0
- package/src/components/A2UIRenderer/index.ts +1 -0
- package/src/components/HumanVerify/CustomParamsRenderer/ArrayField.tsx +6 -4
- package/src/components/HumanVerify/CustomParamsRenderer/EnumField.tsx +5 -3
- package/src/components/HumanVerify/CustomParamsRenderer/FieldRenderer.tsx +6 -4
- package/src/components/HumanVerify/CustomParamsRenderer/FileField.tsx +38 -30
- package/src/components/HumanVerify/CustomParamsRenderer/ObjectField.tsx +4 -2
- package/src/components/HumanVerify/CustomParamsRenderer/TimeField.tsx +3 -1
- package/src/components/HumanVerify/HistoryCard.tsx +6 -4
- package/src/components/HumanVerify/HumanVerify.tsx +5 -3
- package/src/components/MessageBubble.tsx +26 -4
- package/src/components/RichMessageBubble.tsx +4 -2
- package/src/core/RichBubbleContent.tsx +4 -2
- package/src/core/SourceContent.tsx +5 -3
- package/src/core/WebSearchContent.tsx +3 -1
- package/src/i18n/LocaleContext.tsx +70 -0
- package/src/i18n/index.ts +16 -0
- package/src/i18n/translate.ts +42 -0
- package/src/i18n/useTranslation.ts +39 -0
- package/src/i18n/utils.ts +67 -0
- package/src/index.ts +31 -0
- package/src/locales/en-US.ts +77 -0
- package/src/locales/index.ts +7 -0
- package/src/locales/types.ts +35 -0
- package/src/locales/zh-CN.ts +78 -0
- package/src/markdown/components/Chart.tsx +3 -1
- package/src/markdown/components/Mermaid.tsx +8 -6
- package/src/markdown/index.tsx +11 -8
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
UploadFileResponse
|
|
11
11
|
} from './types';
|
|
12
12
|
import styled from 'styled-components';
|
|
13
|
+
import { useTranslation } from '../../../i18n';
|
|
14
|
+
import { translate } from '../../../i18n';
|
|
13
15
|
|
|
14
16
|
// ==================== Styled Components ====================
|
|
15
17
|
|
|
@@ -114,16 +116,17 @@ const getAcceptBySubTypes = (subTypes?: FileSubType[]): string => {
|
|
|
114
116
|
|
|
115
117
|
/**
|
|
116
118
|
* 根据文件子类型数组获取上传提示文本
|
|
119
|
+
* 使用全局 translate(非 hook 版本)以便在组件外调用
|
|
117
120
|
*/
|
|
118
121
|
const getUploadHintBySubTypes = (subTypes?: FileSubType[]): string => {
|
|
119
122
|
if (!subTypes || subTypes.length === 0) {
|
|
120
|
-
return '
|
|
123
|
+
return translate('humanVerify.file.supportAll');
|
|
121
124
|
}
|
|
122
|
-
|
|
125
|
+
|
|
123
126
|
if (subTypes.includes('default')) {
|
|
124
|
-
return '
|
|
127
|
+
return translate('humanVerify.file.supportAll');
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
|
|
127
130
|
const hintMap: Record<FileSubType, string> = {
|
|
128
131
|
'jpg': 'JPG',
|
|
129
132
|
'png': 'PNG',
|
|
@@ -134,11 +137,13 @@ const getUploadHintBySubTypes = (subTypes?: FileSubType[]): string => {
|
|
|
134
137
|
'txt': 'TXT',
|
|
135
138
|
'markdown': 'Markdown',
|
|
136
139
|
'zip': 'ZIP/RAR/7Z',
|
|
137
|
-
'default': '
|
|
140
|
+
'default': translate('humanVerify.file.supportAll'),
|
|
138
141
|
};
|
|
139
|
-
|
|
142
|
+
|
|
140
143
|
const hints = subTypes.map(subType => hintMap[subType] || subType).filter(Boolean);
|
|
141
|
-
return hints.length > 0
|
|
144
|
+
return hints.length > 0
|
|
145
|
+
? translate('humanVerify.file.supportFormats', { formats: hints.join('、') })
|
|
146
|
+
: translate('humanVerify.file.supportAll');
|
|
142
147
|
};
|
|
143
148
|
|
|
144
149
|
/**
|
|
@@ -164,6 +169,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
164
169
|
uploadSender,
|
|
165
170
|
fileUploader,
|
|
166
171
|
}) => {
|
|
172
|
+
const { t } = useTranslation();
|
|
167
173
|
// 优先从 AssociationPropertyMetadata.SubType 读取,兼容旧的 FileSubType 字段
|
|
168
174
|
const subTypes = useMemo((): FileSubType[] => {
|
|
169
175
|
const subTypeArray = schema.AssociationPropertyMetadata?.SubType;
|
|
@@ -190,7 +196,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
190
196
|
if (Array.isArray(value)) {
|
|
191
197
|
return value.map((file: any, index: number) => ({
|
|
192
198
|
uid: file.uid || `${index}`,
|
|
193
|
-
name: file.name ||
|
|
199
|
+
name: file.name || `${t('humanVerify.file.defaultFileName')}${index + 1}`,
|
|
194
200
|
status: 'done' as const,
|
|
195
201
|
url: file.url,
|
|
196
202
|
...file,
|
|
@@ -199,7 +205,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
199
205
|
if (typeof value === 'object') {
|
|
200
206
|
return [{
|
|
201
207
|
uid: value.uid || '0',
|
|
202
|
-
name: value.name || '
|
|
208
|
+
name: value.name || t('humanVerify.file.defaultFileName'),
|
|
203
209
|
status: 'done' as const,
|
|
204
210
|
url: value.url,
|
|
205
211
|
...value,
|
|
@@ -227,7 +233,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
227
233
|
*/
|
|
228
234
|
const getUploadToken = useCallback(async (fileName: string): Promise<UploadTokenResponse | null> => {
|
|
229
235
|
if (!uploadSender) {
|
|
230
|
-
message.error('
|
|
236
|
+
message.error(t('humanVerify.file.uploaderNotConfigured'));
|
|
231
237
|
return null;
|
|
232
238
|
}
|
|
233
239
|
|
|
@@ -238,7 +244,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
238
244
|
});
|
|
239
245
|
|
|
240
246
|
if (!response) {
|
|
241
|
-
message.error('
|
|
247
|
+
message.error(t('humanVerify.file.tokenFailed'));
|
|
242
248
|
return null;
|
|
243
249
|
}
|
|
244
250
|
|
|
@@ -255,14 +261,14 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
255
261
|
return parsedResponse as UploadTokenResponse;
|
|
256
262
|
}
|
|
257
263
|
|
|
258
|
-
message.error('
|
|
264
|
+
message.error(t('humanVerify.file.tokenFailed'));
|
|
259
265
|
return null;
|
|
260
266
|
} catch (error) {
|
|
261
|
-
console.error('
|
|
262
|
-
message.error('
|
|
267
|
+
console.error(t('humanVerify.file.tokenFailed'), error);
|
|
268
|
+
message.error(t('humanVerify.file.tokenFailed'));
|
|
263
269
|
return null;
|
|
264
270
|
}
|
|
265
|
-
}, [uploadSender]);
|
|
271
|
+
}, [uploadSender, t]);
|
|
266
272
|
|
|
267
273
|
/**
|
|
268
274
|
* 获取文件 ID(文件上传专用)
|
|
@@ -298,10 +304,10 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
298
304
|
|
|
299
305
|
return null;
|
|
300
306
|
} catch (error) {
|
|
301
|
-
console.error('
|
|
307
|
+
console.error(t('humanVerify.file.getFileIdFailed'), error);
|
|
302
308
|
return null;
|
|
303
309
|
}
|
|
304
|
-
}, [uploadSender]);
|
|
310
|
+
}, [uploadSender, t]);
|
|
305
311
|
|
|
306
312
|
/**
|
|
307
313
|
* 图片上传处理
|
|
@@ -311,12 +317,12 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
311
317
|
// 1. 获取预签名 URL
|
|
312
318
|
const tokenResponse = await getUploadToken(file.name);
|
|
313
319
|
if (!tokenResponse) {
|
|
314
|
-
throw new Error('
|
|
320
|
+
throw new Error(t('humanVerify.file.tokenFailed'));
|
|
315
321
|
}
|
|
316
322
|
|
|
317
323
|
// 2. 上传文件到 OSS
|
|
318
324
|
if (!fileUploader) {
|
|
319
|
-
throw new Error('
|
|
325
|
+
throw new Error(t('humanVerify.file.uploadMethodNotConfigured'));
|
|
320
326
|
}
|
|
321
327
|
const blob = new Blob([file]);
|
|
322
328
|
await fileUploader(blob, tokenResponse.uploadUrl);
|
|
@@ -329,7 +335,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
329
335
|
type: file.type,
|
|
330
336
|
size: file.size,
|
|
331
337
|
};
|
|
332
|
-
}, [getUploadToken, fileUploader]);
|
|
338
|
+
}, [getUploadToken, fileUploader, t]);
|
|
333
339
|
|
|
334
340
|
/**
|
|
335
341
|
* 文件上传处理
|
|
@@ -339,12 +345,12 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
339
345
|
// 1. 获取预签名 URL
|
|
340
346
|
const tokenResponse = await getUploadToken(file.name);
|
|
341
347
|
if (!tokenResponse) {
|
|
342
|
-
throw new Error('
|
|
348
|
+
throw new Error(t('humanVerify.file.tokenFailed'));
|
|
343
349
|
}
|
|
344
350
|
|
|
345
351
|
// 2. 上传文件到 OSS
|
|
346
352
|
if (!fileUploader) {
|
|
347
|
-
throw new Error('
|
|
353
|
+
throw new Error(t('humanVerify.file.uploadMethodNotConfigured'));
|
|
348
354
|
}
|
|
349
355
|
const blob = new Blob([file]);
|
|
350
356
|
await fileUploader(blob, tokenResponse.uploadUrl);
|
|
@@ -362,7 +368,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
362
368
|
size: file.size,
|
|
363
369
|
fileType: file.name.split('.').pop(),
|
|
364
370
|
};
|
|
365
|
-
}, [getUploadToken, fileUploader, getFileId]);
|
|
371
|
+
}, [getUploadToken, fileUploader, getFileId, t]);
|
|
366
372
|
|
|
367
373
|
/**
|
|
368
374
|
* 自定义上传逻辑
|
|
@@ -379,7 +385,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
379
385
|
onSuccess?.({ url }, new XMLHttpRequest());
|
|
380
386
|
} catch (error) {
|
|
381
387
|
onError?.(error as Error);
|
|
382
|
-
message.error('
|
|
388
|
+
message.error(t('humanVerify.file.uploadFailed'));
|
|
383
389
|
}
|
|
384
390
|
return;
|
|
385
391
|
}
|
|
@@ -399,12 +405,12 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
399
405
|
onSuccess?.(result, new XMLHttpRequest());
|
|
400
406
|
} catch (error) {
|
|
401
407
|
onError?.(error as Error);
|
|
402
|
-
message.error('
|
|
408
|
+
message.error(t('humanVerify.file.uploadFailed'));
|
|
403
409
|
} finally {
|
|
404
410
|
setUploading(false);
|
|
405
411
|
}
|
|
406
412
|
},
|
|
407
|
-
[uploadSender, fileUploader, isImage, handleImageUpload, handleFileUpload]
|
|
413
|
+
[uploadSender, fileUploader, isImage, handleImageUpload, handleFileUpload, t]
|
|
408
414
|
);
|
|
409
415
|
|
|
410
416
|
// 处理文件变化
|
|
@@ -440,19 +446,21 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
440
446
|
// 文件大小限制(默认 10MB)
|
|
441
447
|
const maxSize = 10 * 1024 * 1024;
|
|
442
448
|
if (file.size > maxSize) {
|
|
443
|
-
message.error('
|
|
449
|
+
message.error(t('humanVerify.file.maxSizeError', { size: '10MB' }));
|
|
444
450
|
return Upload.LIST_IGNORE;
|
|
445
451
|
}
|
|
446
452
|
return true;
|
|
447
453
|
},
|
|
448
|
-
[]
|
|
454
|
+
[t]
|
|
449
455
|
);
|
|
450
456
|
|
|
451
457
|
// 上传按钮的加载指示器
|
|
452
458
|
const uploadButton = (
|
|
453
459
|
<div>
|
|
454
460
|
{uploading ? <LoadingOutlined /> : <PlusOutlined />}
|
|
455
|
-
<div style={{ marginTop: 8 }}>
|
|
461
|
+
<div style={{ marginTop: 8 }}>
|
|
462
|
+
{uploading ? t('humanVerify.file.uploading') : t('humanVerify.file.upload')}
|
|
463
|
+
</div>
|
|
456
464
|
</div>
|
|
457
465
|
);
|
|
458
466
|
|
|
@@ -498,7 +506,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
498
506
|
disabled={disabled || uploading}
|
|
499
507
|
loading={uploading}
|
|
500
508
|
>
|
|
501
|
-
{uploading ? '
|
|
509
|
+
{uploading ? t('humanVerify.file.uploading') : t('humanVerify.file.uploadButton')}
|
|
502
510
|
</Button>
|
|
503
511
|
</Upload>
|
|
504
512
|
<FileHint>{hint}</FileHint>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useCallback } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
import { ObjectFieldProps, sortPropertiesByOrder } from './types';
|
|
4
|
+
import { useTranslation } from '../../../i18n';
|
|
4
5
|
|
|
5
6
|
// 前向声明,避免循环依赖
|
|
6
7
|
const FieldRenderer = React.lazy(() => import('./FieldRenderer'));
|
|
@@ -71,7 +72,8 @@ export const ObjectField: React.FC<ObjectFieldProps> = ({
|
|
|
71
72
|
}) => {
|
|
72
73
|
const { Title, Description, Properties, Required: RequiredFields = [] } = schema;
|
|
73
74
|
const displayTitle = Title || name;
|
|
74
|
-
|
|
75
|
+
const { t } = useTranslation();
|
|
76
|
+
|
|
75
77
|
// 计算当前对象的完整路径
|
|
76
78
|
const currentPath = fieldPath ? `${fieldPath}.${name}` : name;
|
|
77
79
|
|
|
@@ -108,7 +110,7 @@ export const ObjectField: React.FC<ObjectFieldProps> = ({
|
|
|
108
110
|
{sortedProperties.map(([propertyName, propertySchema]) => {
|
|
109
111
|
const isRequired = RequiredFields.includes(propertyName);
|
|
110
112
|
return (
|
|
111
|
-
<React.Suspense key={propertyName} fallback={<div
|
|
113
|
+
<React.Suspense key={propertyName} fallback={<div>{t('common.loading')}</div>}>
|
|
112
114
|
<FieldRenderer
|
|
113
115
|
name={propertyName}
|
|
114
116
|
schema={propertySchema}
|
|
@@ -3,6 +3,7 @@ import { DatePicker } from 'antd';
|
|
|
3
3
|
import dayjs, { Dayjs } from 'dayjs';
|
|
4
4
|
import { TimeFieldProps, TimeSubType } from './types';
|
|
5
5
|
import styled from 'styled-components';
|
|
6
|
+
import { useTranslation } from '../../../i18n';
|
|
6
7
|
|
|
7
8
|
// ==================== Styled Components ====================
|
|
8
9
|
|
|
@@ -56,6 +57,7 @@ export const TimeField: React.FC<TimeFieldProps> = ({
|
|
|
56
57
|
onChange,
|
|
57
58
|
disabled = false,
|
|
58
59
|
}) => {
|
|
60
|
+
const { t } = useTranslation();
|
|
59
61
|
// 优先从 AssociationPropertyMetadata.SubType 读取,取数组第一个元素
|
|
60
62
|
// 兼容旧的 TimeSubType 字段
|
|
61
63
|
const subType = useMemo((): TimeSubType | undefined => {
|
|
@@ -95,7 +97,7 @@ export const TimeField: React.FC<TimeFieldProps> = ({
|
|
|
95
97
|
showTime={showTime ? { format: 'HH:mm:ss' } : false}
|
|
96
98
|
disabled={disabled}
|
|
97
99
|
style={{ width: '100%' }}
|
|
98
|
-
placeholder={
|
|
100
|
+
placeholder={t('common.placeholderSelect')}
|
|
99
101
|
/>
|
|
100
102
|
</TimeFieldContainer>
|
|
101
103
|
);
|
|
@@ -3,6 +3,7 @@ import { Button } from 'antd';
|
|
|
3
3
|
import styled from 'styled-components';
|
|
4
4
|
import CustomParamsRenderer from './CustomParamsRenderer';
|
|
5
5
|
import { CustomParamSchema } from './CustomParamsRenderer/types';
|
|
6
|
+
import { useTranslation } from '../../i18n';
|
|
6
7
|
|
|
7
8
|
// ==================== Styled Components ====================
|
|
8
9
|
|
|
@@ -114,6 +115,7 @@ export const convertSchemaToUpperCase = (schema: any): CustomParamSchema | undef
|
|
|
114
115
|
* 用于展示历史对话中的 card 类型消息(只读模式)
|
|
115
116
|
*/
|
|
116
117
|
export const HistoryCard: React.FC<HistoryCardProps> = ({ data }) => {
|
|
118
|
+
const { t } = useTranslation();
|
|
117
119
|
// 从data中提取需要的参数
|
|
118
120
|
const approvalStatus = data?.approvalStatus;
|
|
119
121
|
|
|
@@ -131,7 +133,7 @@ export const HistoryCard: React.FC<HistoryCardProps> = ({ data }) => {
|
|
|
131
133
|
<StatusContainer $approved={isApproved}>
|
|
132
134
|
<StatusContent>
|
|
133
135
|
<StatusText>
|
|
134
|
-
{isApproved ? '
|
|
136
|
+
{isApproved ? t('humanVerify.submitted') : t('humanVerify.pending')}
|
|
135
137
|
</StatusText>
|
|
136
138
|
</StatusContent>
|
|
137
139
|
<Button
|
|
@@ -139,7 +141,7 @@ export const HistoryCard: React.FC<HistoryCardProps> = ({ data }) => {
|
|
|
139
141
|
variant="filled"
|
|
140
142
|
disabled={true}
|
|
141
143
|
>
|
|
142
|
-
{isApproved ? '
|
|
144
|
+
{isApproved ? t('humanVerify.submitted') : t('common.submit')}
|
|
143
145
|
</Button>
|
|
144
146
|
</StatusContainer>
|
|
145
147
|
</HistoryCardContainer>
|
|
@@ -156,7 +158,7 @@ export const HistoryCard: React.FC<HistoryCardProps> = ({ data }) => {
|
|
|
156
158
|
<StatusContainer $approved={isApproved}>
|
|
157
159
|
<StatusContent>
|
|
158
160
|
<StatusText>
|
|
159
|
-
{isApproved ? '
|
|
161
|
+
{isApproved ? t('humanVerify.submitted') : t('humanVerify.pending')}
|
|
160
162
|
</StatusText>
|
|
161
163
|
</StatusContent>
|
|
162
164
|
<Button
|
|
@@ -164,7 +166,7 @@ export const HistoryCard: React.FC<HistoryCardProps> = ({ data }) => {
|
|
|
164
166
|
variant="filled"
|
|
165
167
|
disabled={true}
|
|
166
168
|
>
|
|
167
|
-
{isApproved ? '
|
|
169
|
+
{isApproved ? t('humanVerify.submitted') : t('common.submit')}
|
|
168
170
|
</Button>
|
|
169
171
|
</StatusContainer>
|
|
170
172
|
</HistoryCardContainer>
|
|
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import { Button, message } from 'antd';
|
|
3
3
|
import styled from 'styled-components';
|
|
4
4
|
import CustomParamsRenderer, { validateCustomParams, UploadSender, FileUploader } from './CustomParamsRenderer';
|
|
5
|
+
import { useTranslation } from '../../i18n';
|
|
5
6
|
|
|
6
7
|
// ==================== Styled Components ====================
|
|
7
8
|
|
|
@@ -61,6 +62,7 @@ export const HumanVerify: React.FC<HumanVerifyProps> = ({
|
|
|
61
62
|
fileUploader,
|
|
62
63
|
onSubmit
|
|
63
64
|
}) => {
|
|
65
|
+
const { t } = useTranslation();
|
|
64
66
|
// 从 data 中提取需要的参数
|
|
65
67
|
const verifyId = data?.verifyId;
|
|
66
68
|
const sessionWebhook = data?.sessionWebhook;
|
|
@@ -110,7 +112,7 @@ export const HumanVerify: React.FC<HumanVerifyProps> = ({
|
|
|
110
112
|
setValidationErrors(errors);
|
|
111
113
|
|
|
112
114
|
// 提示用户
|
|
113
|
-
message.error('
|
|
115
|
+
message.error(t('humanVerify.requiredAll'));
|
|
114
116
|
return;
|
|
115
117
|
}
|
|
116
118
|
|
|
@@ -143,7 +145,7 @@ export const HumanVerify: React.FC<HumanVerifyProps> = ({
|
|
|
143
145
|
<StatusContainer $approved={approved}>
|
|
144
146
|
<StatusContent>
|
|
145
147
|
<StatusText>
|
|
146
|
-
{approved ? '
|
|
148
|
+
{approved ? t('humanVerify.submitted') : t('humanVerify.pending')}
|
|
147
149
|
</StatusText>
|
|
148
150
|
</StatusContent>
|
|
149
151
|
<Button
|
|
@@ -152,7 +154,7 @@ export const HumanVerify: React.FC<HumanVerifyProps> = ({
|
|
|
152
154
|
onClick={handleApprove}
|
|
153
155
|
disabled={approved}
|
|
154
156
|
>
|
|
155
|
-
{'
|
|
157
|
+
{t('common.submit')}
|
|
156
158
|
</Button>
|
|
157
159
|
</StatusContainer>
|
|
158
160
|
</HumanVerifyContainer>
|
|
@@ -11,7 +11,10 @@ import { loadEchartsScript } from '@/utils/loadEcharts';
|
|
|
11
11
|
import { DocReferences, DocReferenceItem } from './DocReferences';
|
|
12
12
|
import { WebSearchPanel } from './WebSearchPanel';
|
|
13
13
|
import { HumanVerify, HistoryCard, CustomParamSchema } from './HumanVerify';
|
|
14
|
+
import { A2UISurface } from './A2UIRenderer';
|
|
15
|
+
import type { A2UIMessage } from './A2UIRenderer';
|
|
14
16
|
import { BubbleContent } from '@/core';
|
|
17
|
+
import { useTranslation } from '@/i18n';
|
|
15
18
|
|
|
16
19
|
/** HumanVerify 提交数据类型 */
|
|
17
20
|
export interface HumanVerifySubmitData {
|
|
@@ -58,14 +61,21 @@ export interface MessageBubbleProps {
|
|
|
58
61
|
|
|
59
62
|
// ==================== HumanVerify相关Props ====================
|
|
60
63
|
|
|
61
|
-
/** 事件类型(用于特殊消息如 humanVerify、historyCard) */
|
|
62
|
-
eventType?: 'humanVerify' | 'historyCard';
|
|
64
|
+
/** 事件类型(用于特殊消息如 humanVerify、historyCard、a2ui) */
|
|
65
|
+
eventType?: 'humanVerify' | 'historyCard' | 'a2ui';
|
|
63
66
|
/** HumanVerify 相关数据 */
|
|
64
67
|
humanVerifyData?: HumanVerifyData;
|
|
65
68
|
/** HistoryCard 相关数据(历史对话中的审核卡片) */
|
|
66
69
|
historyCardData?: HistoryCardData;
|
|
67
70
|
/** HumanVerify 提交回调 */
|
|
68
71
|
onHumanVerifySubmit?: (data: HumanVerifySubmitData) => void;
|
|
72
|
+
|
|
73
|
+
// ==================== A2UI相关Props ====================
|
|
74
|
+
|
|
75
|
+
/** A2UI 消息数组(Agent 生成的声明式 UI 描述) */
|
|
76
|
+
a2uiMessages?: A2UIMessage[];
|
|
77
|
+
/** A2UI 用户交互回调 */
|
|
78
|
+
onA2UIAction?: (action: any) => void;
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
// 样式隔离容器
|
|
@@ -270,7 +280,11 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
270
280
|
humanVerifyData,
|
|
271
281
|
historyCardData,
|
|
272
282
|
onHumanVerifySubmit,
|
|
283
|
+
// A2UI 相关 props
|
|
284
|
+
a2uiMessages,
|
|
285
|
+
onA2UIAction,
|
|
273
286
|
}) => {
|
|
287
|
+
const { t } = useTranslation();
|
|
274
288
|
const [modal, contextHolder] = Modal.useModal();
|
|
275
289
|
|
|
276
290
|
// 网页搜索抽屉状态
|
|
@@ -293,7 +307,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
293
307
|
modal.confirm({
|
|
294
308
|
bodyStyle: { maxHeight: '80vh', overflow: 'auto' },
|
|
295
309
|
icon: null,
|
|
296
|
-
title: '
|
|
310
|
+
title: t('source.title'),
|
|
297
311
|
destroyOnClose: true,
|
|
298
312
|
maskClosable: true,
|
|
299
313
|
closable: true,
|
|
@@ -317,7 +331,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
317
331
|
),
|
|
318
332
|
footer: null,
|
|
319
333
|
});
|
|
320
|
-
}, [modal]);
|
|
334
|
+
}, [modal, t]);
|
|
321
335
|
|
|
322
336
|
// 默认的网页搜索点击处理
|
|
323
337
|
const defaultWebSearchClick = useCallback((items: DocReferenceItem[]) => {
|
|
@@ -362,6 +376,14 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
362
376
|
data={historyCardData}
|
|
363
377
|
/>
|
|
364
378
|
)}
|
|
379
|
+
|
|
380
|
+
{/* A2UI 事件 - Agent 声明式 UI 渲染 */}
|
|
381
|
+
{eventType === 'a2ui' && a2uiMessages && a2uiMessages.length > 0 && (
|
|
382
|
+
<A2UISurface
|
|
383
|
+
messages={a2uiMessages}
|
|
384
|
+
onAction={onA2UIAction}
|
|
385
|
+
/>
|
|
386
|
+
)}
|
|
365
387
|
|
|
366
388
|
{/* 参考资料 */}
|
|
367
389
|
{references && references.length > 0 && status === 'Success' && (
|
|
@@ -12,6 +12,7 @@ import { loadEchartsScript } from '../utils/loadEcharts';
|
|
|
12
12
|
import { DocReferences, DocReferenceItem } from './DocReferences';
|
|
13
13
|
import { WebSearchPanel } from './WebSearchPanel';
|
|
14
14
|
import { RichBubbleContent } from '../core';
|
|
15
|
+
import { useTranslation } from '../i18n';
|
|
15
16
|
|
|
16
17
|
export interface RichMessageBubbleProps {
|
|
17
18
|
/** 消息内容 */
|
|
@@ -177,6 +178,7 @@ export const RichMessageBubble: React.FC<RichMessageBubbleProps> = ({
|
|
|
177
178
|
onReferenceClick,
|
|
178
179
|
onWebSearchClick,
|
|
179
180
|
}) => {
|
|
181
|
+
const { t } = useTranslation();
|
|
180
182
|
const [modal, contextHolder] = Modal.useModal();
|
|
181
183
|
|
|
182
184
|
// 网页搜索抽屉状态
|
|
@@ -199,7 +201,7 @@ export const RichMessageBubble: React.FC<RichMessageBubbleProps> = ({
|
|
|
199
201
|
modal.confirm({
|
|
200
202
|
bodyStyle: { maxHeight: '80vh', overflow: 'auto' },
|
|
201
203
|
icon: null,
|
|
202
|
-
title: '
|
|
204
|
+
title: t('source.title'),
|
|
203
205
|
destroyOnClose: true,
|
|
204
206
|
maskClosable: true,
|
|
205
207
|
closable: true,
|
|
@@ -223,7 +225,7 @@ export const RichMessageBubble: React.FC<RichMessageBubbleProps> = ({
|
|
|
223
225
|
),
|
|
224
226
|
footer: null,
|
|
225
227
|
});
|
|
226
|
-
}, [modal]);
|
|
228
|
+
}, [modal, t]);
|
|
227
229
|
|
|
228
230
|
// 默认的网页搜索点击处理
|
|
229
231
|
const defaultWebSearchClick = useCallback((items: DocReferenceItem[]) => {
|
|
@@ -19,6 +19,7 @@ import { Collapse } from 'antd';
|
|
|
19
19
|
import { MarkdownView } from '../markdown';
|
|
20
20
|
import { convertTableDataToMarkdown, processStepContent } from '../markdown/utils/dataProcessor';
|
|
21
21
|
import { RichBubbleProvider } from '../context/RichBubble';
|
|
22
|
+
import { useTranslation } from '../i18n';
|
|
22
23
|
|
|
23
24
|
export interface RichBubbleContentProps {
|
|
24
25
|
/** 消息内容 */
|
|
@@ -110,6 +111,7 @@ export const RichBubbleContent: React.FC<RichBubbleContentProps> = ({
|
|
|
110
111
|
children,
|
|
111
112
|
waitingMessage,
|
|
112
113
|
}) => {
|
|
114
|
+
const { t } = useTranslation();
|
|
113
115
|
// step消息折叠面板激活的索引
|
|
114
116
|
const [activeKey, setActiveKey] = useState<string | string[]>([]);
|
|
115
117
|
|
|
@@ -150,7 +152,7 @@ export const RichBubbleContent: React.FC<RichBubbleContentProps> = ({
|
|
|
150
152
|
} else {
|
|
151
153
|
const contentToRender = detail || '';
|
|
152
154
|
if (!contentToRender) {
|
|
153
|
-
return <StyledStepContent
|
|
155
|
+
return <StyledStepContent>{t('rich.emptyContent')}</StyledStepContent>;
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
return (
|
|
@@ -250,7 +252,7 @@ export const RichBubbleContent: React.FC<RichBubbleContentProps> = ({
|
|
|
250
252
|
<StyledStatusIcon $success={status === 'Success'}>
|
|
251
253
|
{status === 'Success' ? '✓' : '⟳'}
|
|
252
254
|
</StyledStatusIcon>
|
|
253
|
-
<StyledStepNumber
|
|
255
|
+
<StyledStepNumber>{t('rich.stepLabel', { index: index + 1 })}</StyledStepNumber>
|
|
254
256
|
<StyledStepText>{itemContent?.title}</StyledStepText>
|
|
255
257
|
</StyledStepTitleWrapper>
|
|
256
258
|
),
|
|
@@ -11,6 +11,7 @@ import styled from 'styled-components';
|
|
|
11
11
|
import { Image } from 'antd';
|
|
12
12
|
import { SearchOutlined } from '@ant-design/icons';
|
|
13
13
|
import { flatten, uniq } from 'lodash-es';
|
|
14
|
+
import { useTranslation } from '../i18n';
|
|
14
15
|
|
|
15
16
|
// 参考资料项类型
|
|
16
17
|
export interface SourceItem {
|
|
@@ -195,6 +196,7 @@ export const SourceContent: React.FC<SourceContentProps> = ({
|
|
|
195
196
|
className,
|
|
196
197
|
style,
|
|
197
198
|
}) => {
|
|
199
|
+
const { t } = useTranslation();
|
|
198
200
|
// 提取唯一图片
|
|
199
201
|
const uniqueImages = useMemo(() => {
|
|
200
202
|
let images;
|
|
@@ -244,14 +246,14 @@ export const SourceContent: React.FC<SourceContentProps> = ({
|
|
|
244
246
|
{webSearchArray.length > 0 && (
|
|
245
247
|
<StyledWebSearchSource onClick={handleWebSearchClick}>
|
|
246
248
|
<SearchOutlined />
|
|
247
|
-
<div
|
|
249
|
+
<div>{t('webSearch.foundPages', { count: webSearchArray.length })}</div>
|
|
248
250
|
</StyledWebSearchSource>
|
|
249
251
|
)}
|
|
250
252
|
|
|
251
253
|
{/* RAG参考资料 */}
|
|
252
254
|
{ragArray.length > 0 && (
|
|
253
255
|
<StyledAnswerSource>
|
|
254
|
-
<StyledLabel
|
|
256
|
+
<StyledLabel>{t('source.answerFrom')}</StyledLabel>
|
|
255
257
|
<div style={{ width: 'calc(100% - 70px)' }}>
|
|
256
258
|
{ragArray.map((item, index) => (
|
|
257
259
|
<SourceItemComponent
|
|
@@ -268,7 +270,7 @@ export const SourceContent: React.FC<SourceContentProps> = ({
|
|
|
268
270
|
{/* 图片来源 */}
|
|
269
271
|
{uniqueImages.length > 0 && (
|
|
270
272
|
<StyledAnswerSource>
|
|
271
|
-
<StyledLabel
|
|
273
|
+
<StyledLabel>{t('source.imageFrom')}</StyledLabel>
|
|
272
274
|
<StyledWrapSpace>
|
|
273
275
|
{uniqueImages.map((image, index) => (
|
|
274
276
|
<StyledImageBox key={index}>
|
|
@@ -6,6 +6,7 @@ import React from 'react';
|
|
|
6
6
|
import styled from 'styled-components';
|
|
7
7
|
import { Drawer, List, Typography } from 'antd';
|
|
8
8
|
import { CloseOutlined } from '@ant-design/icons';
|
|
9
|
+
import { useTranslation } from '../i18n';
|
|
9
10
|
|
|
10
11
|
const { Title } = Typography;
|
|
11
12
|
|
|
@@ -136,6 +137,7 @@ export const WebSearchContent: React.FC<WebSearchContentProps> = ({
|
|
|
136
137
|
className,
|
|
137
138
|
style,
|
|
138
139
|
}) => {
|
|
140
|
+
const { t } = useTranslation();
|
|
139
141
|
// 判断是否移动端
|
|
140
142
|
const isMobile = typeof window !== 'undefined' && window.innerWidth <= 500;
|
|
141
143
|
const panelWidth = isMobile ? '100%' : width;
|
|
@@ -174,7 +176,7 @@ export const WebSearchContent: React.FC<WebSearchContentProps> = ({
|
|
|
174
176
|
<StyledContainer $isMobile={isMobile}>
|
|
175
177
|
<StyledContent>
|
|
176
178
|
<StyledHeader>
|
|
177
|
-
<Title level={5} style={{ margin: 0 }}
|
|
179
|
+
<Title level={5} style={{ margin: 0 }}>{t('webSearch.title')}</Title>
|
|
178
180
|
<StyledCloseButton onClick={handleClose}>
|
|
179
181
|
<CloseOutlined />
|
|
180
182
|
</StyledCloseButton>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale Context 与 Provider
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { createContext, useEffect, useMemo } from 'react';
|
|
6
|
+
import zhCN from '../locales/zh-CN';
|
|
7
|
+
import type { DeepPartial, Locale } from '../locales/types';
|
|
8
|
+
import { setGlobalLocale } from './translate';
|
|
9
|
+
import { deepMerge } from './utils';
|
|
10
|
+
|
|
11
|
+
export interface LocaleContextValue {
|
|
12
|
+
locale: Locale;
|
|
13
|
+
localeName: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const LocaleContext = createContext<LocaleContextValue>({
|
|
17
|
+
locale: zhCN as unknown as Locale,
|
|
18
|
+
localeName: 'zh-CN',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export interface LocaleProviderProps {
|
|
22
|
+
/** 完整 locale 对象,默认使用内置 zhCN */
|
|
23
|
+
locale?: Locale;
|
|
24
|
+
/** 语言标识,如 'zh-CN' / 'en-US' */
|
|
25
|
+
localeName?: string;
|
|
26
|
+
/** 部分覆盖:在选定 locale 基础上 deep merge 自定义文案 */
|
|
27
|
+
overrides?: DeepPartial<Locale>;
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* LocaleProvider
|
|
33
|
+
*
|
|
34
|
+
* 用法示例:
|
|
35
|
+
* ```tsx
|
|
36
|
+
* import { LocaleProvider, enUS } from '@alicloud/appflow-chat';
|
|
37
|
+
*
|
|
38
|
+
* <LocaleProvider locale={enUS} localeName="en-US">
|
|
39
|
+
* <App />
|
|
40
|
+
* </LocaleProvider>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export const LocaleProvider: React.FC<LocaleProviderProps> = ({
|
|
44
|
+
locale,
|
|
45
|
+
localeName = 'zh-CN',
|
|
46
|
+
overrides,
|
|
47
|
+
children,
|
|
48
|
+
}) => {
|
|
49
|
+
const baseLocale = (locale ?? (zhCN as unknown as Locale)) as Locale;
|
|
50
|
+
|
|
51
|
+
const mergedLocale = useMemo<Locale>(() => {
|
|
52
|
+
if (!overrides) return baseLocale;
|
|
53
|
+
return deepMerge(
|
|
54
|
+
baseLocale as unknown as Record<string, unknown>,
|
|
55
|
+
overrides as Record<string, unknown>
|
|
56
|
+
) as unknown as Locale;
|
|
57
|
+
}, [baseLocale, overrides]);
|
|
58
|
+
|
|
59
|
+
// 同步到全局,供非 React 场景(如 ChatService)使用
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
setGlobalLocale(mergedLocale, localeName);
|
|
62
|
+
}, [mergedLocale, localeName]);
|
|
63
|
+
|
|
64
|
+
const contextValue = useMemo<LocaleContextValue>(
|
|
65
|
+
() => ({ locale: mergedLocale, localeName }),
|
|
66
|
+
[mergedLocale, localeName]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return <LocaleContext.Provider value={contextValue}>{children}</LocaleContext.Provider>;
|
|
70
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n 模块统一导出
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { LocaleProvider, LocaleContext } from './LocaleContext';
|
|
6
|
+
export type { LocaleProviderProps, LocaleContextValue } from './LocaleContext';
|
|
7
|
+
|
|
8
|
+
export { useTranslation } from './useTranslation';
|
|
9
|
+
export type { UseTranslationResult } from './useTranslation';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
translate,
|
|
13
|
+
setGlobalLocale,
|
|
14
|
+
getGlobalLocale,
|
|
15
|
+
getGlobalLocaleName,
|
|
16
|
+
} from './translate';
|