@huyooo/ai-chat-frontend-react 0.2.14 → 0.2.16

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.
Files changed (76) hide show
  1. package/dist/index.css +0 -1
  2. package/dist/index.js +1 -5418
  3. package/package.json +4 -5
  4. package/dist/index.css.map +0 -1
  5. package/dist/index.js.map +0 -1
  6. package/src/adapter.ts +0 -68
  7. package/src/components/ChatPanel.tsx +0 -553
  8. package/src/components/common/ConfirmDialog.css +0 -136
  9. package/src/components/common/ConfirmDialog.tsx +0 -91
  10. package/src/components/common/CopyButton.css +0 -22
  11. package/src/components/common/CopyButton.tsx +0 -46
  12. package/src/components/common/IndexingSettings.css +0 -207
  13. package/src/components/common/IndexingSettings.tsx +0 -398
  14. package/src/components/common/SettingsPanel.css +0 -337
  15. package/src/components/common/SettingsPanel.tsx +0 -215
  16. package/src/components/common/Toast.css +0 -50
  17. package/src/components/common/Toast.tsx +0 -38
  18. package/src/components/common/ToggleSwitch.css +0 -52
  19. package/src/components/common/ToggleSwitch.tsx +0 -20
  20. package/src/components/header/ChatHeader.css +0 -285
  21. package/src/components/header/ChatHeader.tsx +0 -376
  22. package/src/components/input/AtFilePicker.css +0 -147
  23. package/src/components/input/AtFilePicker.tsx +0 -519
  24. package/src/components/input/ChatInput.css +0 -283
  25. package/src/components/input/ChatInput.tsx +0 -575
  26. package/src/components/input/DropdownSelector.css +0 -231
  27. package/src/components/input/DropdownSelector.tsx +0 -333
  28. package/src/components/input/ImagePreviewModal.css +0 -124
  29. package/src/components/input/ImagePreviewModal.tsx +0 -118
  30. package/src/components/input/at-views/AtBranchView.tsx +0 -34
  31. package/src/components/input/at-views/AtBrowserView.tsx +0 -34
  32. package/src/components/input/at-views/AtChatsView.tsx +0 -34
  33. package/src/components/input/at-views/AtDocsView.tsx +0 -34
  34. package/src/components/input/at-views/AtFilesView.tsx +0 -168
  35. package/src/components/input/at-views/AtTerminalsView.tsx +0 -34
  36. package/src/components/input/at-views/AtViewStyles.css +0 -143
  37. package/src/components/input/at-views/index.ts +0 -9
  38. package/src/components/message/ContentRenderer.css +0 -9
  39. package/src/components/message/MessageBubble.css +0 -193
  40. package/src/components/message/MessageBubble.tsx +0 -240
  41. package/src/components/message/PartsRenderer.css +0 -12
  42. package/src/components/message/PartsRenderer.tsx +0 -168
  43. package/src/components/message/WelcomeMessage.css +0 -221
  44. package/src/components/message/WelcomeMessage.tsx +0 -93
  45. package/src/components/message/parts/CollapsibleCard.css +0 -80
  46. package/src/components/message/parts/CollapsibleCard.tsx +0 -80
  47. package/src/components/message/parts/ErrorPart.css +0 -9
  48. package/src/components/message/parts/ErrorPart.tsx +0 -40
  49. package/src/components/message/parts/ImagePart.css +0 -49
  50. package/src/components/message/parts/ImagePart.tsx +0 -54
  51. package/src/components/message/parts/SearchPart.css +0 -44
  52. package/src/components/message/parts/SearchPart.tsx +0 -63
  53. package/src/components/message/parts/TextPart.css +0 -579
  54. package/src/components/message/parts/TextPart.tsx +0 -213
  55. package/src/components/message/parts/ThinkingPart.css +0 -9
  56. package/src/components/message/parts/ThinkingPart.tsx +0 -48
  57. package/src/components/message/parts/ToolCallPart.css +0 -246
  58. package/src/components/message/parts/ToolCallPart.tsx +0 -289
  59. package/src/components/message/parts/ToolResultPart.css +0 -67
  60. package/src/components/message/parts/index.ts +0 -13
  61. package/src/components/message/parts/visual-predicate.ts +0 -43
  62. package/src/components/message/parts/visual-render.ts +0 -19
  63. package/src/components/message/parts/visual.ts +0 -12
  64. package/src/components/message/welcome-types.ts +0 -46
  65. package/src/context/AutoRunConfigContext.tsx +0 -13
  66. package/src/context/ChatAdapterContext.tsx +0 -8
  67. package/src/context/ChatInputContext.tsx +0 -40
  68. package/src/context/RenderersContext.tsx +0 -35
  69. package/src/hooks/useChat.ts +0 -1569
  70. package/src/hooks/useImageUpload.ts +0 -345
  71. package/src/hooks/useVoiceInput.ts +0 -454
  72. package/src/hooks/useVoiceToTextInput.ts +0 -87
  73. package/src/index.ts +0 -151
  74. package/src/styles.css +0 -330
  75. package/src/types/index.ts +0 -196
  76. package/src/utils/fileIcon.ts +0 -49
@@ -1,575 +0,0 @@
1
- /**
2
- * ChatInput Component
3
- * 与 Vue 版本 ChatInput.vue 保持一致
4
- */
5
-
6
- import { useState, useRef, useCallback, useEffect, forwardRef, useImperativeHandle, useLayoutEffect, useMemo } from 'react';
7
- import './ChatInput.css';
8
- import { Icon } from '@iconify/react';
9
- import type { ChatMode, ModelOption } from '../../types';
10
- import { AtFilePicker } from './AtFilePicker';
11
- import { ImagePreviewModal } from './ImagePreviewModal';
12
- import { DropdownSelector } from './DropdownSelector';
13
- import type { DropdownOption } from './DropdownSelector';
14
- import { useChatInputContext } from '../../context/ChatInputContext';
15
- import { useImageUpload } from '../../hooks/useImageUpload';
16
- import { useVoiceToTextInput } from '../../hooks/useVoiceToTextInput';
17
- import type { ImageData } from '../../adapter';
18
-
19
- /** ChatInput 暴露给外部的方法 */
20
- export interface ChatInputHandle {
21
- /** 设置输入框内容 */
22
- setText: (text: string) => void;
23
- /** 聚焦输入框 */
24
- focus: () => void;
25
- /** 清空输入框 */
26
- clear: () => void;
27
- /** 在光标位置插入文本(用于 @ 上下文) */
28
- insertText: (text: string) => void;
29
- /** 添加图片 */
30
- addImages: (files: File[]) => void;
31
- }
32
-
33
- interface ChatInputProps {
34
- /** 变体模式:input-底部输入框,message-历史消息 */
35
- variant?: 'input' | 'message';
36
- /** 受控值(用于历史消息编辑) */
37
- value?: string;
38
- /** 图片数据(用于历史消息编辑) */
39
- images?: string[];
40
- isLoading?: boolean;
41
- mode?: ChatMode;
42
- model?: string;
43
- /** 模型列表(tooltip 由后端下发,前端仅透传渲染) */
44
- models?: ModelOption[];
45
- webSearchEnabled?: boolean;
46
- thinkingEnabled?: boolean;
47
- onSend?: (text: string, images?: ImageData[]) => void;
48
- onCancel?: () => void;
49
- onAtContext?: () => void;
50
- onModeChange?: (mode: ChatMode) => void;
51
- onModelChange?: (model: string) => void;
52
- onWebSearchChange?: (enabled: boolean) => void;
53
- onThinkingChange?: (enabled: boolean) => void;
54
- }
55
-
56
- /** 模式配置 */
57
- const MODE_OPTIONS: DropdownOption[] = [
58
- { value: 'agent', label: 'Agent', icon: 'lucide:zap' },
59
- { value: 'ask', label: 'Ask', icon: 'lucide:message-circle' },
60
- ];
61
-
62
- export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
63
- (
64
- {
65
- variant = 'input',
66
- value = '',
67
- images = [],
68
- isLoading = false,
69
- mode = 'agent',
70
- model = '',
71
- models = [],
72
- webSearchEnabled = false,
73
- thinkingEnabled = false,
74
- onSend,
75
- onCancel,
76
- onModeChange,
77
- onModelChange,
78
- onWebSearchChange,
79
- onThinkingChange,
80
- },
81
- ref
82
- ) => {
83
- const isMessageVariant = variant === 'message';
84
-
85
- const inputContext = useChatInputContext();
86
- const adapter = inputContext?.adapter;
87
- // cwd 已解耦,不再通过 context 传递
88
-
89
- const [inputText, setInputText] = useState(value);
90
- const [isFocused, setIsFocused] = useState(false);
91
-
92
- // 图片上传(使用 hook)
93
- const imageUpload = useImageUpload();
94
- const imageInputRef = useRef<HTMLInputElement>(null);
95
-
96
- const inputRef = useRef<HTMLTextAreaElement>(null);
97
- const containerRef = useRef<HTMLDivElement>(null);
98
- const atSelectorRef = useRef<HTMLDivElement>(null);
99
-
100
- // @ 文件选择面板
101
- const [atPickerVisible, setAtPickerVisible] = useState(false);
102
- const replaceRangeRef = useRef<{ start: number; end: number } | null>(null);
103
- const pendingCaretRef = useRef<number | null>(null);
104
- const pendingFocusRef = useRef(false);
105
- const prevValueRef = useRef(value);
106
-
107
- // 触发文件选择
108
- const triggerImageUpload = useCallback(() => {
109
- imageInputRef.current?.click();
110
- }, []);
111
-
112
- // 模型选项
113
- // 模型选项:tooltip/能力由后端(ai-chat-core)统一提供,前端只负责渲染
114
- const modelOptions = useMemo<DropdownOption[]>(
115
- () =>
116
- models.map((m) => ({
117
- value: m.modelId,
118
- label: m.displayName,
119
- tooltip: m.tooltip,
120
- })),
121
- [models]
122
- );
123
-
124
- // 当前模型是否支持深度思考
125
- const currentModelSupportsThinking = useMemo(() => {
126
- const currentModel = models.find(m => m.modelId === model);
127
- return currentModel?.supportsThinking ?? false;
128
- }, [models, model]);
129
-
130
- const closeAtPicker = useCallback(() => {
131
- setAtPickerVisible(false);
132
- replaceRangeRef.current = null;
133
- pendingFocusRef.current = true;
134
- }, []);
135
-
136
- const applyAtPath = useCallback(
137
- (path: string) => {
138
- const el = inputRef.current;
139
- const current = el?.value ?? inputText;
140
- const insert = `@${path}\n`;
141
- const range = replaceRangeRef.current;
142
-
143
- let nextText = current;
144
- let nextCaret = 0;
145
-
146
- if (range && current.slice(range.start, range.end) === '@') {
147
- nextText = current.slice(0, range.start) + insert + current.slice(range.end);
148
- nextCaret = range.start + insert.length;
149
- } else if (el) {
150
- const start = el.selectionStart ?? current.length;
151
- const end = el.selectionEnd ?? start;
152
- nextText = current.slice(0, start) + insert + current.slice(end);
153
- nextCaret = start + insert.length;
154
- } else {
155
- nextText = current + insert;
156
- nextCaret = nextText.length;
157
- }
158
-
159
- setInputText(nextText);
160
- pendingCaretRef.current = nextCaret;
161
- pendingFocusRef.current = true;
162
-
163
- closeAtPicker();
164
- },
165
- [closeAtPicker, inputText]
166
- );
167
-
168
- // 切换 @ 选择器
169
- const toggleAtPicker = useCallback(() => {
170
- if (!adapter) return;
171
- if (!atPickerVisible) {
172
- replaceRangeRef.current = null;
173
- }
174
- setAtPickerVisible((prev) => !prev);
175
- }, [adapter, atPickerVisible]);
176
-
177
- // 同步外部 value
178
- useEffect(() => {
179
- setInputText(value);
180
- }, [value]);
181
-
182
- // 同步外部 images(用于历史消息编辑)
183
- // 使用 JSON.stringify 比较,避免空数组引用变化导致无限循环
184
- const imagesKey = JSON.stringify(images);
185
- useEffect(() => {
186
- if (images?.length) {
187
- imageUpload.initImages(images);
188
- } else {
189
- imageUpload.clearImages();
190
- }
191
- // eslint-disable-next-line react-hooks/exhaustive-deps
192
- }, [imagesKey]);
193
-
194
- // 是否显示工具栏
195
- const showToolbar = !isMessageVariant || isFocused;
196
-
197
- // 占位符
198
- const placeholder = mode === 'ask' ? '有什么问题想问我?' : '描述任务,@ 添加上下文';
199
-
200
- // 是否有内容可发送
201
- const hasContent = useMemo(() => {
202
- return inputText.trim() || imageUpload.hasImages;
203
- }, [inputText, imageUpload.hasImages]);
204
-
205
- const voiceCtl = useVoiceToTextInput({
206
- adapter,
207
- inputText,
208
- setInputText,
209
- hasImages: imageUpload.hasImages,
210
- isLoading,
211
- });
212
- const voiceInput = voiceCtl.voiceInput;
213
- const toggleVoiceInput = useCallback(async () => {
214
- await voiceCtl.toggleVoice();
215
- // 停止/取消后聚焦输入框
216
- if (voiceInput.status === 'recording' || voiceInput.status === 'connecting') {
217
- pendingFocusRef.current = true;
218
- pendingCaretRef.current = null;
219
- }
220
- }, [voiceCtl, voiceInput.status]);
221
-
222
- // 自动调整高度
223
- const adjustTextareaHeight = useCallback(() => {
224
- if (inputRef.current) {
225
- inputRef.current.style.height = 'auto';
226
- const scrollHeight = inputRef.current.scrollHeight;
227
- inputRef.current.style.height = `${Math.min(scrollHeight, 150)}px`;
228
- }
229
- }, []);
230
-
231
- // 统一在 DOM commit 后调整高度/光标
232
- useLayoutEffect(() => {
233
- adjustTextareaHeight();
234
- const el = inputRef.current;
235
- if (!el) return;
236
-
237
- if (pendingFocusRef.current) {
238
- el.focus();
239
- pendingFocusRef.current = false;
240
- }
241
- if (pendingCaretRef.current !== null) {
242
- const pos = pendingCaretRef.current;
243
- pendingCaretRef.current = null;
244
- el.setSelectionRange(pos, pos);
245
- }
246
- }, [inputText, adjustTextareaHeight]);
247
-
248
- // 暴露给外部的方法
249
- useImperativeHandle(
250
- ref,
251
- () => ({
252
- setText: (text: string) => {
253
- setInputText(text);
254
- pendingCaretRef.current = text.length;
255
- pendingFocusRef.current = false;
256
- },
257
- focus: () => {
258
- inputRef.current?.focus();
259
- },
260
- clear: () => {
261
- setInputText('');
262
- imageUpload.clearImages();
263
- // 重置语音输入状态
264
- if (voiceInput.status !== 'idle') {
265
- voiceInput.cancel();
266
- }
267
- // 重置 @ 选择器
268
- setAtPickerVisible(false);
269
- replaceRangeRef.current = null;
270
- // 重置输入框高度
271
- if (inputRef.current) {
272
- inputRef.current.style.height = 'auto';
273
- }
274
- pendingCaretRef.current = 0;
275
- },
276
- insertText: (text: string) => {
277
- if (!text) return;
278
- const el = inputRef.current;
279
- const current = inputText;
280
- if (!el) {
281
- setInputText(current + text);
282
- pendingCaretRef.current = (current + text).length;
283
- return;
284
- }
285
- const start = el.selectionStart ?? current.length;
286
- const end = el.selectionEnd ?? start;
287
- const next = current.slice(0, start) + text + current.slice(end);
288
- setInputText(next);
289
- pendingCaretRef.current = start + text.length;
290
- pendingFocusRef.current = true;
291
- },
292
- addImages: (files: File[]) => {
293
- imageUpload.addImages(files);
294
- },
295
- }),
296
- [inputText, imageUpload]
297
- );
298
-
299
- // 发送或取消
300
- const handleSendOrCancel = useCallback(() => {
301
- if (isLoading) {
302
- onCancel?.();
303
- return;
304
- }
305
-
306
- const text = inputText.trim();
307
- if (!text && !imageUpload.hasImages) return;
308
-
309
- // 获取图片数据
310
- const images = imageUpload.imageData.length > 0 ? imageUpload.imageData : undefined;
311
-
312
- onSend?.(text, images);
313
-
314
- if (!isMessageVariant) {
315
- setInputText('');
316
- imageUpload.clearImages();
317
- if (inputRef.current) {
318
- inputRef.current.style.height = 'auto';
319
- }
320
- pendingCaretRef.current = 0;
321
- pendingFocusRef.current = true;
322
- } else {
323
- setIsFocused(false);
324
- }
325
- }, [isLoading, inputText, imageUpload, isMessageVariant, onCancel, onSend]);
326
-
327
- // 处理键盘事件
328
- const handleKeyDown = useCallback(
329
- (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
330
- if (voiceCtl.handleKeyDownForVoice(e)) return;
331
- if (e.key === 'Enter' && !e.shiftKey) {
332
- e.preventDefault();
333
- handleSendOrCancel();
334
- }
335
- },
336
- [handleSendOrCancel, voiceCtl]
337
- );
338
-
339
- // 点击外部关闭菜单
340
- useEffect(() => {
341
- const handleClickOutside = (event: MouseEvent) => {
342
- const target = event.target as HTMLElement;
343
- // 关闭 @ 选择器
344
- if (!target.closest('.at-picker-wrapper')) {
345
- setAtPickerVisible(false);
346
- }
347
- if (isMessageVariant && containerRef.current && !containerRef.current.contains(target)) {
348
- setIsFocused(false);
349
- }
350
- };
351
-
352
- document.addEventListener('click', handleClickOutside);
353
- return () => document.removeEventListener('click', handleClickOutside);
354
- }, [isMessageVariant]);
355
-
356
- return (
357
- <div
358
- className={`chat-input${isMessageVariant ? ' message-variant' : ''}`.trim()}
359
- onDragOver={imageUpload.handleDragOver}
360
- onDragLeave={imageUpload.handleDragLeave}
361
- onDrop={imageUpload.handleDrop}
362
- >
363
- <div
364
- ref={containerRef}
365
- className={`input-container${isFocused ? ' focused' : ''}${imageUpload.isDragOver ? ' drag-over' : ''}${voiceInput.status === 'connecting' ? ' connecting' : ''}${voiceInput.isRecording ? ' recording' : ''}`.trim()}
366
- >
367
- {/* 图片预览区 */}
368
- {imageUpload.hasImages && (
369
- <div className="images-preview">
370
- {imageUpload.images.map((img, i) => (
371
- <div key={i} className="image-preview-item">
372
- <img
373
- src={img.dataUrl}
374
- alt="预览"
375
- className="image-thumbnail"
376
- onClick={() => imageUpload.openPreview(i)}
377
- />
378
- <button
379
- className="image-remove-btn"
380
- title="移除图片"
381
- onClick={(e) => {
382
- e.stopPropagation();
383
- imageUpload.removeImage(i);
384
- }}
385
- >
386
- <Icon icon="lucide:x" width={10} />
387
- </button>
388
- </div>
389
- ))}
390
- </div>
391
- )}
392
-
393
- {/* 图片预览弹窗 */}
394
- <ImagePreviewModal
395
- visible={imageUpload.previewVisible}
396
- images={imageUpload.imageUrls}
397
- initialIndex={imageUpload.previewIndex}
398
- onClose={imageUpload.closePreview}
399
- onIndexChange={imageUpload.setPreviewIndex}
400
- />
401
-
402
- {/* 输入框 */}
403
- <div className="input-field-wrapper">
404
- <textarea
405
- ref={inputRef}
406
- value={inputText}
407
- placeholder={placeholder}
408
- rows={1}
409
- className="input-field chat-scrollbar"
410
- readOnly={voiceInput.isRecording}
411
- spellCheck={false}
412
- autoCorrect="off"
413
- autoComplete="off"
414
- autoCapitalize="off"
415
- onChange={(e) => {
416
- const next = e.target.value;
417
- const prev = prevValueRef.current;
418
- prevValueRef.current = next;
419
- setInputText(next);
420
-
421
- // 检测是否新增输入了 '@',用事件驱动打开面板
422
- if (adapter && next.length === prev.length + 1) {
423
- const cursor = e.target.selectionStart ?? next.length;
424
- const inserted = next.slice(cursor - 1, cursor);
425
- if (inserted === '@') {
426
- replaceRangeRef.current = { start: cursor - 1, end: cursor };
427
- setAtPickerVisible(true);
428
- }
429
- }
430
- }}
431
- onInput={adjustTextareaHeight}
432
- onKeyDown={handleKeyDown}
433
- onFocus={() => setIsFocused(true)}
434
- onPaste={imageUpload.handlePaste}
435
- />
436
- </div>
437
-
438
- {/* 底部控制栏 */}
439
- {showToolbar && (
440
- <div className="input-controls">
441
- {/* 左侧 */}
442
- <div className="input-left">
443
- {/* 模式选择 */}
444
- <DropdownSelector
445
- value={mode}
446
- options={MODE_OPTIONS}
447
- onSelect={(v) => onModeChange?.(v as ChatMode)}
448
- />
449
-
450
- {/* 模型选择(分组显示:原生和 OpenRouter) */}
451
- <DropdownSelector
452
- value={model}
453
- options={modelOptions}
454
- onSelect={(v) => onModelChange?.(v)}
455
- />
456
- </div>
457
-
458
- {/* 右侧 */}
459
- <div className="input-right">
460
- {/* 深度思考(ask 模式隐藏,仅支持的模型显示) */}
461
- {mode !== 'ask' && currentModelSupportsThinking && (
462
- <button
463
- className={`toggle-btn${thinkingEnabled ? ' active' : ''}`}
464
- title="深度思考"
465
- onClick={() => onThinkingChange?.(!thinkingEnabled)}
466
- >
467
- <Icon icon="lucide:sparkles" width={18} />
468
- </button>
469
- )}
470
-
471
- {/* 联网搜索(ask 模式隐藏) */}
472
- {mode !== 'ask' && (
473
- <button
474
- className={`toggle-btn${webSearchEnabled ? ' active' : ''}`}
475
- title="联网搜索"
476
- onClick={() => onWebSearchChange?.(!webSearchEnabled)}
477
- >
478
- <Icon icon="lucide:globe" width={18} />
479
- </button>
480
- )}
481
-
482
- {/* 图片上传按钮 */}
483
- <button className="icon-btn" title="添加图片" onClick={triggerImageUpload}>
484
- <Icon icon="lucide:image" width={18} />
485
- </button>
486
- <input
487
- ref={imageInputRef}
488
- type="file"
489
- accept="image/*"
490
- multiple
491
- className="hidden-input"
492
- onChange={imageUpload.handleImageSelect}
493
- />
494
-
495
- {/* @ 上下文选择器 */}
496
- <div
497
- ref={atSelectorRef}
498
- className="at-picker-wrapper"
499
- onClick={(e) => {
500
- e.stopPropagation();
501
- toggleAtPicker();
502
- }}
503
- >
504
- <button className="icon-btn" title="提及上下文 (@)">
505
- <Icon icon="lucide:at-sign" width={18} />
506
- </button>
507
-
508
- {/* @ 下拉菜单 */}
509
- {adapter && (
510
- <AtFilePicker
511
- visible={atPickerVisible}
512
- adapter={adapter}
513
- // initialDir 已移除,cwd 已解耦
514
- anchorEl={atSelectorRef.current}
515
- onClose={closeAtPicker}
516
- onSelect={applyAtPath}
517
- />
518
- )}
519
- </div>
520
-
521
- {/* 方案 A:录音按钮 + 发送按钮独立(停止录音入口永不消失) */}
522
- <button
523
- className={`voice-btn${voiceInput.status === 'connecting' ? ' connecting' : ''}${voiceInput.isRecording ? ' recording' : ''}`}
524
- title={
525
- voiceInput.status === 'connecting'
526
- ? '正在连接,点击取消'
527
- : voiceInput.isRecording
528
- ? '点击停止'
529
- : '点击录音'
530
- }
531
- onClick={() => toggleVoiceInput().catch(() => {})}
532
- disabled={isLoading || !adapter}
533
- >
534
- {voiceInput.status === 'connecting' ? (
535
- <Icon icon="lucide:loader-2" width={18} className="spin" />
536
- ) : voiceInput.isRecording ? (
537
- <Icon icon="streamline-flex:button-pause-circle-remix" width={18} />
538
- ) : (
539
- <Icon icon="lucide:mic" width={18} />
540
- )}
541
- </button>
542
-
543
- <button
544
- className={`send-btn${isLoading ? ' loading' : ''}`}
545
- title={
546
- isLoading
547
- ? '停止'
548
- : voiceInput.status === 'connecting'
549
- ? '语音连接中,先取消/停止'
550
- : voiceInput.isRecording
551
- ? '录音中,先停止'
552
- : isMessageVariant
553
- ? '重新发送'
554
- : '发送'
555
- }
556
- onClick={handleSendOrCancel}
557
- disabled={(!hasContent && !isLoading) || voiceInput.isRecording || voiceInput.status === 'connecting'}
558
- >
559
- {isLoading ? (
560
- <Icon icon="streamline-flex:button-pause-circle-remix" width={18} />
561
- ) : (
562
- <Icon icon="uil:message" width={18} />
563
- )}
564
- </button>
565
- </div>
566
- </div>
567
- )}
568
- </div>
569
- </div>
570
- );
571
- }
572
- );
573
-
574
- // 添加 displayName 以便于调试
575
- ChatInput.displayName = 'ChatInput';