@alicloud/appflow-chat 0.0.4-alpha.1 → 0.0.4-alpha.3

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.
@@ -0,0 +1,677 @@
1
+ /**
2
+ * ChatSender - 聊天输入框组件
3
+ */
4
+
5
+ import React, { useState, useRef, useCallback, useMemo, useEffect } from 'react';
6
+ import { Sender, Attachments } from '@ant-design/x';
7
+ import type { AttachmentsProps } from '@ant-design/x';
8
+ import { Select, Switch, Button, Tooltip } from 'antd';
9
+ import {
10
+ PaperClipOutlined,
11
+ PictureOutlined,
12
+ GlobalOutlined,
13
+ CloudUploadOutlined,
14
+ AudioOutlined,
15
+ } from '@ant-design/icons';
16
+ import styled from 'styled-components';
17
+ import type { ModelInfo, ModelCapabilities } from '../services/ChatService';
18
+
19
+ /** Attachments 组件的 ref 类型 */
20
+ interface AttachmentsRefType {
21
+ nativeElement: HTMLDivElement | null;
22
+ upload: (file: File) => void;
23
+ }
24
+
25
+ /** 附件文件项类型(兼容 antd Upload fileList 项) */
26
+ interface AttachmentFileItem {
27
+ uid: string;
28
+ name: string;
29
+ status?: 'uploading' | 'done' | 'error' | 'removed';
30
+ thumbUrl?: string;
31
+ url?: string;
32
+ }
33
+
34
+ // ==================== 类型定义 ====================
35
+
36
+ /** 附件信息(上传完成后携带下载URL) */
37
+ export interface ChatAttachment {
38
+ /** 文件唯一标识 */
39
+ uid: string;
40
+ /** 文件名 */
41
+ name: string;
42
+ /** 上传状态 */
43
+ status: 'uploading' | 'done' | 'error';
44
+ /** 文件类型:image 或 file */
45
+ type: 'image' | 'file';
46
+ /** 上传后的下载URL */
47
+ url?: string;
48
+ /** 本地预览URL(图片) */
49
+ thumbUrl?: string;
50
+ /** 原始文件对象 */
51
+ originFile?: File;
52
+ }
53
+
54
+ /** 提交时的消息数据 */
55
+ export interface ChatSenderSubmitData {
56
+ /** 文本内容 */
57
+ text: string;
58
+ /** 图片URL列表 */
59
+ images: string[];
60
+ /** 文件列表(包含文件名和URL) */
61
+ files: { name: string; url: string }[];
62
+ /** 语音文件URL(录音上传后的下载地址) */
63
+ audio?: string;
64
+ /** 选中的模型ID */
65
+ modelId?: string;
66
+ /** 是否启用联网搜索 */
67
+ webSearch: boolean;
68
+ }
69
+
70
+ export interface ChatSenderProps {
71
+ /** 是否处于加载状态(AI正在回复) */
72
+ loading?: boolean;
73
+ /** 是否禁用 */
74
+ disabled?: boolean;
75
+ /** 输入框占位文本 */
76
+ placeholder?: string;
77
+ /** 提交方式:'enter' 回车发送 | 'shiftEnter' Shift+回车发送 */
78
+ submitType?: 'enter' | 'shiftEnter';
79
+
80
+ // ==================== 模型相关 ====================
81
+
82
+ /** 可用模型列表,传入且长度>1时显示模型选择下拉框 */
83
+ models?: ModelInfo[];
84
+ /** 当前选中的模型ID */
85
+ modelId?: string;
86
+ /** 默认选中的模型ID(非受控) */
87
+ defaultModelId?: string;
88
+ /** 模型切换回调 */
89
+ onModelChange?: (modelId: string) => void;
90
+
91
+ // ==================== 能力配置 ====================
92
+
93
+ /**
94
+ * 模型能力配置,控制功能按钮的显隐
95
+ * 不传时默认所有功能关闭
96
+ */
97
+ capabilities?: ModelCapabilities;
98
+
99
+ // ==================== 事件回调 ====================
100
+
101
+ /** 提交消息回调 */
102
+ onSubmit?: (data: ChatSenderSubmitData) => void;
103
+ /** 取消当前请求 */
104
+ onCancel?: () => void;
105
+ /** 文件上传方法,返回下载URL */
106
+ onUpload?: (file: File) => Promise<string>;
107
+
108
+ // ==================== 样式 ====================
109
+
110
+ /** 自定义类名 */
111
+ className?: string;
112
+ /** 自定义样式 */
113
+ style?: React.CSSProperties;
114
+ }
115
+
116
+ // ==================== 样式组件 ====================
117
+
118
+ const SenderWrapper = styled.div`
119
+ .appflow-chat-sender-footer {
120
+ display: flex;
121
+ align-items: center;
122
+ justify-content: space-between;
123
+ padding: 4px 0;
124
+ }
125
+
126
+ .appflow-chat-sender-footer-left {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 4px;
130
+ }
131
+
132
+ .appflow-chat-sender-footer-right {
133
+ display: flex;
134
+ align-items: center;
135
+ gap: 4px;
136
+ }
137
+
138
+ .appflow-chat-sender-model-select {
139
+ min-width: 100px;
140
+ }
141
+
142
+ .appflow-chat-sender-separator {
143
+ width: 1px;
144
+ height: 16px;
145
+ background: #e0e0e0;
146
+ margin: 0 2px;
147
+ }
148
+
149
+ .appflow-chat-sender-web-search {
150
+ display: inline-flex;
151
+ align-items: center;
152
+ gap: 4px;
153
+ font-size: 13px;
154
+ color: #666;
155
+ padding: 2px 6px;
156
+ height: 28px;
157
+ border-radius: 6px;
158
+ cursor: pointer;
159
+ transition: background 0.2s;
160
+ user-select: none;
161
+
162
+ &:hover {
163
+ background: rgba(0, 0, 0, 0.04);
164
+ }
165
+ }
166
+ `;
167
+
168
+ // ==================== 录音中波形图标 ====================
169
+
170
+ /** 录音中的波形动画图标(复用 @ant-design/x 的 RecordingIcon 设计) */
171
+ const RecordingIcon: React.FC = () => {
172
+ const SIZE = 1000;
173
+ const COUNT = 4;
174
+ const RECT_WIDTH = 140;
175
+ const RECT_RADIUS = RECT_WIDTH / 2;
176
+ const RECT_HEIGHT_MIN = 250;
177
+ const RECT_HEIGHT_MAX = 500;
178
+ const DURATION = 0.8;
179
+
180
+ return (
181
+ <svg
182
+ width="1em"
183
+ height="1em"
184
+ viewBox={`0 0 ${SIZE} ${SIZE}`}
185
+ xmlns="http://www.w3.org/2000/svg"
186
+ fill="currentColor"
187
+ >
188
+ {Array.from({ length: COUNT }).map((_, index) => {
189
+ const dest = (SIZE - RECT_WIDTH * COUNT) / (COUNT - 1);
190
+ const x = index * (dest + RECT_WIDTH);
191
+ const yMin = SIZE / 2 - RECT_HEIGHT_MIN / 2;
192
+ const yMax = SIZE / 2 - RECT_HEIGHT_MAX / 2;
193
+ return (
194
+ <rect
195
+ key={index}
196
+ fill="currentColor"
197
+ rx={RECT_RADIUS}
198
+ ry={RECT_RADIUS}
199
+ height={RECT_HEIGHT_MIN}
200
+ width={RECT_WIDTH}
201
+ x={x}
202
+ y={yMin}
203
+ >
204
+ <animate
205
+ attributeName="height"
206
+ values={`${RECT_HEIGHT_MIN}; ${RECT_HEIGHT_MAX}; ${RECT_HEIGHT_MIN}`}
207
+ keyTimes="0; 0.5; 1"
208
+ dur={`${DURATION}s`}
209
+ begin={`${(DURATION / COUNT) * index}s`}
210
+ repeatCount="indefinite"
211
+ />
212
+ <animate
213
+ attributeName="y"
214
+ values={`${yMin}; ${yMax}; ${yMin}`}
215
+ keyTimes="0; 0.5; 1"
216
+ dur={`${DURATION}s`}
217
+ begin={`${(DURATION / COUNT) * index}s`}
218
+ repeatCount="indefinite"
219
+ />
220
+ </rect>
221
+ );
222
+ })}
223
+ </svg>
224
+ );
225
+ };
226
+
227
+ // ==================== 工具函数 ====================
228
+
229
+ function generateUid(): string {
230
+ return `file_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
231
+ }
232
+
233
+ function isImageFile(file: File): boolean {
234
+ return file.type.startsWith('image/');
235
+ }
236
+
237
+ // ==================== 组件实现 ====================
238
+
239
+ /**
240
+ * ChatSender - 聊天输入框组件
241
+ *
242
+ * 集成了文本输入、文件/图片上传、语音输入、联网搜索、模型选择等功能。
243
+ * 根据模型能力自动控制功能按钮的显隐。
244
+ *
245
+ * @example
246
+ * ```tsx
247
+ * <ChatSender
248
+ * loading={isLoading}
249
+ * models={config.models}
250
+ * capabilities={chatService.getModelCapabilities(modelId)}
251
+ * onSubmit={({ text, images, files, modelId, webSearch }) => {
252
+ * chatService.chat({ text, images, files, modelId, webSearch });
253
+ * }}
254
+ * onCancel={() => chatService.cancel()}
255
+ * onClear={() => chatService.clear()}
256
+ * onUpload={(file) => chatService.upload(file)}
257
+ * />
258
+ * ```
259
+ */
260
+ export const ChatSender: React.FC<ChatSenderProps> = ({
261
+ loading = false,
262
+ disabled = false,
263
+ placeholder = '',
264
+ submitType = 'enter',
265
+ models = [],
266
+ modelId: controlledModelId,
267
+ defaultModelId,
268
+ onModelChange,
269
+ capabilities,
270
+ onSubmit,
271
+ onCancel,
272
+ onUpload,
273
+ className,
274
+ style,
275
+ }) => {
276
+ // ==================== 状态管理 ====================
277
+
278
+ const [inputValue, setInputValue] = useState('');
279
+ const [internalModelId, setInternalModelId] = useState<string | undefined>(
280
+ defaultModelId || models[0]?.id
281
+ );
282
+ const [webSearchEnabled, setWebSearchEnabled] = useState(false);
283
+ const [speechRecording, setSpeechRecording] = useState(false);
284
+ const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
285
+ const [headerOpen, setHeaderOpen] = useState(false);
286
+
287
+ const attachmentsRef = useRef<AttachmentsRefType>(null);
288
+ const mediaRecorderRef = useRef<MediaRecorder | null>(null);
289
+ const audioChunksRef = useRef<Blob[]>([]);
290
+ const mediaStreamRef = useRef<MediaStream | null>(null);
291
+ const speechRecordingRef = useRef(false);
292
+
293
+ // 受控/非受控模型ID
294
+ const currentModelId = controlledModelId ?? internalModelId;
295
+
296
+ // ==================== 能力判断 ====================
297
+
298
+ const hasImageCapability = capabilities?.image ?? false;
299
+ const hasFileCapability = capabilities?.file ?? false;
300
+ const hasAudioCapability = capabilities?.audio ?? false;
301
+ const hasWebSearchCapability = capabilities?.webSearch ?? false;
302
+
303
+ const hasUploadCapability = hasImageCapability || hasFileCapability;
304
+ const hasAttachments = attachments.length > 0;
305
+ const uploadingCount = attachments.filter(a => a.status === 'uploading').length;
306
+ const isUploading = uploadingCount > 0;
307
+
308
+ // ==================== 文件上传处理 ====================
309
+
310
+ const handleFileUpload = useCallback(async (file: File) => {
311
+ if (!onUpload) return;
312
+
313
+ const uid = generateUid();
314
+ const fileType = isImageFile(file) ? 'image' : 'file';
315
+
316
+ // 检查能力限制
317
+ if (fileType === 'image' && !hasImageCapability) return;
318
+ if (fileType === 'file' && !hasFileCapability) return;
319
+
320
+ const newAttachment: ChatAttachment = {
321
+ uid,
322
+ name: file.name,
323
+ status: 'uploading',
324
+ type: fileType,
325
+ thumbUrl: fileType === 'image' ? URL.createObjectURL(file) : undefined,
326
+ originFile: file,
327
+ };
328
+
329
+ setAttachments(prev => [...prev, newAttachment]);
330
+ setHeaderOpen(true);
331
+
332
+ try {
333
+ const downloadUrl = await onUpload(file);
334
+ setAttachments(prev =>
335
+ prev.map(a => a.uid === uid ? { ...a, status: 'done' as const, url: downloadUrl } : a)
336
+ );
337
+ } catch {
338
+ setAttachments(prev =>
339
+ prev.map(a => a.uid === uid ? { ...a, status: 'error' as const } : a)
340
+ );
341
+ }
342
+ }, [onUpload, hasImageCapability, hasFileCapability]);
343
+
344
+ // ==================== 提交处理 ====================
345
+
346
+ const handleSubmit = useCallback((text: string) => {
347
+ if (!text.trim() && !hasAttachments) return;
348
+ if (isUploading) return;
349
+
350
+ const images = attachments
351
+ .filter(a => a.type === 'image' && a.status === 'done' && a.url)
352
+ .map(a => a.url!);
353
+
354
+ const files = attachments
355
+ .filter(a => a.type === 'file' && a.status === 'done' && a.url)
356
+ .map(a => ({ name: a.name, url: a.url! }));
357
+
358
+ onSubmit?.({
359
+ text: text.trim(),
360
+ images,
361
+ files,
362
+ audio: undefined,
363
+ modelId: currentModelId,
364
+ webSearch: webSearchEnabled,
365
+ });
366
+
367
+ // 清空状态
368
+ setInputValue('');
369
+ setAttachments([]);
370
+ setHeaderOpen(false);
371
+ }, [attachments, hasAttachments, isUploading, currentModelId, webSearchEnabled, onSubmit]);
372
+
373
+ // ==================== 模型切换 ====================
374
+
375
+ const handleModelChange = useCallback((value: string) => {
376
+ setInternalModelId(value);
377
+ onModelChange?.(value);
378
+ }, [onModelChange]);
379
+
380
+ // ==================== 粘贴文件 ====================
381
+
382
+ const handlePasteFile = useCallback((firstFile: File) => {
383
+ handleFileUpload(firstFile);
384
+ }, [handleFileUpload]);
385
+
386
+ // ==================== Attachments 配置 ====================
387
+
388
+ const attachmentItems: AttachmentFileItem[] = useMemo(() => {
389
+ return attachments.map(a => ({
390
+ uid: a.uid,
391
+ name: a.name,
392
+ status: a.status === 'done' ? 'done' as const : a.status === 'error' ? 'error' as const : 'uploading' as const,
393
+ thumbUrl: a.thumbUrl,
394
+ url: a.url,
395
+ }));
396
+ }, [attachments]);
397
+
398
+ // 文件类型限制
399
+ const acceptTypes = useMemo(() => {
400
+ const types: string[] = [];
401
+ if (hasImageCapability) {
402
+ types.push('image/*');
403
+ }
404
+ if (hasFileCapability) {
405
+ const fileConfig = capabilities?.fileConfig;
406
+ if (fileConfig?.supportFileTypes?.length) {
407
+ types.push(...fileConfig.supportFileTypes.map(t => `.${t}`));
408
+ } else {
409
+ types.push('.pdf', '.doc', '.docx', '.txt', '.csv', '.xlsx', '.xls', '.md', '.json');
410
+ }
411
+ }
412
+ return types.join(',');
413
+ }, [hasImageCapability, hasFileCapability, capabilities?.fileConfig]);
414
+
415
+ // ==================== 渲染:Header(附件区域) ====================
416
+
417
+ const renderHeader = useMemo(() => {
418
+ if (!hasUploadCapability) return undefined;
419
+
420
+ return (
421
+ <Sender.Header
422
+ title="附件"
423
+ open={headerOpen}
424
+ onOpenChange={setHeaderOpen}
425
+ closable
426
+ styles={{
427
+ content: { padding: 4 },
428
+ }}
429
+ >
430
+ <Attachments
431
+ ref={attachmentsRef as any}
432
+ items={attachmentItems as AttachmentsProps['items']}
433
+ accept={acceptTypes}
434
+ multiple
435
+ customRequest={({ file }) => {
436
+ if (file instanceof File) {
437
+ handleFileUpload(file);
438
+ }
439
+ }}
440
+ onRemove={(file) => {
441
+ setAttachments(prev => {
442
+ const updated = prev.filter(a => a.uid !== file.uid);
443
+ if (updated.length === 0) {
444
+ setHeaderOpen(false);
445
+ }
446
+ return updated;
447
+ });
448
+ }}
449
+ placeholder={{
450
+ icon: <CloudUploadOutlined />,
451
+ title: '拖拽文件到此处',
452
+ description: '支持图片和文件',
453
+ }}
454
+ />
455
+ </Sender.Header>
456
+ );
457
+ }, [hasUploadCapability, headerOpen, attachmentItems]);
458
+
459
+ // ==================== 触发文件选择 ====================
460
+
461
+ const triggerFileSelect = useCallback(() => {
462
+ const input = document.createElement('input');
463
+ input.type = 'file';
464
+ input.accept = acceptTypes;
465
+ input.multiple = true;
466
+ input.onchange = (event) => {
467
+ const fileList = (event.target as HTMLInputElement).files;
468
+ if (fileList) {
469
+ Array.from(fileList).forEach(handleFileUpload);
470
+ }
471
+ };
472
+ input.click();
473
+ }, [acceptTypes, handleFileUpload]);
474
+
475
+ // ==================== 语音录音管理 ====================
476
+ // 使用 MediaRecorder 录制音频文件,录音结束后上传并发送
477
+
478
+ const stopMediaStream = useCallback(() => {
479
+ mediaStreamRef.current?.getTracks().forEach(track => track.stop());
480
+ mediaStreamRef.current = null;
481
+ }, []);
482
+
483
+ const toggleSpeechRecording = useCallback(async () => {
484
+ if (speechRecordingRef.current) {
485
+ // 停止录音 — MediaRecorder.onstop 中会处理上传和发送
486
+ mediaRecorderRef.current?.stop();
487
+ speechRecordingRef.current = false;
488
+ setSpeechRecording(false);
489
+ } else {
490
+ // 开始录音
491
+ try {
492
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
493
+ mediaStreamRef.current = stream;
494
+ audioChunksRef.current = [];
495
+
496
+ const mediaRecorder = new MediaRecorder(stream);
497
+
498
+ mediaRecorder.ondataavailable = (event) => {
499
+ if (event.data.size > 0) {
500
+ audioChunksRef.current.push(event.data);
501
+ }
502
+ };
503
+
504
+ mediaRecorder.onstop = async () => {
505
+ // 停止麦克风流
506
+ stopMediaStream();
507
+
508
+ const chunks = audioChunksRef.current;
509
+ if (chunks.length === 0 || !onUpload) return;
510
+
511
+ // 合并音频 chunks 为 Blob
512
+ const audioBlob = new Blob(chunks, { type: 'audio/webm' });
513
+ const audioFile = new File(
514
+ [audioBlob],
515
+ `recording_${Date.now()}.webm`,
516
+ { type: 'audio/webm' }
517
+ );
518
+
519
+ try {
520
+ // 上传音频文件
521
+ const audioUrl = await onUpload(audioFile);
522
+
523
+ // 发送语音消息(audio 优先级最高,服务端会忽略 text/images/files)
524
+ onSubmit?.({
525
+ text: '',
526
+ images: [],
527
+ files: [],
528
+ audio: audioUrl,
529
+ modelId: currentModelId,
530
+ webSearch: false,
531
+ });
532
+ } catch (err) {
533
+ console.error('语音上传失败:', err);
534
+ }
535
+ };
536
+
537
+ mediaRecorder.onerror = () => {
538
+ speechRecordingRef.current = false;
539
+ setSpeechRecording(false);
540
+ stopMediaStream();
541
+ };
542
+
543
+ mediaRecorderRef.current = mediaRecorder;
544
+ mediaRecorder.start();
545
+ speechRecordingRef.current = true;
546
+ setSpeechRecording(true);
547
+ } catch (err) {
548
+ console.error('无法获取麦克风权限:', err);
549
+ speechRecordingRef.current = false;
550
+ setSpeechRecording(false);
551
+ }
552
+ }
553
+ }, [onUpload, onSubmit, inputValue, currentModelId, webSearchEnabled, stopMediaStream]);
554
+
555
+ // 组件卸载时停止录音和麦克风流
556
+ useEffect(() => {
557
+ return () => {
558
+ mediaRecorderRef.current?.stop();
559
+ stopMediaStream();
560
+ };
561
+ }, [stopMediaStream]);
562
+
563
+ // ==================== 渲染:Footer(功能栏) ====================
564
+ // 布局:左侧 [模型选择] [联网搜索] 右侧 [文件上传] [语音] [发送/停止]
565
+
566
+ const renderFooter = useCallback((info: any) => {
567
+ const { SendButton, LoadingButton } = info.components;
568
+
569
+ return (
570
+ <div className="appflow-chat-sender-footer">
571
+ <div className="appflow-chat-sender-footer-left">
572
+ {/* 模型选择 */}
573
+ {models.length > 1 && (
574
+ <Select
575
+ className="appflow-chat-sender-model-select"
576
+ size="small"
577
+ value={currentModelId}
578
+ onChange={handleModelChange}
579
+ options={models.map(m => ({ label: m.name, value: m.id }))}
580
+ variant="borderless"
581
+ popupMatchSelectWidth={false}
582
+ />
583
+ )}
584
+
585
+ {/* 联网搜索开关 */}
586
+ {hasWebSearchCapability && (
587
+ <div
588
+ className="appflow-chat-sender-web-search"
589
+ onClick={() => setWebSearchEnabled(prev => !prev)}
590
+ >
591
+ <GlobalOutlined />
592
+ <Switch
593
+ size="small"
594
+ checked={webSearchEnabled}
595
+ onChange={setWebSearchEnabled}
596
+ />
597
+ <span>联网搜索</span>
598
+ </div>
599
+ )}
600
+ </div>
601
+
602
+ <div className="appflow-chat-sender-footer-right">
603
+ {/* 文件上传按钮 */}
604
+ {hasUploadCapability && (
605
+ <Tooltip title="上传文件">
606
+ <Button
607
+ type="text"
608
+ size="small"
609
+ icon={hasImageCapability && !hasFileCapability ? <PictureOutlined /> : <PaperClipOutlined />}
610
+ disabled={disabled || loading}
611
+ onClick={triggerFileSelect}
612
+ />
613
+ </Tooltip>
614
+ )}
615
+
616
+ {/* 分隔线 */}
617
+ {hasUploadCapability && hasAudioCapability && (
618
+ <div className="appflow-chat-sender-separator" />
619
+ )}
620
+
621
+ {/* 语音按钮 */}
622
+ {hasAudioCapability && (
623
+ <Tooltip title={speechRecording ? '停止录音' : '语音输入'}>
624
+ <Button
625
+ type="text"
626
+ size="small"
627
+ icon={speechRecording ? <RecordingIcon /> : <AudioOutlined />}
628
+ disabled={disabled}
629
+ onClick={toggleSpeechRecording}
630
+ />
631
+ </Tooltip>
632
+ )}
633
+
634
+ {/* 发送/停止按钮 */}
635
+ {loading ? (
636
+ <LoadingButton />
637
+ ) : (
638
+ <SendButton disabled={disabled || isUploading || (!inputValue.trim() && !hasAttachments)} />
639
+ )}
640
+ </div>
641
+ </div>
642
+ );
643
+ }, [
644
+ hasUploadCapability, hasImageCapability, hasFileCapability,
645
+ hasAttachments, attachments.length, triggerFileSelect,
646
+ models, currentModelId, handleModelChange,
647
+ hasWebSearchCapability, webSearchEnabled,
648
+ hasAudioCapability, speechRecording, toggleSpeechRecording,
649
+ disabled, loading, isUploading, inputValue,
650
+ ]);
651
+
652
+ // ==================== 主渲染 ====================
653
+
654
+ return (
655
+ <SenderWrapper className={`appflow-chat-sender ${className || ''}`} style={style}>
656
+ <Sender
657
+ value={inputValue}
658
+ onChange={setInputValue}
659
+ onSubmit={handleSubmit}
660
+ loading={loading}
661
+ disabled={disabled}
662
+ placeholder={isUploading ? '文件上传中...' : placeholder}
663
+ submitType={submitType}
664
+ onCancel={onCancel}
665
+ onPasteFile={hasUploadCapability ? handlePasteFile : undefined}
666
+ allowSpeech={false}
667
+ header={renderHeader}
668
+ footer={renderFooter}
669
+ actions={false}
670
+ autoSize={{ minRows: 1, maxRows: 6 }}
671
+ readOnly={isUploading}
672
+ />
673
+ </SenderWrapper>
674
+ );
675
+ };
676
+
677
+ export default ChatSender;