@alicloud/appflow-chat 0.0.4-alpha.1 → 0.0.4-alpha.2
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 +236 -858
- package/dist/appflow-chat.esm.js +18560 -19658
- package/dist/types/index.d.ts +93 -74
- package/package.json +2 -3
- package/src/components/ChatSender.tsx +516 -0
- package/src/components/MessageBubble.tsx +2 -22
- package/src/index.ts +2 -6
- package/src/components/A2UIRenderer/A2UIRenderer.tsx +0 -181
- package/src/components/A2UIRenderer/index.ts +0 -1
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatSender - 聊天输入框组件
|
|
3
|
+
* 封装 @ant-design/x 的 Sender + Attachments,集成文件上传、语音输入、联网搜索、模型选择等能力
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useState, useRef, useCallback, useMemo } from 'react';
|
|
7
|
+
import { Sender, Attachments } from '@ant-design/x';
|
|
8
|
+
import type { AttachmentsProps } from '@ant-design/x';
|
|
9
|
+
import { Select, Switch, Button, Tooltip, Badge } from 'antd';
|
|
10
|
+
import {
|
|
11
|
+
PaperClipOutlined,
|
|
12
|
+
PictureOutlined,
|
|
13
|
+
GlobalOutlined,
|
|
14
|
+
DeleteOutlined,
|
|
15
|
+
CloudUploadOutlined,
|
|
16
|
+
} from '@ant-design/icons';
|
|
17
|
+
import styled from 'styled-components';
|
|
18
|
+
import type { ModelInfo, ModelCapabilities } from '../services/ChatService';
|
|
19
|
+
|
|
20
|
+
/** Attachments 组件的 ref 类型 */
|
|
21
|
+
interface AttachmentsRefType {
|
|
22
|
+
nativeElement: HTMLDivElement | null;
|
|
23
|
+
upload: (file: File) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 附件文件项类型(兼容 antd Upload fileList 项) */
|
|
27
|
+
interface AttachmentFileItem {
|
|
28
|
+
uid: string;
|
|
29
|
+
name: string;
|
|
30
|
+
status?: 'uploading' | 'done' | 'error' | 'removed';
|
|
31
|
+
thumbUrl?: string;
|
|
32
|
+
url?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ==================== 类型定义 ====================
|
|
36
|
+
|
|
37
|
+
/** 附件信息(上传完成后携带下载URL) */
|
|
38
|
+
export interface ChatAttachment {
|
|
39
|
+
/** 文件唯一标识 */
|
|
40
|
+
uid: string;
|
|
41
|
+
/** 文件名 */
|
|
42
|
+
name: string;
|
|
43
|
+
/** 上传状态 */
|
|
44
|
+
status: 'uploading' | 'done' | 'error';
|
|
45
|
+
/** 文件类型:image 或 file */
|
|
46
|
+
type: 'image' | 'file';
|
|
47
|
+
/** 上传后的下载URL */
|
|
48
|
+
url?: string;
|
|
49
|
+
/** 本地预览URL(图片) */
|
|
50
|
+
thumbUrl?: string;
|
|
51
|
+
/** 原始文件对象 */
|
|
52
|
+
originFile?: File;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 提交时的消息数据 */
|
|
56
|
+
export interface ChatSenderSubmitData {
|
|
57
|
+
/** 文本内容 */
|
|
58
|
+
text: string;
|
|
59
|
+
/** 图片URL列表 */
|
|
60
|
+
images: string[];
|
|
61
|
+
/** 文件URL列表 */
|
|
62
|
+
files: string[];
|
|
63
|
+
/** 选中的模型ID */
|
|
64
|
+
modelId?: string;
|
|
65
|
+
/** 是否启用联网搜索 */
|
|
66
|
+
webSearch: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ChatSenderProps {
|
|
70
|
+
/** 是否处于加载状态(AI正在回复) */
|
|
71
|
+
loading?: boolean;
|
|
72
|
+
/** 是否禁用 */
|
|
73
|
+
disabled?: boolean;
|
|
74
|
+
/** 输入框占位文本 */
|
|
75
|
+
placeholder?: string;
|
|
76
|
+
/** 提交方式:'enter' 回车发送 | 'shiftEnter' Shift+回车发送 */
|
|
77
|
+
submitType?: 'enter' | 'shiftEnter';
|
|
78
|
+
|
|
79
|
+
// ==================== 模型相关 ====================
|
|
80
|
+
|
|
81
|
+
/** 可用模型列表 */
|
|
82
|
+
models?: ModelInfo[];
|
|
83
|
+
/** 当前选中的模型ID */
|
|
84
|
+
modelId?: string;
|
|
85
|
+
/** 默认选中的模型ID(非受控) */
|
|
86
|
+
defaultModelId?: string;
|
|
87
|
+
/** 模型切换回调 */
|
|
88
|
+
onModelChange?: (modelId: string) => void;
|
|
89
|
+
|
|
90
|
+
// ==================== 能力配置 ====================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 模型能力配置,控制功能按钮的显隐
|
|
94
|
+
* 如果不传,所有功能按钮都显示
|
|
95
|
+
*/
|
|
96
|
+
capabilities?: ModelCapabilities;
|
|
97
|
+
|
|
98
|
+
// ==================== 事件回调 ====================
|
|
99
|
+
|
|
100
|
+
/** 提交消息回调 */
|
|
101
|
+
onSubmit?: (data: ChatSenderSubmitData) => void;
|
|
102
|
+
/** 取消当前请求 */
|
|
103
|
+
onCancel?: () => void;
|
|
104
|
+
/** 清除会话回调 */
|
|
105
|
+
onClear?: () => void;
|
|
106
|
+
/** 文件上传方法,返回下载URL */
|
|
107
|
+
onUpload?: (file: File) => Promise<string>;
|
|
108
|
+
|
|
109
|
+
// ==================== 样式 ====================
|
|
110
|
+
|
|
111
|
+
/** 自定义类名 */
|
|
112
|
+
className?: string;
|
|
113
|
+
/** 自定义样式 */
|
|
114
|
+
style?: React.CSSProperties;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ==================== 样式组件 ====================
|
|
118
|
+
|
|
119
|
+
const SenderWrapper = styled.div`
|
|
120
|
+
.appflow-chat-sender-prefix {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 4px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.appflow-chat-sender-actions {
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 4px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.appflow-chat-sender-footer {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: space-between;
|
|
136
|
+
padding: 4px 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.appflow-chat-sender-footer-left {
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: 8px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.appflow-chat-sender-footer-right {
|
|
146
|
+
display: flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: 8px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.appflow-chat-sender-web-search {
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: 4px;
|
|
155
|
+
font-size: 13px;
|
|
156
|
+
color: #666;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.appflow-chat-sender-model-select {
|
|
160
|
+
min-width: 120px;
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
// ==================== 工具函数 ====================
|
|
165
|
+
|
|
166
|
+
function generateUid(): string {
|
|
167
|
+
return `file_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isImageFile(file: File): boolean {
|
|
171
|
+
return file.type.startsWith('image/');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ==================== 组件实现 ====================
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* ChatSender - 聊天输入框组件
|
|
178
|
+
*
|
|
179
|
+
* 集成了文本输入、文件/图片上传、语音输入、联网搜索、模型选择等功能。
|
|
180
|
+
* 根据模型能力自动控制功能按钮的显隐。
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```tsx
|
|
184
|
+
* <ChatSender
|
|
185
|
+
* loading={isLoading}
|
|
186
|
+
* models={config.models}
|
|
187
|
+
* capabilities={chatService.getModelCapabilities(modelId)}
|
|
188
|
+
* onSubmit={({ text, images, files, modelId, webSearch }) => {
|
|
189
|
+
* chatService.chat({ text, images, files, modelId, webSearch });
|
|
190
|
+
* }}
|
|
191
|
+
* onCancel={() => chatService.cancel()}
|
|
192
|
+
* onClear={() => chatService.clear()}
|
|
193
|
+
* onUpload={(file) => chatService.upload(file)}
|
|
194
|
+
* />
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export const ChatSender: React.FC<ChatSenderProps> = ({
|
|
198
|
+
loading = false,
|
|
199
|
+
disabled = false,
|
|
200
|
+
placeholder = '',
|
|
201
|
+
submitType = 'enter',
|
|
202
|
+
models = [],
|
|
203
|
+
modelId: controlledModelId,
|
|
204
|
+
defaultModelId,
|
|
205
|
+
onModelChange,
|
|
206
|
+
capabilities,
|
|
207
|
+
onSubmit,
|
|
208
|
+
onCancel,
|
|
209
|
+
onClear,
|
|
210
|
+
onUpload,
|
|
211
|
+
className,
|
|
212
|
+
style,
|
|
213
|
+
}) => {
|
|
214
|
+
// ==================== 状态管理 ====================
|
|
215
|
+
|
|
216
|
+
const [inputValue, setInputValue] = useState('');
|
|
217
|
+
const [internalModelId, setInternalModelId] = useState<string | undefined>(
|
|
218
|
+
defaultModelId || models[0]?.id
|
|
219
|
+
);
|
|
220
|
+
const [webSearchEnabled, setWebSearchEnabled] = useState(false);
|
|
221
|
+
const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
|
|
222
|
+
const [headerOpen, setHeaderOpen] = useState(false);
|
|
223
|
+
|
|
224
|
+
const attachmentsRef = useRef<AttachmentsRefType>(null);
|
|
225
|
+
|
|
226
|
+
// 受控/非受控模型ID
|
|
227
|
+
const currentModelId = controlledModelId ?? internalModelId;
|
|
228
|
+
|
|
229
|
+
// ==================== 能力判断 ====================
|
|
230
|
+
|
|
231
|
+
const hasImageCapability = capabilities?.image ?? true;
|
|
232
|
+
const hasFileCapability = capabilities?.file ?? true;
|
|
233
|
+
const hasAudioCapability = capabilities?.audio ?? false;
|
|
234
|
+
const hasWebSearchCapability = capabilities?.webSearch ?? false;
|
|
235
|
+
|
|
236
|
+
const hasUploadCapability = hasImageCapability || hasFileCapability;
|
|
237
|
+
const hasAttachments = attachments.length > 0;
|
|
238
|
+
const uploadingCount = attachments.filter(a => a.status === 'uploading').length;
|
|
239
|
+
const isUploading = uploadingCount > 0;
|
|
240
|
+
|
|
241
|
+
// ==================== 文件上传处理 ====================
|
|
242
|
+
|
|
243
|
+
const handleFileUpload = useCallback(async (file: File) => {
|
|
244
|
+
if (!onUpload) return;
|
|
245
|
+
|
|
246
|
+
const uid = generateUid();
|
|
247
|
+
const fileType = isImageFile(file) ? 'image' : 'file';
|
|
248
|
+
|
|
249
|
+
// 检查能力限制
|
|
250
|
+
if (fileType === 'image' && !hasImageCapability) return;
|
|
251
|
+
if (fileType === 'file' && !hasFileCapability) return;
|
|
252
|
+
|
|
253
|
+
const newAttachment: ChatAttachment = {
|
|
254
|
+
uid,
|
|
255
|
+
name: file.name,
|
|
256
|
+
status: 'uploading',
|
|
257
|
+
type: fileType,
|
|
258
|
+
thumbUrl: fileType === 'image' ? URL.createObjectURL(file) : undefined,
|
|
259
|
+
originFile: file,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
setAttachments(prev => [...prev, newAttachment]);
|
|
263
|
+
setHeaderOpen(true);
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const downloadUrl = await onUpload(file);
|
|
267
|
+
setAttachments(prev =>
|
|
268
|
+
prev.map(a => a.uid === uid ? { ...a, status: 'done' as const, url: downloadUrl } : a)
|
|
269
|
+
);
|
|
270
|
+
} catch {
|
|
271
|
+
setAttachments(prev =>
|
|
272
|
+
prev.map(a => a.uid === uid ? { ...a, status: 'error' as const } : a)
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}, [onUpload, hasImageCapability, hasFileCapability]);
|
|
276
|
+
|
|
277
|
+
// ==================== 提交处理 ====================
|
|
278
|
+
|
|
279
|
+
const handleSubmit = useCallback((text: string) => {
|
|
280
|
+
if (!text.trim() && !hasAttachments) return;
|
|
281
|
+
if (isUploading) return;
|
|
282
|
+
|
|
283
|
+
const images = attachments
|
|
284
|
+
.filter(a => a.type === 'image' && a.status === 'done' && a.url)
|
|
285
|
+
.map(a => a.url!);
|
|
286
|
+
|
|
287
|
+
const files = attachments
|
|
288
|
+
.filter(a => a.type === 'file' && a.status === 'done' && a.url)
|
|
289
|
+
.map(a => a.url!);
|
|
290
|
+
|
|
291
|
+
onSubmit?.({
|
|
292
|
+
text: text.trim(),
|
|
293
|
+
images,
|
|
294
|
+
files,
|
|
295
|
+
modelId: currentModelId,
|
|
296
|
+
webSearch: webSearchEnabled,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// 清空状态
|
|
300
|
+
setInputValue('');
|
|
301
|
+
setAttachments([]);
|
|
302
|
+
setHeaderOpen(false);
|
|
303
|
+
}, [attachments, hasAttachments, isUploading, currentModelId, webSearchEnabled, onSubmit]);
|
|
304
|
+
|
|
305
|
+
// ==================== 模型切换 ====================
|
|
306
|
+
|
|
307
|
+
const handleModelChange = useCallback((value: string) => {
|
|
308
|
+
setInternalModelId(value);
|
|
309
|
+
onModelChange?.(value);
|
|
310
|
+
}, [onModelChange]);
|
|
311
|
+
|
|
312
|
+
// ==================== 粘贴文件 ====================
|
|
313
|
+
|
|
314
|
+
const handlePasteFile = useCallback((firstFile: File) => {
|
|
315
|
+
handleFileUpload(firstFile);
|
|
316
|
+
}, [handleFileUpload]);
|
|
317
|
+
|
|
318
|
+
// ==================== Attachments 配置 ====================
|
|
319
|
+
|
|
320
|
+
const attachmentItems: AttachmentFileItem[] = useMemo(() => {
|
|
321
|
+
return attachments.map(a => ({
|
|
322
|
+
uid: a.uid,
|
|
323
|
+
name: a.name,
|
|
324
|
+
status: a.status === 'done' ? 'done' as const : a.status === 'error' ? 'error' as const : 'uploading' as const,
|
|
325
|
+
thumbUrl: a.thumbUrl,
|
|
326
|
+
url: a.url,
|
|
327
|
+
}));
|
|
328
|
+
}, [attachments]);
|
|
329
|
+
|
|
330
|
+
// 文件类型限制
|
|
331
|
+
const acceptTypes = useMemo(() => {
|
|
332
|
+
const types: string[] = [];
|
|
333
|
+
if (hasImageCapability) {
|
|
334
|
+
types.push('image/*');
|
|
335
|
+
}
|
|
336
|
+
if (hasFileCapability) {
|
|
337
|
+
const fileConfig = capabilities?.fileConfig;
|
|
338
|
+
if (fileConfig?.supportFileTypes?.length) {
|
|
339
|
+
types.push(...fileConfig.supportFileTypes.map(t => `.${t}`));
|
|
340
|
+
} else {
|
|
341
|
+
types.push('.pdf', '.doc', '.docx', '.txt', '.csv', '.xlsx', '.xls', '.md', '.json');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return types.join(',');
|
|
345
|
+
}, [hasImageCapability, hasFileCapability, capabilities?.fileConfig]);
|
|
346
|
+
|
|
347
|
+
// ==================== 渲染:Header(附件区域) ====================
|
|
348
|
+
|
|
349
|
+
const renderHeader = useMemo(() => {
|
|
350
|
+
if (!hasUploadCapability) return undefined;
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<Sender.Header
|
|
354
|
+
title="附件"
|
|
355
|
+
open={headerOpen}
|
|
356
|
+
onOpenChange={setHeaderOpen}
|
|
357
|
+
closable
|
|
358
|
+
styles={{
|
|
359
|
+
content: { padding: 4 },
|
|
360
|
+
}}
|
|
361
|
+
>
|
|
362
|
+
<Attachments
|
|
363
|
+
ref={attachmentsRef as any}
|
|
364
|
+
items={attachmentItems as AttachmentsProps['items']}
|
|
365
|
+
beforeUpload={() => false}
|
|
366
|
+
onRemove={(file) => {
|
|
367
|
+
setAttachments(prev => {
|
|
368
|
+
const updated = prev.filter(a => a.uid !== file.uid);
|
|
369
|
+
if (updated.length === 0) {
|
|
370
|
+
setHeaderOpen(false);
|
|
371
|
+
}
|
|
372
|
+
return updated;
|
|
373
|
+
});
|
|
374
|
+
}}
|
|
375
|
+
placeholder={{
|
|
376
|
+
icon: <CloudUploadOutlined />,
|
|
377
|
+
title: '拖拽文件到此处',
|
|
378
|
+
description: '支持图片和文件',
|
|
379
|
+
}}
|
|
380
|
+
/>
|
|
381
|
+
</Sender.Header>
|
|
382
|
+
);
|
|
383
|
+
}, [hasUploadCapability, headerOpen, attachmentItems]);
|
|
384
|
+
|
|
385
|
+
// ==================== 渲染:Footer(模型选择 + 联网搜索 + 清除) ====================
|
|
386
|
+
|
|
387
|
+
const renderFooter = useMemo(() => {
|
|
388
|
+
const hasFooterContent = models.length > 1 || hasWebSearchCapability || onClear;
|
|
389
|
+
if (!hasFooterContent) return undefined;
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<div className="appflow-chat-sender-footer">
|
|
393
|
+
<div className="appflow-chat-sender-footer-left">
|
|
394
|
+
{/* 模型选择 */}
|
|
395
|
+
{models.length > 1 && (
|
|
396
|
+
<Select
|
|
397
|
+
className="appflow-chat-sender-model-select"
|
|
398
|
+
size="small"
|
|
399
|
+
value={currentModelId}
|
|
400
|
+
onChange={handleModelChange}
|
|
401
|
+
options={models.map(m => ({ label: m.name, value: m.id }))}
|
|
402
|
+
variant="borderless"
|
|
403
|
+
popupMatchSelectWidth={false}
|
|
404
|
+
/>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
{/* 联网搜索开关 */}
|
|
408
|
+
{hasWebSearchCapability && (
|
|
409
|
+
<div className="appflow-chat-sender-web-search">
|
|
410
|
+
<GlobalOutlined />
|
|
411
|
+
<Switch
|
|
412
|
+
size="small"
|
|
413
|
+
checked={webSearchEnabled}
|
|
414
|
+
onChange={setWebSearchEnabled}
|
|
415
|
+
/>
|
|
416
|
+
<span>联网搜索</span>
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
<div className="appflow-chat-sender-footer-right">
|
|
422
|
+
{/* 清除会话 */}
|
|
423
|
+
{onClear && (
|
|
424
|
+
<Tooltip title="清除会话">
|
|
425
|
+
<Button
|
|
426
|
+
type="text"
|
|
427
|
+
size="small"
|
|
428
|
+
icon={<DeleteOutlined />}
|
|
429
|
+
onClick={onClear}
|
|
430
|
+
disabled={loading}
|
|
431
|
+
/>
|
|
432
|
+
</Tooltip>
|
|
433
|
+
)}
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
);
|
|
437
|
+
}, [models, currentModelId, handleModelChange, hasWebSearchCapability, webSearchEnabled, onClear, loading]);
|
|
438
|
+
|
|
439
|
+
// ==================== 渲染:Actions(上传按钮) ====================
|
|
440
|
+
|
|
441
|
+
const renderActions = useCallback((
|
|
442
|
+
_oriNode: React.ReactNode,
|
|
443
|
+
info: { components: { SendButton: React.ComponentType<any>; ClearButton: React.ComponentType<any>; LoadingButton: React.ComponentType<any> } }
|
|
444
|
+
) => {
|
|
445
|
+
const { SendButton, LoadingButton } = info.components;
|
|
446
|
+
|
|
447
|
+
return (
|
|
448
|
+
<div className="appflow-chat-sender-actions">
|
|
449
|
+
{/* 上传按钮 */}
|
|
450
|
+
{hasUploadCapability && (
|
|
451
|
+
<Tooltip title={hasAttachments ? `${attachments.length} 个附件` : '上传文件'}>
|
|
452
|
+
<Badge count={attachments.length} size="small" offset={[-4, 4]}>
|
|
453
|
+
<Button
|
|
454
|
+
type="text"
|
|
455
|
+
size="small"
|
|
456
|
+
icon={hasImageCapability && !hasFileCapability ? <PictureOutlined /> : <PaperClipOutlined />}
|
|
457
|
+
disabled={disabled || loading}
|
|
458
|
+
onClick={() => {
|
|
459
|
+
// 触发文件选择
|
|
460
|
+
const input = document.createElement('input');
|
|
461
|
+
input.type = 'file';
|
|
462
|
+
input.accept = acceptTypes;
|
|
463
|
+
input.multiple = true;
|
|
464
|
+
input.onchange = (e) => {
|
|
465
|
+
const fileList = (e.target as HTMLInputElement).files;
|
|
466
|
+
if (fileList) {
|
|
467
|
+
Array.from(fileList).forEach(handleFileUpload);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
input.click();
|
|
471
|
+
}}
|
|
472
|
+
/>
|
|
473
|
+
</Badge>
|
|
474
|
+
</Tooltip>
|
|
475
|
+
)}
|
|
476
|
+
|
|
477
|
+
{/* 发送/停止按钮 */}
|
|
478
|
+
{loading ? (
|
|
479
|
+
<LoadingButton />
|
|
480
|
+
) : (
|
|
481
|
+
<SendButton disabled={disabled || isUploading || (!inputValue.trim() && !hasAttachments)} />
|
|
482
|
+
)}
|
|
483
|
+
</div>
|
|
484
|
+
);
|
|
485
|
+
}, [
|
|
486
|
+
hasUploadCapability, hasImageCapability, hasFileCapability,
|
|
487
|
+
hasAttachments, attachments.length, acceptTypes,
|
|
488
|
+
disabled, loading, isUploading, inputValue, handleFileUpload,
|
|
489
|
+
]);
|
|
490
|
+
|
|
491
|
+
// ==================== 主渲染 ====================
|
|
492
|
+
|
|
493
|
+
return (
|
|
494
|
+
<SenderWrapper className={`appflow-chat-sender ${className || ''}`} style={style}>
|
|
495
|
+
<Sender
|
|
496
|
+
value={inputValue}
|
|
497
|
+
onChange={setInputValue}
|
|
498
|
+
onSubmit={handleSubmit}
|
|
499
|
+
loading={loading}
|
|
500
|
+
disabled={disabled}
|
|
501
|
+
placeholder={isUploading ? '文件上传中...' : placeholder}
|
|
502
|
+
submitType={submitType}
|
|
503
|
+
onCancel={onCancel}
|
|
504
|
+
onPasteFile={hasUploadCapability ? handlePasteFile : undefined}
|
|
505
|
+
allowSpeech={hasAudioCapability}
|
|
506
|
+
header={renderHeader}
|
|
507
|
+
footer={renderFooter}
|
|
508
|
+
actions={renderActions}
|
|
509
|
+
autoSize={{ minRows: 1, maxRows: 6 }}
|
|
510
|
+
readOnly={isUploading}
|
|
511
|
+
/>
|
|
512
|
+
</SenderWrapper>
|
|
513
|
+
);
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
export default ChatSender;
|
|
@@ -11,8 +11,6 @@ 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';
|
|
16
14
|
import { BubbleContent } from '@/core';
|
|
17
15
|
|
|
18
16
|
/** HumanVerify 提交数据类型 */
|
|
@@ -60,21 +58,14 @@ export interface MessageBubbleProps {
|
|
|
60
58
|
|
|
61
59
|
// ==================== HumanVerify相关Props ====================
|
|
62
60
|
|
|
63
|
-
/** 事件类型(用于特殊消息如 humanVerify、historyCard
|
|
64
|
-
eventType?: 'humanVerify' | 'historyCard'
|
|
61
|
+
/** 事件类型(用于特殊消息如 humanVerify、historyCard) */
|
|
62
|
+
eventType?: 'humanVerify' | 'historyCard';
|
|
65
63
|
/** HumanVerify 相关数据 */
|
|
66
64
|
humanVerifyData?: HumanVerifyData;
|
|
67
65
|
/** HistoryCard 相关数据(历史对话中的审核卡片) */
|
|
68
66
|
historyCardData?: HistoryCardData;
|
|
69
67
|
/** HumanVerify 提交回调 */
|
|
70
68
|
onHumanVerifySubmit?: (data: HumanVerifySubmitData) => void;
|
|
71
|
-
|
|
72
|
-
// ==================== A2UI相关Props ====================
|
|
73
|
-
|
|
74
|
-
/** A2UI 消息数组(Agent 生成的声明式 UI 描述) */
|
|
75
|
-
a2uiMessages?: A2UIMessage[];
|
|
76
|
-
/** A2UI 用户交互回调 */
|
|
77
|
-
onA2UIAction?: (action: any) => void;
|
|
78
69
|
}
|
|
79
70
|
|
|
80
71
|
// 样式隔离容器
|
|
@@ -279,9 +270,6 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
279
270
|
humanVerifyData,
|
|
280
271
|
historyCardData,
|
|
281
272
|
onHumanVerifySubmit,
|
|
282
|
-
// A2UI 相关 props
|
|
283
|
-
a2uiMessages,
|
|
284
|
-
onA2UIAction,
|
|
285
273
|
}) => {
|
|
286
274
|
const [modal, contextHolder] = Modal.useModal();
|
|
287
275
|
|
|
@@ -374,14 +362,6 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
374
362
|
data={historyCardData}
|
|
375
363
|
/>
|
|
376
364
|
)}
|
|
377
|
-
|
|
378
|
-
{/* A2UI 事件 - Agent 声明式 UI 渲染 */}
|
|
379
|
-
{eventType === 'a2ui' && a2uiMessages && a2uiMessages.length > 0 && (
|
|
380
|
-
<A2UISurface
|
|
381
|
-
messages={a2uiMessages}
|
|
382
|
-
onAction={onA2UIAction}
|
|
383
|
-
/>
|
|
384
|
-
)}
|
|
385
365
|
|
|
386
366
|
{/* 参考资料 */}
|
|
387
367
|
{references && references.length > 0 && status === 'Success' && (
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export { MessageBubble } from './components/MessageBubble';
|
|
|
28
28
|
export { RichMessageBubble } from './components/RichMessageBubble';
|
|
29
29
|
export { DocReferences } from './components/DocReferences';
|
|
30
30
|
export { WebSearchPanel } from './components/WebSearchPanel';
|
|
31
|
+
export { ChatSender } from './components/ChatSender';
|
|
31
32
|
|
|
32
33
|
// ==================== UI 组件类型导出 ====================
|
|
33
34
|
export type { MarkdownRendererProps } from './components/MarkdownRenderer';
|
|
@@ -40,6 +41,7 @@ export type {
|
|
|
40
41
|
export type { RichMessageBubbleProps } from './components/RichMessageBubble';
|
|
41
42
|
export type { DocReferencesProps, DocReferenceItem } from './components/DocReferences';
|
|
42
43
|
export type { WebSearchPanelProps, WebSearchItem } from './components/WebSearchPanel';
|
|
44
|
+
export type { ChatSenderProps, ChatSenderSubmitData, ChatAttachment } from './components/ChatSender';
|
|
43
45
|
|
|
44
46
|
// ==================== Core 组件导出(纯展示组件,供高级定制) ====================
|
|
45
47
|
export { BubbleContent } from './core/BubbleContent';
|
|
@@ -76,12 +78,6 @@ export type {
|
|
|
76
78
|
ValidationError,
|
|
77
79
|
} from './components/HumanVerify';
|
|
78
80
|
|
|
79
|
-
// ==================== A2UI 组件导出 ====================
|
|
80
|
-
export { A2UISurface, A2UIStaticViewer } from './components/A2UIRenderer';
|
|
81
|
-
|
|
82
|
-
// ==================== A2UI 组件类型导出 ====================
|
|
83
|
-
export type { A2UISurfaceProps, A2UIStaticViewerProps, A2UIMessage } from './components/A2UIRenderer';
|
|
84
|
-
|
|
85
81
|
// ==================== 工具函数导出 ====================
|
|
86
82
|
export { loadEchartsScript } from './utils/loadEcharts';
|
|
87
83
|
export { loadMermaidScript } from './utils/loadMermaid';
|