@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.
- package/dist/index.css +0 -1
- package/dist/index.js +1 -5418
- package/package.json +4 -5
- package/dist/index.css.map +0 -1
- package/dist/index.js.map +0 -1
- package/src/adapter.ts +0 -68
- package/src/components/ChatPanel.tsx +0 -553
- package/src/components/common/ConfirmDialog.css +0 -136
- package/src/components/common/ConfirmDialog.tsx +0 -91
- package/src/components/common/CopyButton.css +0 -22
- package/src/components/common/CopyButton.tsx +0 -46
- package/src/components/common/IndexingSettings.css +0 -207
- package/src/components/common/IndexingSettings.tsx +0 -398
- package/src/components/common/SettingsPanel.css +0 -337
- package/src/components/common/SettingsPanel.tsx +0 -215
- package/src/components/common/Toast.css +0 -50
- package/src/components/common/Toast.tsx +0 -38
- package/src/components/common/ToggleSwitch.css +0 -52
- package/src/components/common/ToggleSwitch.tsx +0 -20
- package/src/components/header/ChatHeader.css +0 -285
- package/src/components/header/ChatHeader.tsx +0 -376
- package/src/components/input/AtFilePicker.css +0 -147
- package/src/components/input/AtFilePicker.tsx +0 -519
- package/src/components/input/ChatInput.css +0 -283
- package/src/components/input/ChatInput.tsx +0 -575
- package/src/components/input/DropdownSelector.css +0 -231
- package/src/components/input/DropdownSelector.tsx +0 -333
- package/src/components/input/ImagePreviewModal.css +0 -124
- package/src/components/input/ImagePreviewModal.tsx +0 -118
- package/src/components/input/at-views/AtBranchView.tsx +0 -34
- package/src/components/input/at-views/AtBrowserView.tsx +0 -34
- package/src/components/input/at-views/AtChatsView.tsx +0 -34
- package/src/components/input/at-views/AtDocsView.tsx +0 -34
- package/src/components/input/at-views/AtFilesView.tsx +0 -168
- package/src/components/input/at-views/AtTerminalsView.tsx +0 -34
- package/src/components/input/at-views/AtViewStyles.css +0 -143
- package/src/components/input/at-views/index.ts +0 -9
- package/src/components/message/ContentRenderer.css +0 -9
- package/src/components/message/MessageBubble.css +0 -193
- package/src/components/message/MessageBubble.tsx +0 -240
- package/src/components/message/PartsRenderer.css +0 -12
- package/src/components/message/PartsRenderer.tsx +0 -168
- package/src/components/message/WelcomeMessage.css +0 -221
- package/src/components/message/WelcomeMessage.tsx +0 -93
- package/src/components/message/parts/CollapsibleCard.css +0 -80
- package/src/components/message/parts/CollapsibleCard.tsx +0 -80
- package/src/components/message/parts/ErrorPart.css +0 -9
- package/src/components/message/parts/ErrorPart.tsx +0 -40
- package/src/components/message/parts/ImagePart.css +0 -49
- package/src/components/message/parts/ImagePart.tsx +0 -54
- package/src/components/message/parts/SearchPart.css +0 -44
- package/src/components/message/parts/SearchPart.tsx +0 -63
- package/src/components/message/parts/TextPart.css +0 -579
- package/src/components/message/parts/TextPart.tsx +0 -213
- package/src/components/message/parts/ThinkingPart.css +0 -9
- package/src/components/message/parts/ThinkingPart.tsx +0 -48
- package/src/components/message/parts/ToolCallPart.css +0 -246
- package/src/components/message/parts/ToolCallPart.tsx +0 -289
- package/src/components/message/parts/ToolResultPart.css +0 -67
- package/src/components/message/parts/index.ts +0 -13
- package/src/components/message/parts/visual-predicate.ts +0 -43
- package/src/components/message/parts/visual-render.ts +0 -19
- package/src/components/message/parts/visual.ts +0 -12
- package/src/components/message/welcome-types.ts +0 -46
- package/src/context/AutoRunConfigContext.tsx +0 -13
- package/src/context/ChatAdapterContext.tsx +0 -8
- package/src/context/ChatInputContext.tsx +0 -40
- package/src/context/RenderersContext.tsx +0 -35
- package/src/hooks/useChat.ts +0 -1569
- package/src/hooks/useImageUpload.ts +0 -345
- package/src/hooks/useVoiceInput.ts +0 -454
- package/src/hooks/useVoiceToTextInput.ts +0 -87
- package/src/index.ts +0 -151
- package/src/styles.css +0 -330
- package/src/types/index.ts +0 -196
- 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'
|