@huyooo/ai-chat-frontend-react 0.1.6 → 0.1.8

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 (91) hide show
  1. package/README.md +368 -0
  2. package/dist/index.css +2575 -0
  3. package/dist/index.css.map +1 -0
  4. package/dist/index.d.ts +378 -135
  5. package/dist/index.js +3956 -1042
  6. package/dist/index.js.map +1 -1
  7. package/dist/style.css +48 -987
  8. package/package.json +7 -4
  9. package/src/adapter.ts +10 -70
  10. package/src/components/ChatPanel.tsx +373 -117
  11. package/src/components/common/ConfirmDialog.css +136 -0
  12. package/src/components/common/ConfirmDialog.tsx +91 -0
  13. package/src/components/common/CopyButton.css +22 -0
  14. package/src/components/common/CopyButton.tsx +46 -0
  15. package/src/components/common/IndexingSettings.css +207 -0
  16. package/src/components/common/IndexingSettings.tsx +398 -0
  17. package/src/components/common/SettingsPanel.css +256 -0
  18. package/src/components/common/SettingsPanel.tsx +120 -0
  19. package/src/components/common/Toast.css +50 -0
  20. package/src/components/common/Toast.tsx +38 -0
  21. package/src/components/common/ToggleSwitch.css +52 -0
  22. package/src/components/common/ToggleSwitch.tsx +20 -0
  23. package/src/components/header/ChatHeader.css +285 -0
  24. package/src/components/header/ChatHeader.tsx +376 -0
  25. package/src/components/input/AtFilePicker.css +147 -0
  26. package/src/components/input/AtFilePicker.tsx +519 -0
  27. package/src/components/input/ChatInput.css +204 -0
  28. package/src/components/input/ChatInput.tsx +506 -0
  29. package/src/components/input/DropdownSelector.css +159 -0
  30. package/src/components/input/DropdownSelector.tsx +195 -0
  31. package/src/components/input/ImagePreviewModal.css +124 -0
  32. package/src/components/input/ImagePreviewModal.tsx +118 -0
  33. package/src/components/input/at-views/AtBranchView.tsx +34 -0
  34. package/src/components/input/at-views/AtBrowserView.tsx +34 -0
  35. package/src/components/input/at-views/AtChatsView.tsx +34 -0
  36. package/src/components/input/at-views/AtDocsView.tsx +34 -0
  37. package/src/components/input/at-views/AtFilesView.tsx +168 -0
  38. package/src/components/input/at-views/AtTerminalsView.tsx +34 -0
  39. package/src/components/input/at-views/AtViewStyles.css +143 -0
  40. package/src/components/input/at-views/index.ts +9 -0
  41. package/src/components/message/ContentRenderer.css +9 -0
  42. package/src/components/message/ContentRenderer.tsx +63 -0
  43. package/src/components/message/MessageBubble.css +190 -0
  44. package/src/components/message/MessageBubble.tsx +231 -0
  45. package/src/components/message/PartsRenderer.css +4 -0
  46. package/src/components/message/PartsRenderer.tsx +114 -0
  47. package/src/components/message/ToolResultRenderer.tsx +21 -0
  48. package/src/components/message/WelcomeMessage.css +221 -0
  49. package/src/components/message/WelcomeMessage.tsx +93 -0
  50. package/src/components/message/blocks/CodeBlock.tsx +60 -0
  51. package/src/components/message/blocks/TextBlock.tsx +15 -0
  52. package/src/components/message/blocks/blocks.css +141 -0
  53. package/src/components/message/blocks/index.ts +6 -0
  54. package/src/components/message/parts/CollapsibleCard.css +78 -0
  55. package/src/components/message/parts/CollapsibleCard.tsx +77 -0
  56. package/src/components/message/parts/ErrorPart.css +9 -0
  57. package/src/components/message/parts/ErrorPart.tsx +40 -0
  58. package/src/components/message/parts/ImagePart.css +50 -0
  59. package/src/components/message/parts/ImagePart.tsx +54 -0
  60. package/src/components/message/parts/SearchPart.css +44 -0
  61. package/src/components/message/parts/SearchPart.tsx +63 -0
  62. package/src/components/message/parts/TextPart.css +10 -0
  63. package/src/components/message/parts/TextPart.tsx +20 -0
  64. package/src/components/message/parts/ThinkingPart.css +9 -0
  65. package/src/components/message/parts/ThinkingPart.tsx +48 -0
  66. package/src/components/message/parts/ToolCallPart.css +220 -0
  67. package/src/components/message/parts/ToolCallPart.tsx +285 -0
  68. package/src/components/message/parts/ToolResultPart.css +68 -0
  69. package/src/components/message/parts/ToolResultPart.tsx +96 -0
  70. package/src/components/message/parts/index.ts +11 -0
  71. package/src/components/message/tool-results/DefaultToolResult.tsx +26 -0
  72. package/src/components/message/tool-results/SearchResults.tsx +69 -0
  73. package/src/components/message/tool-results/WeatherCard.tsx +63 -0
  74. package/src/components/message/tool-results/index.ts +7 -0
  75. package/src/components/message/tool-results/tool-results.css +179 -0
  76. package/src/components/message/welcome-types.ts +46 -0
  77. package/src/context/AutoRunConfigContext.tsx +13 -0
  78. package/src/context/ChatAdapterContext.tsx +8 -0
  79. package/src/context/ChatInputContext.tsx +40 -0
  80. package/src/context/RenderersContext.tsx +41 -0
  81. package/src/hooks/useChat.ts +855 -237
  82. package/src/hooks/useImageUpload.ts +253 -0
  83. package/src/index.ts +96 -39
  84. package/src/styles.css +48 -987
  85. package/src/types/index.ts +172 -103
  86. package/src/utils/fileIcon.ts +49 -0
  87. package/src/components/ChatInput.tsx +0 -368
  88. package/src/components/chat/messages/ExecutionSteps.tsx +0 -234
  89. package/src/components/chat/messages/MessageBubble.tsx +0 -130
  90. package/src/components/chat/ui/ChatHeader.tsx +0 -301
  91. package/src/components/chat/ui/WelcomeMessage.tsx +0 -107
@@ -3,48 +3,140 @@
3
3
  * 与 Vue 版本 ChatPanel.vue 保持一致
4
4
  */
5
5
 
6
- import { useEffect, useRef, useCallback, type FC, type ReactNode } from 'react'
7
- import { useChat } from '../hooks/useChat'
8
- import type { ChatAdapter, ModelConfig, ChatMode } from '@huyooo/ai-chat-bridge-electron/renderer'
9
- import { DEFAULT_MODELS } from '../types'
10
- import { ChatHeader } from './chat/ui/ChatHeader'
11
- import { WelcomeMessage } from './chat/ui/WelcomeMessage'
12
- import { MessageBubble } from './chat/messages/MessageBubble'
13
- import { ChatInput } from './ChatInput'
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 { ChatAdapter, ModelOption, ChatMode } from '@huyooo/ai-chat-bridge-electron/renderer'
10
+ import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer'
11
+ import type { ToolRendererProps } from '@huyooo/ai-chat-shared'
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 { RenderersProvider } 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
+ }
14
37
 
15
38
  interface ChatPanelProps {
16
39
  /** Adapter 实例 */
17
- adapter?: ChatAdapter
18
- /** 工作目录 */
19
- workingDir?: string
40
+ adapter: ChatAdapter
20
41
  /** 默认模型 */
21
42
  defaultModel?: string
22
43
  /** 默认模式 */
23
44
  defaultMode?: ChatMode
24
45
  /** 可用模型列表 */
25
- models?: ModelConfig[]
46
+ models?: ModelOption[]
26
47
  /** 隐藏标题栏 */
27
48
  hideHeader?: boolean
28
49
  /** 关闭回调(有此属性时显示关闭按钮) */
29
50
  onClose?: () => void
51
+ /** 工具执行完成回调 */
52
+ onToolComplete?: (event: ToolCompleteEvent) => void
30
53
  /** 自定义类名 */
31
54
  className?: string
32
- /** 自定义 Markdown 渲染器 */
33
- renderMarkdown?: (content: string) => ReactNode
55
+ /** 欢迎页配置 */
56
+ welcomeConfig?: Partial<WelcomeConfig>
57
+ /** 自定义工具结果渲染器 - 根据工具名称选择渲染组件 */
58
+ toolRenderers?: Record<string, ComponentType<ToolRendererProps>>
59
+ /**
60
+ * 执行步骤折叠模式
61
+ * - 'open': 始终展开
62
+ * - 'close': 始终折叠
63
+ * - 'auto': 执行时展开,完成后折叠
64
+ */
65
+ stepsExpandedType?: 'open' | 'close' | 'auto'
34
66
  }
35
67
 
36
- export const ChatPanel: FC<ChatPanelProps> = ({
68
+ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
37
69
  adapter,
38
- workingDir,
39
70
  defaultModel = 'anthropic/claude-opus-4.5',
40
71
  defaultMode = 'agent',
41
- models = DEFAULT_MODELS,
72
+ models: propModels,
42
73
  hideHeader = false,
43
74
  onClose,
75
+ onToolComplete,
44
76
  className = '',
45
- renderMarkdown,
46
- }) => {
77
+ welcomeConfig,
78
+ toolRenderers = {},
79
+ stepsExpandedType = 'auto',
80
+ }, ref) => {
47
81
  const messagesRef = useRef<HTMLDivElement>(null)
82
+ const inputRef = useRef<ChatInputHandle>(null)
83
+
84
+ // 是否应该自动滚动(用户在底部附近时才自动滚动)
85
+ const [shouldAutoScroll, setShouldAutoScroll] = useState(true)
86
+ const SCROLL_THRESHOLD = 100
87
+
88
+ // 设置面板状态
89
+ const [settingsPanelVisible, setSettingsPanelVisible] = useState(false)
90
+
91
+ // 从后端获取模型列表,如果传入了 models 则使用传入的
92
+ const [models, setModels] = useState<ModelOption[]>(propModels || [])
93
+
94
+ // 确认弹窗状态
95
+ const [confirmDialog, setConfirmDialog] = useState<{
96
+ visible: boolean
97
+ title: string
98
+ message: string
99
+ type: 'info' | 'warning' | 'danger'
100
+ confirmText: string
101
+ onConfirm: () => void
102
+ }>({
103
+ visible: false,
104
+ title: '确认',
105
+ message: '',
106
+ type: 'warning',
107
+ confirmText: '确定',
108
+ onConfirm: () => {},
109
+ })
110
+
111
+ /** 显示确认弹窗 */
112
+ const showConfirm = useCallback((options: {
113
+ title?: string
114
+ message: string
115
+ type?: 'info' | 'warning' | 'danger'
116
+ confirmText?: string
117
+ onConfirm: () => void
118
+ }) => {
119
+ setConfirmDialog({
120
+ visible: true,
121
+ title: options.title || '确认',
122
+ message: options.message,
123
+ type: options.type || 'warning',
124
+ confirmText: options.confirmText || '确定',
125
+ onConfirm: options.onConfirm,
126
+ })
127
+ }, [])
128
+
129
+ // Toast 消息状态
130
+ const [toast, setToast] = useState<{
131
+ visible: boolean
132
+ message: string
133
+ type: 'info' | 'success' | 'warning' | 'error'
134
+ }>({ visible: false, message: '', type: 'info' })
135
+
136
+ /** 显示 Toast 消息 */
137
+ const showToast = useCallback((message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info') => {
138
+ setToast({ visible: true, message, type })
139
+ }, [])
48
140
 
49
141
  const {
50
142
  sessions,
@@ -59,6 +151,10 @@ export const ChatPanel: FC<ChatPanelProps> = ({
59
151
  switchSession,
60
152
  createNewSession,
61
153
  deleteSession,
154
+ hideSession,
155
+ clearAllSessions,
156
+ hideOtherSessions,
157
+ exportCurrentSession,
62
158
  sendMessage,
63
159
  cancelRequest,
64
160
  copyMessage,
@@ -68,46 +164,93 @@ export const ChatPanel: FC<ChatPanelProps> = ({
68
164
  setWebSearch,
69
165
  setThinking,
70
166
  setWorkingDirectory,
167
+ autoRunConfig,
168
+ saveAutoRunConfig,
71
169
  } = useChat({
72
170
  adapter,
73
171
  defaultModel,
74
172
  defaultMode,
173
+ onToolComplete,
75
174
  })
76
175
 
77
- // 选中的图片
78
- // const [selectedImages, setSelectedImages] = useState<string[]>([])
176
+ // 暴露给外部的方法
177
+ useImperativeHandle(ref, () => ({
178
+ setInputText: (text: string) => {
179
+ inputRef.current?.setText(text)
180
+ },
181
+ insertInputText: (text: string) => {
182
+ inputRef.current?.insertText(text)
183
+ },
184
+ focusInput: () => {
185
+ inputRef.current?.focus()
186
+ },
187
+ sendMessage: (text: string) => {
188
+ sendMessage(text)
189
+ },
190
+ setCwd: setWorkingDirectory,
191
+ }), [sendMessage, setWorkingDirectory])
79
192
 
80
193
  // 初始化
81
194
  useEffect(() => {
82
195
  loadSessions()
83
196
  }, [loadSessions])
84
197
 
85
- // 工作目录变化时更新
198
+ // 从后端获取模型列表
86
199
  useEffect(() => {
87
- if (workingDir) {
88
- setWorkingDirectory(workingDir)
89
- }
90
- }, [workingDir, setWorkingDirectory])
200
+ adapter.getModels()
201
+ .then(setModels)
202
+ .catch((err) => console.warn('获取模型列表失败:', err))
203
+ }, [adapter])
204
+
205
+ // 注意:cwd 已解耦,不再通过 prop 传递
206
+ // FileBrowser 会直接通过 adapter.setCwd() 同步到 Agent
207
+ // getCwdTool 会自动从 Agent 的 context.cwd 读取最新值
208
+
209
+ // 检查是否在底部附近
210
+ const isNearBottom = useCallback((): boolean => {
211
+ if (!messagesRef.current) return true
212
+ const { scrollTop, scrollHeight, clientHeight } = messagesRef.current
213
+ return scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD
214
+ }, [])
215
+
216
+ // 处理滚动事件
217
+ const handleScroll = useCallback(() => {
218
+ setShouldAutoScroll(isNearBottom())
219
+ }, [isNearBottom])
91
220
 
92
221
  // 滚动到底部
93
- const scrollToBottom = useCallback(() => {
94
- if (messagesRef.current) {
222
+ const scrollToBottom = useCallback((force = false) => {
223
+ if (messagesRef.current && (force || shouldAutoScroll)) {
95
224
  messagesRef.current.scrollTop = messagesRef.current.scrollHeight
225
+ setShouldAutoScroll(true)
96
226
  }
97
- }, [])
227
+ }, [shouldAutoScroll])
98
228
 
99
- // 消息变化时滚动
229
+ // 消息变化时滚动(只在用户在底部时)
100
230
  useEffect(() => {
101
231
  scrollToBottom()
102
232
  }, [messages, scrollToBottom])
103
233
 
234
+ // 发送新消息时强制滚动到底部
235
+ const prevIsLoadingRef = useRef(isLoading)
236
+ useEffect(() => {
237
+ // 开始加载时(发送消息时)强制滚动到底部
238
+ if (isLoading && !prevIsLoadingRef.current) {
239
+ scrollToBottom(true)
240
+ }
241
+ prevIsLoadingRef.current = isLoading
242
+ }, [isLoading, scrollToBottom])
243
+
104
244
  // 发送消息
105
- const handleSend = useCallback(
106
- (text: string) => {
107
- sendMessage(text)
108
- },
109
- [sendMessage]
110
- )
245
+ const handleSend = useCallback((text: string) => {
246
+ sendMessage(text)
247
+ }, [sendMessage])
248
+
249
+ // @ 上下文
250
+ const handleAtContext = useCallback(() => {
251
+ // TODO: 实现 @ 上下文
252
+ console.log('@ 上下文')
253
+ }, [])
111
254
 
112
255
  // 快捷操作
113
256
  const handleQuickAction = useCallback(
@@ -119,7 +262,7 @@ export const ChatPanel: FC<ChatPanelProps> = ({
119
262
 
120
263
  // 重新发送(编辑后)
121
264
  const handleResend = useCallback(
122
- (index: number, text: string) => {
265
+ (_index: number, text: string) => {
123
266
  // 删除当前消息及后续消息,然后重新发送
124
267
  // 这里简化处理,实际需要更完善的逻辑
125
268
  sendMessage(text)
@@ -132,103 +275,216 @@ export const ChatPanel: FC<ChatPanelProps> = ({
132
275
  onClose?.()
133
276
  }, [onClose])
134
277
 
278
+ // 设置
279
+ const handleSettings = useCallback(() => {
280
+ setSettingsPanelVisible(true)
281
+ }, [])
282
+
283
+ // 保存设置(静默保存)
284
+ const handleSaveSettings = useCallback(async (config: AutoRunConfig) => {
285
+ try {
286
+ await saveAutoRunConfig(config)
287
+ } catch (error) {
288
+ console.error('保存设置失败:', error)
289
+ showToast('保存设置失败', 'error')
290
+ }
291
+ }, [saveAutoRunConfig, showToast])
292
+
293
+ // 模式变更现在在 ToolCallPart 中处理,不再需要此函数
294
+
135
295
  // 清空所有对话
136
296
  const handleClearAll = useCallback(() => {
137
- console.log('清空所有对话')
138
- }, [])
297
+ showConfirm({
298
+ title: '清空所有对话',
299
+ message: '确定要清空所有对话吗?此操作不可恢复。',
300
+ type: 'danger',
301
+ confirmText: '清空',
302
+ onConfirm: () => clearAllSessions(),
303
+ })
304
+ }, [showConfirm, clearAllSessions])
139
305
 
140
306
  // 关闭其他对话
141
- const handleCloseOthers = useCallback(() => {
142
- console.log('关闭其他对话')
143
- }, [])
307
+ const handleCloseOthers = useCallback(async () => {
308
+ await hideOtherSessions()
309
+ }, [hideOtherSessions])
144
310
 
145
311
  // 导出对话
146
312
  const handleExport = useCallback(() => {
147
- console.log('导出对话')
148
- }, [])
313
+ const data = exportCurrentSession()
314
+ if (!data) {
315
+ showToast('当前会话没有内容可导出', 'warning')
316
+ return
317
+ }
318
+
319
+ // 创建下载链接
320
+ const blob = new Blob([data], { type: 'application/json' })
321
+ const url = URL.createObjectURL(blob)
322
+ const a = document.createElement('a')
323
+ const session = sessions.find((s) => s.id === currentSessionId)
324
+ const filename = `chat-${session?.title || 'export'}-${new Date().toISOString().slice(0, 10)}.json`
325
+ a.href = url
326
+ a.download = filename
327
+ document.body.appendChild(a)
328
+ a.click()
329
+ document.body.removeChild(a)
330
+ URL.revokeObjectURL(url)
331
+ }, [exportCurrentSession, sessions, currentSessionId, showToast])
149
332
 
150
- // 复制请求 ID
151
- const handleCopyId = useCallback(() => {
152
- if (currentSessionId) {
153
- navigator.clipboard.writeText(currentSessionId)
154
- console.log('已复制请求 ID:', currentSessionId)
333
+ // 复制会话 ID
334
+ const handleCopyId = useCallback(async () => {
335
+ if (!currentSessionId) return
336
+ try {
337
+ await navigator.clipboard.writeText(currentSessionId)
338
+ showToast('已复制会话 ID', 'success')
339
+ } catch (error) {
340
+ console.error('复制失败:', error)
155
341
  }
156
- }, [currentSessionId])
342
+ }, [currentSessionId, showToast])
157
343
 
158
344
  // 反馈
159
345
  const handleFeedback = useCallback(() => {
160
346
  console.log('反馈')
161
347
  }, [])
162
348
 
163
- // 设置
164
- const handleSettings = useCallback(() => {
165
- console.log('Agent 设置')
166
- }, [])
349
+ // 全局 input 状态 context value
350
+ const inputContextValue = useMemo(
351
+ () => ({
352
+ mode,
353
+ model,
354
+ models,
355
+ webSearch,
356
+ thinking,
357
+ isLoading,
358
+ adapter,
359
+ // cwd 已解耦,不再通过 prop 传递
360
+ // FileBrowser 会直接通过 adapter.setCwd() 同步到 Agent
361
+ // getCwdTool 会自动从 Agent 的 context.cwd 读取最新值
362
+ setMode,
363
+ setModel,
364
+ setWebSearch,
365
+ setThinking,
366
+ }),
367
+ [mode, model, models, webSearch, thinking, isLoading, adapter, setMode, setModel, setWebSearch, setThinking]
368
+ )
167
369
 
168
370
  return (
169
- <div className={`chat-panel ${className}`.trim()}>
170
- {/* 顶部标题栏 */}
171
- {!hideHeader && (
172
- <ChatHeader
173
- sessions={sessions}
174
- currentSessionId={currentSessionId}
175
- showClose={!!onClose}
176
- onNewSession={createNewSession}
177
- onSwitchSession={switchSession}
178
- onDeleteSession={deleteSession}
179
- onClose={handleClose}
180
- onClearAll={handleClearAll}
181
- onCloseOthers={handleCloseOthers}
182
- onExport={handleExport}
183
- onCopyId={handleCopyId}
184
- onFeedback={handleFeedback}
185
- onSettings={handleSettings}
371
+ <ChatInputProvider value={inputContextValue}>
372
+ <RenderersProvider blockRenderers={{}} toolRenderers={toolRenderers}>
373
+ <div className={`chat-panel ${className}`.trim()}>
374
+ {/* 确认弹窗 */}
375
+ <ConfirmDialog
376
+ visible={confirmDialog.visible}
377
+ title={confirmDialog.title}
378
+ message={confirmDialog.message}
379
+ type={confirmDialog.type}
380
+ confirmText={confirmDialog.confirmText}
381
+ onConfirm={() => {
382
+ setConfirmDialog((prev) => ({ ...prev, visible: false }))
383
+ confirmDialog.onConfirm()
384
+ }}
385
+ onCancel={() => setConfirmDialog((prev) => ({ ...prev, visible: false }))}
186
386
  />
187
- )}
188
-
189
- {/* 消息列表 */}
190
- <div ref={messagesRef} className="messages-container">
191
- {messages.length === 0 ? (
192
- <WelcomeMessage onQuickAction={handleQuickAction} />
193
- ) : (
194
- messages.map((msg, index) => (
195
- <MessageBubble
196
- key={msg.id}
197
- role={msg.role}
198
- content={msg.content}
199
- images={msg.images}
200
- thinking={msg.thinking}
201
- thinkingComplete={msg.thinkingComplete}
202
- searchResults={msg.searchResults}
203
- searching={msg.searching}
204
- toolCalls={msg.toolCalls}
205
- copied={msg.copied}
206
- loading={msg.loading}
207
- onCopy={() => copyMessage(msg.id)}
208
- onRegenerate={() => regenerateMessage(index)}
209
- onSend={(text) => handleResend(index, text)}
210
- renderMarkdown={renderMarkdown}
211
- />
212
- ))
387
+
388
+ {/* Toast 消息 */}
389
+ <Toast
390
+ visible={toast.visible}
391
+ message={toast.message}
392
+ type={toast.type}
393
+ onClose={() => setToast((prev) => ({ ...prev, visible: false }))}
394
+ />
395
+
396
+ {/* 工具批准现在内嵌在 ToolCallPart 中,不再需要全局对话框 */}
397
+
398
+ {/* 设置面板 */}
399
+ <SettingsPanel
400
+ visible={settingsPanelVisible}
401
+ config={autoRunConfig}
402
+ onChange={handleSaveSettings}
403
+ onClose={() => setSettingsPanelVisible(false)}
404
+ />
405
+
406
+ {/* 顶部标题栏 */}
407
+ {!hideHeader && (
408
+ <ChatHeader
409
+ sessions={sessions}
410
+ currentSessionId={currentSessionId}
411
+ showClose={!!onClose}
412
+ onNewSession={createNewSession}
413
+ onSwitchSession={switchSession}
414
+ onDeleteSession={deleteSession}
415
+ onHideSession={hideSession}
416
+ onClose={handleClose}
417
+ onClearAll={handleClearAll}
418
+ onCloseOthers={handleCloseOthers}
419
+ onExport={handleExport}
420
+ onCopyId={handleCopyId}
421
+ onFeedback={handleFeedback}
422
+ onSettings={handleSettings}
423
+ />
213
424
  )}
214
- </div>
215
-
216
- {/* 输入区域 */}
217
- <ChatInput
218
- selectedImages={[]}
219
- isLoading={isLoading}
220
- mode={mode}
221
- model={model}
222
- models={models}
223
- webSearchEnabled={webSearch}
224
- thinkingEnabled={thinking}
225
- onSend={handleSend}
226
- onCancel={cancelRequest}
227
- onModeChange={setMode}
228
- onModelChange={setModel}
229
- onWebSearchChange={setWebSearch}
230
- onThinkingChange={setThinking}
231
- />
232
- </div>
425
+
426
+ {/* 消息列表容器 */}
427
+ <div className="messages-wrapper">
428
+ <div ref={messagesRef} className="messages-container chat-scrollbar" onScroll={handleScroll}>
429
+ {messages.length === 0 ? (
430
+ <WelcomeMessage config={welcomeConfig} onQuickAction={handleQuickAction} />
431
+ ) : (
432
+ messages.map((msg, index) => (
433
+ <MessageBubble
434
+ key={msg.id}
435
+ role={msg.role}
436
+ parts={msg.parts}
437
+ model={msg.model}
438
+ mode={msg.mode}
439
+ images={msg.images}
440
+ copied={msg.copied}
441
+ loading={msg.loading}
442
+ timestamp={msg.timestamp}
443
+ stepsExpandedType={stepsExpandedType}
444
+ adapter={adapter}
445
+ autoRunConfig={autoRunConfig}
446
+ onSaveConfig={saveAutoRunConfig}
447
+ onCopy={() => copyMessage(msg.id)}
448
+ onRegenerate={() => regenerateMessage(index)}
449
+ onSend={(text) => handleResend(index, text)}
450
+ />
451
+ ))
452
+ )}
453
+ </div>
454
+ {/* 滚动到底部按钮 */}
455
+ {!shouldAutoScroll && messages.length > 0 && (
456
+ <button
457
+ className="scroll-to-bottom-btn"
458
+ onClick={() => scrollToBottom(true)}
459
+ title="滚动到底部"
460
+ >
461
+ <Icon icon="lucide:arrow-down" width={16} />
462
+ </button>
463
+ )}
464
+ </div>
465
+
466
+ {/* 输入区域 */}
467
+ <ChatInput
468
+ ref={inputRef}
469
+ isLoading={isLoading}
470
+ mode={mode}
471
+ model={model}
472
+ models={models}
473
+ webSearchEnabled={webSearch}
474
+ thinkingEnabled={thinking}
475
+ onSend={handleSend}
476
+ onCancel={cancelRequest}
477
+ onModeChange={setMode}
478
+ onModelChange={setModel}
479
+ onWebSearchChange={setWebSearch}
480
+ onThinkingChange={setThinking}
481
+ onAtContext={handleAtContext}
482
+ />
483
+ </div>
484
+ </RenderersProvider>
485
+ </ChatInputProvider>
233
486
  )
234
- }
487
+ })
488
+
489
+ // 添加 displayName 以便于调试
490
+ ChatPanel.displayName = 'ChatPanel'