@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.
- package/README.md +368 -0
- package/dist/index.css +2575 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +378 -135
- package/dist/index.js +3956 -1042
- package/dist/index.js.map +1 -1
- package/dist/style.css +48 -987
- package/package.json +7 -4
- package/src/adapter.ts +10 -70
- package/src/components/ChatPanel.tsx +373 -117
- package/src/components/common/ConfirmDialog.css +136 -0
- package/src/components/common/ConfirmDialog.tsx +91 -0
- package/src/components/common/CopyButton.css +22 -0
- package/src/components/common/CopyButton.tsx +46 -0
- package/src/components/common/IndexingSettings.css +207 -0
- package/src/components/common/IndexingSettings.tsx +398 -0
- package/src/components/common/SettingsPanel.css +256 -0
- package/src/components/common/SettingsPanel.tsx +120 -0
- package/src/components/common/Toast.css +50 -0
- package/src/components/common/Toast.tsx +38 -0
- package/src/components/common/ToggleSwitch.css +52 -0
- package/src/components/common/ToggleSwitch.tsx +20 -0
- package/src/components/header/ChatHeader.css +285 -0
- package/src/components/header/ChatHeader.tsx +376 -0
- package/src/components/input/AtFilePicker.css +147 -0
- package/src/components/input/AtFilePicker.tsx +519 -0
- package/src/components/input/ChatInput.css +204 -0
- package/src/components/input/ChatInput.tsx +506 -0
- package/src/components/input/DropdownSelector.css +159 -0
- package/src/components/input/DropdownSelector.tsx +195 -0
- package/src/components/input/ImagePreviewModal.css +124 -0
- package/src/components/input/ImagePreviewModal.tsx +118 -0
- package/src/components/input/at-views/AtBranchView.tsx +34 -0
- package/src/components/input/at-views/AtBrowserView.tsx +34 -0
- package/src/components/input/at-views/AtChatsView.tsx +34 -0
- package/src/components/input/at-views/AtDocsView.tsx +34 -0
- package/src/components/input/at-views/AtFilesView.tsx +168 -0
- package/src/components/input/at-views/AtTerminalsView.tsx +34 -0
- package/src/components/input/at-views/AtViewStyles.css +143 -0
- package/src/components/input/at-views/index.ts +9 -0
- package/src/components/message/ContentRenderer.css +9 -0
- package/src/components/message/ContentRenderer.tsx +63 -0
- package/src/components/message/MessageBubble.css +190 -0
- package/src/components/message/MessageBubble.tsx +231 -0
- package/src/components/message/PartsRenderer.css +4 -0
- package/src/components/message/PartsRenderer.tsx +114 -0
- package/src/components/message/ToolResultRenderer.tsx +21 -0
- package/src/components/message/WelcomeMessage.css +221 -0
- package/src/components/message/WelcomeMessage.tsx +93 -0
- package/src/components/message/blocks/CodeBlock.tsx +60 -0
- package/src/components/message/blocks/TextBlock.tsx +15 -0
- package/src/components/message/blocks/blocks.css +141 -0
- package/src/components/message/blocks/index.ts +6 -0
- package/src/components/message/parts/CollapsibleCard.css +78 -0
- package/src/components/message/parts/CollapsibleCard.tsx +77 -0
- package/src/components/message/parts/ErrorPart.css +9 -0
- package/src/components/message/parts/ErrorPart.tsx +40 -0
- package/src/components/message/parts/ImagePart.css +50 -0
- package/src/components/message/parts/ImagePart.tsx +54 -0
- package/src/components/message/parts/SearchPart.css +44 -0
- package/src/components/message/parts/SearchPart.tsx +63 -0
- package/src/components/message/parts/TextPart.css +10 -0
- package/src/components/message/parts/TextPart.tsx +20 -0
- package/src/components/message/parts/ThinkingPart.css +9 -0
- package/src/components/message/parts/ThinkingPart.tsx +48 -0
- package/src/components/message/parts/ToolCallPart.css +220 -0
- package/src/components/message/parts/ToolCallPart.tsx +285 -0
- package/src/components/message/parts/ToolResultPart.css +68 -0
- package/src/components/message/parts/ToolResultPart.tsx +96 -0
- package/src/components/message/parts/index.ts +11 -0
- package/src/components/message/tool-results/DefaultToolResult.tsx +26 -0
- package/src/components/message/tool-results/SearchResults.tsx +69 -0
- package/src/components/message/tool-results/WeatherCard.tsx +63 -0
- package/src/components/message/tool-results/index.ts +7 -0
- package/src/components/message/tool-results/tool-results.css +179 -0
- package/src/components/message/welcome-types.ts +46 -0
- package/src/context/AutoRunConfigContext.tsx +13 -0
- package/src/context/ChatAdapterContext.tsx +8 -0
- package/src/context/ChatInputContext.tsx +40 -0
- package/src/context/RenderersContext.tsx +41 -0
- package/src/hooks/useChat.ts +855 -237
- package/src/hooks/useImageUpload.ts +253 -0
- package/src/index.ts +96 -39
- package/src/styles.css +48 -987
- package/src/types/index.ts +172 -103
- package/src/utils/fileIcon.ts +49 -0
- package/src/components/ChatInput.tsx +0 -368
- package/src/components/chat/messages/ExecutionSteps.tsx +0 -234
- package/src/components/chat/messages/MessageBubble.tsx +0 -130
- package/src/components/chat/ui/ChatHeader.tsx +0 -301
- 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,
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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
|
|
18
|
-
/** 工作目录 */
|
|
19
|
-
workingDir?: string
|
|
40
|
+
adapter: ChatAdapter
|
|
20
41
|
/** 默认模型 */
|
|
21
42
|
defaultModel?: string
|
|
22
43
|
/** 默认模式 */
|
|
23
44
|
defaultMode?: ChatMode
|
|
24
45
|
/** 可用模型列表 */
|
|
25
|
-
models?:
|
|
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
|
-
/**
|
|
33
|
-
|
|
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
|
|
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
|
|
72
|
+
models: propModels,
|
|
42
73
|
hideHeader = false,
|
|
43
74
|
onClose,
|
|
75
|
+
onToolComplete,
|
|
44
76
|
className = '',
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}, [
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
}, [])
|
|
307
|
+
const handleCloseOthers = useCallback(async () => {
|
|
308
|
+
await hideOtherSessions()
|
|
309
|
+
}, [hideOtherSessions])
|
|
144
310
|
|
|
145
311
|
// 导出对话
|
|
146
312
|
const handleExport = useCallback(() => {
|
|
147
|
-
|
|
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
|
-
//
|
|
151
|
-
const handleCopyId = useCallback(() => {
|
|
152
|
-
if (currentSessionId)
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
165
|
-
|
|
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
|
-
<
|
|
170
|
-
{
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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'
|