@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
package/src/adapter.ts DELETED
@@ -1,68 +0,0 @@
1
- /**
2
- * Chat Adapter 辅助类型和工具
3
- * 核心 ChatAdapter 接口从 bridge-electron 导入
4
- */
5
-
6
- import type { ChatAdapter, ChatMode, ThinkingMode, FileInfo } from '@huyooo/ai-chat-bridge-electron/renderer'
7
-
8
- /**
9
- * 重新导出 ChatAdapter 类型供内部组件使用
10
- */
11
- export type { ChatAdapter, FileInfo }
12
-
13
- /** 思考数据 */
14
- export interface ThinkingData {
15
- content: string
16
- isComplete: boolean
17
- }
18
-
19
- /** 工具调用数据 */
20
- export interface ToolCallData {
21
- name: string
22
- args: Record<string, unknown>
23
- }
24
-
25
- /** 工具结果数据 */
26
- export interface ToolResultData {
27
- name: string
28
- result: string
29
- }
30
-
31
- /** 图片数据 */
32
- export interface ImageData {
33
- base64: string
34
- mimeType: string
35
- }
36
-
37
- /** 发送消息选项 */
38
- export interface SendMessageOptions {
39
- mode: ChatMode
40
- model: string
41
- enableWebSearch: boolean
42
- /** 深度思考开关(每个 provider 内部使用最优参数) */
43
- thinkingMode: ThinkingMode
44
- }
45
-
46
- /** 创建会话选项 */
47
- export interface CreateSessionOptions {
48
- title: string
49
- model: string
50
- mode: ChatMode
51
- }
52
-
53
- /** 更新会话选项 */
54
- export interface UpdateSessionOptions {
55
- title?: string
56
- model?: string
57
- mode?: ChatMode
58
- }
59
-
60
- /** 保存消息选项 */
61
- export interface SaveMessageOptions {
62
- sessionId: string
63
- role: 'user' | 'assistant'
64
- content: string
65
- /** 执行步骤列表 JSON */
66
- steps?: string
67
- operationIds?: string
68
- }
@@ -1,553 +0,0 @@
1
- /**
2
- * ChatPanel Component
3
- * 与 Vue 版本 ChatPanel.vue 保持一致
4
- */
5
-
6
- import { useEffect, useRef, useCallback, useMemo, useState, forwardRef, useImperativeHandle, type ComponentType } from 'react'
7
- import { Icon } from '@iconify/react'
8
- import { useChat, type ToolCompleteEvent } from '../hooks/useChat'
9
- import type { ImageData } from '../adapter'
10
- import type { ChatAdapter, ModelOption, ChatMode } from '@huyooo/ai-chat-bridge-electron/renderer'
11
- import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer'
12
- import { ChatHeader } from './header/ChatHeader'
13
- import { WelcomeMessage } from './message/WelcomeMessage'
14
- import type { WelcomeConfig } from './message/welcome-types'
15
- import { MessageBubble } from './message/MessageBubble'
16
- import { ChatInput, type ChatInputHandle } from './input/ChatInput'
17
- import { ChatInputProvider } from '../context/ChatInputContext'
18
- import { PartRenderersProvider, type PartRenderers } from '../context/RenderersContext'
19
- import { ConfirmDialog } from './common/ConfirmDialog'
20
- import { Toast } from './common/Toast'
21
- // ToolApprovalDialog 已移除,工具批准现在内嵌在 ToolCallPart 中
22
- import { SettingsPanel } from './common/SettingsPanel'
23
-
24
- /** ChatPanel 暴露给外部的方法 */
25
- export interface ChatPanelHandle {
26
- /** 设置输入框内容 */
27
- setInputText: (text: string) => void
28
- /** 在光标位置插入文本(用于 @ 上下文) */
29
- insertInputText: (text: string) => void
30
- /** 聚焦输入框 */
31
- focusInput: () => void
32
- /** 发送消息 */
33
- sendMessage: (text: string) => void
34
- /** 设置当前工作目录 */
35
- setCwd: (dir: string) => void
36
- }
37
-
38
- interface ChatPanelProps {
39
- /** Adapter 实例 */
40
- adapter: ChatAdapter
41
- /** 默认模型 */
42
- defaultModel?: string
43
- /** 默认模式 */
44
- defaultMode?: ChatMode
45
- /** 可用模型列表 */
46
- models?: ModelOption[]
47
- /** 隐藏标题栏 */
48
- hideHeader?: boolean
49
- /** 关闭回调(有此属性时显示关闭按钮) */
50
- onClose?: () => void
51
- /** 工具执行完成回调 */
52
- onToolComplete?: (event: ToolCompleteEvent) => void
53
- /** 自定义类名 */
54
- className?: string
55
- /** 欢迎页配置 */
56
- welcomeConfig?: Partial<WelcomeConfig>
57
- /** 自定义 Part 渲染器 - 根据 part.type 选择渲染组件(如 weather, stock 等) */
58
- partRenderers?: PartRenderers
59
- /**
60
- * 执行步骤折叠模式
61
- * - 'open': 始终展开
62
- * - 'close': 始终折叠
63
- * - 'auto': 执行时展开,完成后折叠
64
- */
65
- stepsExpandedType?: 'open' | 'close' | 'auto'
66
- }
67
-
68
- export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
69
- adapter,
70
- defaultModel = 'anthropic/claude-opus-4.5',
71
- defaultMode = 'agent',
72
- models: propModels,
73
- hideHeader = false,
74
- onClose,
75
- onToolComplete,
76
- className = '',
77
- welcomeConfig,
78
- partRenderers = {},
79
- stepsExpandedType = 'auto',
80
- }, ref) => {
81
- const messagesRef = useRef<HTMLDivElement>(null)
82
- const inputRef = useRef<ChatInputHandle>(null)
83
-
84
- // 是否应该自动滚动(用户在底部附近时才自动滚动)
85
- const [shouldAutoScroll, setShouldAutoScroll] = useState(true)
86
- // 距离底部多少像素内算"在底部"
87
- const SCROLL_THRESHOLD = 25
88
- // 上次滚动位置(用于检测滚动方向)
89
- const lastScrollTopRef = useRef(0)
90
- // 是否正在程序化滚动(用于区分用户手动滚动)
91
- const isProgrammaticScrollRef = useRef(false)
92
-
93
- // 设置面板状态
94
- const [settingsPanelVisible, setSettingsPanelVisible] = useState(false)
95
-
96
- // 从后端获取模型列表,如果传入了 models 则使用传入的
97
- const [models, setModels] = useState<ModelOption[]>(propModels || [])
98
-
99
- // 确认弹窗状态
100
- const [confirmDialog, setConfirmDialog] = useState<{
101
- visible: boolean
102
- title: string
103
- message: string
104
- type: 'info' | 'warning' | 'danger'
105
- confirmText: string
106
- onConfirm: () => void
107
- }>({
108
- visible: false,
109
- title: '确认',
110
- message: '',
111
- type: 'warning',
112
- confirmText: '确定',
113
- onConfirm: () => {},
114
- })
115
-
116
- /** 显示确认弹窗 */
117
- const showConfirm = useCallback((options: {
118
- title?: string
119
- message: string
120
- type?: 'info' | 'warning' | 'danger'
121
- confirmText?: string
122
- onConfirm: () => void
123
- }) => {
124
- setConfirmDialog({
125
- visible: true,
126
- title: options.title || '确认',
127
- message: options.message,
128
- type: options.type || 'warning',
129
- confirmText: options.confirmText || '确定',
130
- onConfirm: options.onConfirm,
131
- })
132
- }, [])
133
-
134
- // Toast 消息状态
135
- const [toast, setToast] = useState<{
136
- visible: boolean
137
- message: string
138
- type: 'info' | 'success' | 'warning' | 'error'
139
- }>({ visible: false, message: '', type: 'info' })
140
-
141
- /** 显示 Toast 消息 */
142
- const showToast = useCallback((message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info') => {
143
- setToast({ visible: true, message, type })
144
- }, [])
145
-
146
- const {
147
- sessions,
148
- currentSessionId,
149
- messages,
150
- isLoading,
151
- mode,
152
- model,
153
- webSearch,
154
- thinking,
155
- loadSessions,
156
- switchSession,
157
- createNewSession,
158
- deleteSession,
159
- hideSession,
160
- clearAllSessions,
161
- hideOtherSessions,
162
- exportCurrentSession,
163
- sendMessage,
164
- cancelRequest,
165
- copyMessage,
166
- regenerateMessage,
167
- resendFromIndex,
168
- setMode,
169
- setModel,
170
- setWebSearch,
171
- setThinking,
172
- setWorkingDirectory,
173
- autoRunConfig,
174
- saveAutoRunConfig,
175
- // 工具管理
176
- allTools,
177
- enabledTools,
178
- saveEnabledTools,
179
- } = useChat({
180
- adapter,
181
- defaultModel,
182
- defaultMode,
183
- onToolComplete,
184
- })
185
-
186
- const handleCancelToolCall = useCallback((_toolCallId: string) => {
187
- cancelRequest()
188
- }, [cancelRequest])
189
-
190
- // 暴露给外部的方法
191
- useImperativeHandle(ref, () => ({
192
- setInputText: (text: string) => {
193
- inputRef.current?.setText(text)
194
- },
195
- insertInputText: (text: string) => {
196
- inputRef.current?.insertText(text)
197
- },
198
- focusInput: () => {
199
- inputRef.current?.focus()
200
- },
201
- sendMessage: (text: string) => {
202
- sendMessage(text)
203
- },
204
- setCwd: setWorkingDirectory,
205
- }), [sendMessage, setWorkingDirectory])
206
-
207
- // 初始化
208
- useEffect(() => {
209
- loadSessions()
210
- }, [loadSessions])
211
-
212
- // 从后端获取模型列表
213
- useEffect(() => {
214
- adapter.getModels()
215
- .then(setModels)
216
- .catch((err) => console.warn('获取模型列表失败:', err))
217
- }, [adapter])
218
-
219
- // 注意:cwd 已解耦,不再通过 prop 传递
220
- // FileBrowser 会直接通过 adapter.setCwd() 同步到 Agent
221
- // getCwdTool 会自动从 Agent 的 context.cwd 读取最新值
222
-
223
- // 检查是否在底部附近
224
- const isNearBottom = useCallback((): boolean => {
225
- if (!messagesRef.current) return true
226
- const { scrollTop, scrollHeight, clientHeight } = messagesRef.current
227
- return scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD
228
- }, [])
229
-
230
- // 处理滚动事件(优化:检测滚动方向,区分程序化滚动)
231
- const handleScroll = useCallback(() => {
232
- if (!messagesRef.current) return
233
-
234
- // 忽略程序化滚动触发的事件
235
- if (isProgrammaticScrollRef.current) return
236
-
237
- const { scrollTop } = messagesRef.current
238
- const isScrollingUp = scrollTop < lastScrollTopRef.current
239
- lastScrollTopRef.current = scrollTop
240
-
241
- // 用户向上滚动时,立即停止自动滚动
242
- if (isScrollingUp) {
243
- setShouldAutoScroll(false)
244
- return
245
- }
246
-
247
- // 用户向下滚动到底部附近时,恢复自动滚动
248
- if (isNearBottom()) {
249
- setShouldAutoScroll(true)
250
- }
251
- }, [isNearBottom])
252
-
253
- // 滚动到底部
254
- const scrollToBottom = useCallback((force = false) => {
255
- if (messagesRef.current && (force || shouldAutoScroll)) {
256
- // 标记为程序化滚动,避免触发 handleScroll 逻辑
257
- isProgrammaticScrollRef.current = true
258
- messagesRef.current.scrollTop = messagesRef.current.scrollHeight
259
- lastScrollTopRef.current = messagesRef.current.scrollTop
260
- // 强制滚动时才恢复自动滚动
261
- if (force) {
262
- setShouldAutoScroll(true)
263
- }
264
- // 延迟重置标志,确保 scroll 事件已处理
265
- requestAnimationFrame(() => {
266
- isProgrammaticScrollRef.current = false
267
- })
268
- }
269
- }, [shouldAutoScroll])
270
-
271
- // 消息变化时滚动(只在用户在底部时)
272
- useEffect(() => {
273
- scrollToBottom()
274
- }, [messages, scrollToBottom])
275
-
276
- // 发送新消息时强制滚动到底部
277
- const prevIsLoadingRef = useRef(isLoading)
278
- useEffect(() => {
279
- // 开始加载时(发送消息时)强制滚动到底部
280
- if (isLoading && !prevIsLoadingRef.current) {
281
- scrollToBottom(true)
282
- }
283
- prevIsLoadingRef.current = isLoading
284
- }, [isLoading, scrollToBottom])
285
-
286
- // 发送消息
287
- const handleSend = useCallback((text: string, images?: ImageData[]) => {
288
- // 将 ImageData[] 转换为 string[] (data URL)
289
- const imageUrls = images?.map(img => `data:${img.mimeType};base64,${img.base64}`)
290
- sendMessage(text, imageUrls)
291
- }, [sendMessage])
292
-
293
- // @ 上下文
294
- const handleAtContext = useCallback(() => {
295
- // TODO: 实现 @ 上下文
296
- console.log('@ 上下文')
297
- }, [])
298
-
299
- // 快捷操作
300
- const handleQuickAction = useCallback(
301
- (text: string) => {
302
- sendMessage(text)
303
- },
304
- [sendMessage]
305
- )
306
-
307
- // 重新发送(编辑后)
308
- const handleResend = useCallback(
309
- (index: number, text: string) => {
310
- // 与 Vue 版一致:使用分叉重发(以当前索引为锚点,删除后续并继续生成)
311
- resendFromIndex(index, text)
312
- },
313
- [resendFromIndex]
314
- )
315
-
316
- // 创建新会话(重置输入框状态)
317
- const handleNewSession = useCallback(async () => {
318
- await createNewSession()
319
- // 重置输入框状态
320
- inputRef.current?.clear()
321
- }, [createNewSession])
322
-
323
- // 关闭
324
- const handleClose = useCallback(() => {
325
- onClose?.()
326
- }, [onClose])
327
-
328
- // 设置
329
- const handleSettings = useCallback(() => {
330
- setSettingsPanelVisible(true)
331
- }, [])
332
-
333
- // 保存设置(静默保存)
334
- const handleSaveSettings = useCallback(async (config: AutoRunConfig) => {
335
- try {
336
- await saveAutoRunConfig(config)
337
- } catch (error) {
338
- console.error('保存设置失败:', error)
339
- showToast('保存设置失败', 'error')
340
- }
341
- }, [saveAutoRunConfig, showToast])
342
-
343
- const handleUpdateEnabledTools = useCallback(async (tools: string[] | undefined) => {
344
- try {
345
- await saveEnabledTools(tools)
346
- } catch (error) {
347
- console.error('保存工具开关失败:', error)
348
- showToast('保存工具开关失败', 'error')
349
- }
350
- }, [saveEnabledTools, showToast])
351
-
352
- // 模式变更现在在 ToolCallPart 中处理,不再需要此函数
353
-
354
- // 清空所有对话
355
- const handleClearAll = useCallback(() => {
356
- showConfirm({
357
- title: '清空所有对话',
358
- message: '确定要清空所有对话吗?此操作不可恢复。',
359
- type: 'danger',
360
- confirmText: '清空',
361
- onConfirm: () => clearAllSessions(),
362
- })
363
- }, [showConfirm, clearAllSessions])
364
-
365
- // 关闭其他对话
366
- const handleCloseOthers = useCallback(async () => {
367
- await hideOtherSessions()
368
- }, [hideOtherSessions])
369
-
370
- // 导出对话
371
- const handleExport = useCallback(() => {
372
- const data = exportCurrentSession()
373
- if (!data) {
374
- showToast('当前会话没有内容可导出', 'warning')
375
- return
376
- }
377
-
378
- // 创建下载链接
379
- const blob = new Blob([data], { type: 'application/json' })
380
- const url = URL.createObjectURL(blob)
381
- const a = document.createElement('a')
382
- const session = sessions.find((s) => s.id === currentSessionId)
383
- const filename = `chat-${session?.title || 'export'}-${new Date().toISOString().slice(0, 10)}.json`
384
- a.href = url
385
- a.download = filename
386
- document.body.appendChild(a)
387
- a.click()
388
- document.body.removeChild(a)
389
- URL.revokeObjectURL(url)
390
- }, [exportCurrentSession, sessions, currentSessionId, showToast])
391
-
392
- // 复制会话 ID
393
- const handleCopyId = useCallback(async () => {
394
- if (!currentSessionId) return
395
- try {
396
- await navigator.clipboard.writeText(currentSessionId)
397
- showToast('已复制会话 ID', 'success')
398
- } catch (error) {
399
- console.error('复制失败:', error)
400
- }
401
- }, [currentSessionId, showToast])
402
-
403
- // 反馈
404
- const handleFeedback = useCallback(() => {
405
- console.log('反馈')
406
- }, [])
407
-
408
- // 全局 input 状态 context value
409
- const inputContextValue = useMemo(
410
- () => ({
411
- mode,
412
- model,
413
- models,
414
- webSearch,
415
- thinking,
416
- isLoading,
417
- adapter,
418
- // cwd 已解耦,不再通过 prop 传递
419
- // FileBrowser 会直接通过 adapter.setCwd() 同步到 Agent
420
- // getCwdTool 会自动从 Agent 的 context.cwd 读取最新值
421
- setMode,
422
- setModel,
423
- setWebSearch,
424
- setThinking,
425
- }),
426
- [mode, model, models, webSearch, thinking, isLoading, adapter, setMode, setModel, setWebSearch, setThinking]
427
- )
428
-
429
- return (
430
- <ChatInputProvider value={inputContextValue}>
431
- <PartRenderersProvider partRenderers={partRenderers}>
432
- <div className={`chat-panel ${className}`.trim()}>
433
- {/* 确认弹窗 */}
434
- <ConfirmDialog
435
- visible={confirmDialog.visible}
436
- title={confirmDialog.title}
437
- message={confirmDialog.message}
438
- type={confirmDialog.type}
439
- confirmText={confirmDialog.confirmText}
440
- onConfirm={() => {
441
- setConfirmDialog((prev) => ({ ...prev, visible: false }))
442
- confirmDialog.onConfirm()
443
- }}
444
- onCancel={() => setConfirmDialog((prev) => ({ ...prev, visible: false }))}
445
- />
446
-
447
- {/* Toast 消息 */}
448
- <Toast
449
- visible={toast.visible}
450
- message={toast.message}
451
- type={toast.type}
452
- onClose={() => setToast((prev) => ({ ...prev, visible: false }))}
453
- />
454
-
455
- {/* 工具批准现在内嵌在 ToolCallPart 中,不再需要全局对话框 */}
456
-
457
- {/* 设置面板 */}
458
- <SettingsPanel
459
- visible={settingsPanelVisible}
460
- config={autoRunConfig}
461
- allTools={allTools}
462
- enabledTools={enabledTools}
463
- onUpdateEnabledTools={handleUpdateEnabledTools}
464
- onChange={handleSaveSettings}
465
- onClose={() => setSettingsPanelVisible(false)}
466
- />
467
-
468
- {/* 顶部标题栏 */}
469
- {!hideHeader && (
470
- <ChatHeader
471
- sessions={sessions}
472
- currentSessionId={currentSessionId}
473
- showClose={!!onClose}
474
- onNewSession={handleNewSession}
475
- onSwitchSession={switchSession}
476
- onDeleteSession={deleteSession}
477
- onHideSession={hideSession}
478
- onClose={handleClose}
479
- onClearAll={handleClearAll}
480
- onCloseOthers={handleCloseOthers}
481
- onExport={handleExport}
482
- onCopyId={handleCopyId}
483
- onFeedback={handleFeedback}
484
- onSettings={handleSettings}
485
- />
486
- )}
487
-
488
- {/* 消息列表容器 */}
489
- <div className="messages-wrapper">
490
- <div ref={messagesRef} className="messages-container chat-scrollbar" onScroll={handleScroll}>
491
- {messages.length === 0 ? (
492
- <WelcomeMessage config={welcomeConfig} onQuickAction={handleQuickAction} />
493
- ) : (
494
- messages.map((msg, index) => (
495
- <MessageBubble
496
- key={msg.id}
497
- role={msg.role}
498
- parts={msg.parts}
499
- model={msg.model}
500
- mode={msg.mode}
501
- images={msg.images}
502
- copied={msg.copied}
503
- loading={msg.loading}
504
- timestamp={msg.timestamp}
505
- stepsExpandedType={stepsExpandedType}
506
- adapter={adapter}
507
- onCancelToolCall={handleCancelToolCall}
508
- autoRunConfig={autoRunConfig}
509
- onSaveConfig={saveAutoRunConfig}
510
- onCopy={() => copyMessage(msg.id)}
511
- onRegenerate={() => regenerateMessage(index)}
512
- onSend={(text) => handleResend(index, text)}
513
- />
514
- ))
515
- )}
516
- </div>
517
- {/* 滚动到底部按钮 */}
518
- {!shouldAutoScroll && messages.length > 0 && (
519
- <button
520
- className="scroll-to-bottom-btn"
521
- onClick={() => scrollToBottom(true)}
522
- title="滚动到底部"
523
- >
524
- <Icon icon="lucide:arrow-down" width={16} />
525
- </button>
526
- )}
527
- </div>
528
-
529
- {/* 输入区域 */}
530
- <ChatInput
531
- ref={inputRef}
532
- isLoading={isLoading}
533
- mode={mode}
534
- model={model}
535
- models={models}
536
- webSearchEnabled={webSearch}
537
- thinkingEnabled={thinking}
538
- onSend={handleSend}
539
- onCancel={cancelRequest}
540
- onModeChange={setMode}
541
- onModelChange={setModel}
542
- onWebSearchChange={setWebSearch}
543
- onThinkingChange={setThinking}
544
- onAtContext={handleAtContext}
545
- />
546
- </div>
547
- </PartRenderersProvider>
548
- </ChatInputProvider>
549
- )
550
- })
551
-
552
- // 添加 displayName 以便于调试
553
- ChatPanel.displayName = 'ChatPanel'