@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
@@ -1,368 +0,0 @@
1
- /**
2
- * ChatInput Component
3
- * 与 Vue 版本 ChatInput.vue 保持一致
4
- */
5
-
6
- import { useState, useRef, useCallback, useEffect, type FC } from 'react'
7
- import {
8
- X,
9
- ChevronDown,
10
- Check,
11
- Globe,
12
- Sparkles,
13
- ImageIcon,
14
- Square,
15
- ArrowUp,
16
- Zap,
17
- MessageCircle,
18
- AtSign,
19
- Mic,
20
- } from 'lucide-react'
21
- import type { ChatMode, ModelConfig } from '../types'
22
- import { DEFAULT_MODELS } from '../types'
23
-
24
- interface ChatInputProps {
25
- /** 变体模式:input-底部输入框,message-历史消息 */
26
- variant?: 'input' | 'message'
27
- /** 受控值(用于历史消息编辑) */
28
- value?: string
29
- selectedImages?: string[]
30
- isLoading?: boolean
31
- mode?: ChatMode
32
- model?: string
33
- models?: ModelConfig[]
34
- webSearchEnabled?: boolean
35
- thinkingEnabled?: boolean
36
- onSend?: (text: string) => void
37
- onRemoveImage?: (index: number) => void
38
- onCancel?: () => void
39
- onUploadImage?: () => void
40
- onAtContext?: () => void
41
- onModeChange?: (mode: ChatMode) => void
42
- onModelChange?: (model: string) => void
43
- onWebSearchChange?: (enabled: boolean) => void
44
- onThinkingChange?: (enabled: boolean) => void
45
- }
46
-
47
- /** 模式配置 */
48
- const MODES = [
49
- { value: 'agent' as const, label: 'Agent', Icon: Zap },
50
- { value: 'ask' as const, label: 'Ask', Icon: MessageCircle },
51
- ]
52
-
53
- export const ChatInput: FC<ChatInputProps> = ({
54
- variant = 'input',
55
- value = '',
56
- selectedImages = [],
57
- isLoading = false,
58
- mode = 'agent',
59
- model = '',
60
- models = DEFAULT_MODELS,
61
- webSearchEnabled = false,
62
- thinkingEnabled = false,
63
- onSend,
64
- onRemoveImage,
65
- onCancel,
66
- onUploadImage,
67
- onAtContext,
68
- onModeChange,
69
- onModelChange,
70
- onWebSearchChange,
71
- onThinkingChange,
72
- }) => {
73
- const isMessageVariant = variant === 'message'
74
-
75
- const [inputText, setInputText] = useState(value)
76
- const [isFocused, setIsFocused] = useState(false)
77
- const [modeMenuOpen, setModeMenuOpen] = useState(false)
78
- const [modelMenuOpen, setModelMenuOpen] = useState(false)
79
-
80
- const inputRef = useRef<HTMLTextAreaElement>(null)
81
- const containerRef = useRef<HTMLDivElement>(null)
82
-
83
- // 同步外部 value
84
- useEffect(() => {
85
- setInputText(value)
86
- }, [value])
87
-
88
- const currentMode = MODES.find((m) => m.value === mode) || MODES[0]
89
- const currentModel = models.find((m) => m.model === model)
90
-
91
- // 是否显示工具栏
92
- const showToolbar = !isMessageVariant || isFocused
93
-
94
- // 预览
95
- const selectedPreview = selectedImages.slice(0, 3)
96
-
97
- // 占位符
98
- const placeholder = selectedImages.length > 0
99
- ? '描述你想要的效果...'
100
- : mode === 'ask'
101
- ? '有什么问题想问我?'
102
- : '描述任务,@ 添加上下文'
103
-
104
- // 自动调整高度
105
- const adjustTextareaHeight = useCallback(() => {
106
- if (inputRef.current) {
107
- inputRef.current.style.height = 'auto'
108
- const scrollHeight = inputRef.current.scrollHeight
109
- inputRef.current.style.height = `${Math.min(scrollHeight, 150)}px`
110
- }
111
- }, [])
112
-
113
- // 发送或取消
114
- const handleSendOrCancel = useCallback(() => {
115
- if (isLoading) {
116
- onCancel?.()
117
- return
118
- }
119
-
120
- const text = inputText.trim()
121
- if (!text) return
122
-
123
- onSend?.(text)
124
-
125
- if (!isMessageVariant) {
126
- setInputText('')
127
- if (inputRef.current) {
128
- inputRef.current.style.height = 'auto'
129
- }
130
- inputRef.current?.focus()
131
- } else {
132
- setIsFocused(false)
133
- }
134
- }, [isLoading, inputText, onSend, onCancel, isMessageVariant])
135
-
136
- // 键盘事件
137
- const handleKeydown = useCallback(
138
- (event: React.KeyboardEvent) => {
139
- if (event.key === 'Enter' && !event.shiftKey) {
140
- event.preventDefault()
141
- handleSendOrCancel()
142
- } else {
143
- setTimeout(adjustTextareaHeight, 0)
144
- }
145
- },
146
- [handleSendOrCancel, adjustTextareaHeight]
147
- )
148
-
149
- // 选择模式
150
- const selectMode = useCallback(
151
- (value: ChatMode) => {
152
- onModeChange?.(value)
153
- setModeMenuOpen(false)
154
- },
155
- [onModeChange]
156
- )
157
-
158
- // 选择模型
159
- const selectModel = useCallback(
160
- (m: ModelConfig) => {
161
- onModelChange?.(m.model)
162
- setModelMenuOpen(false)
163
- },
164
- [onModelChange]
165
- )
166
-
167
- // 图片 URL 处理
168
- const getImageUrl = (path: string): string => {
169
- if (
170
- path.startsWith('app://') ||
171
- path.startsWith('file://') ||
172
- path.startsWith('data:') ||
173
- path.startsWith('http')
174
- ) {
175
- return path
176
- }
177
- if (path.match(/^[A-Z]:\\/i)) {
178
- return `app://file${encodeURIComponent(path.replace(/\\/g, '/'))}`
179
- }
180
- return `app://file${encodeURIComponent(path)}`
181
- }
182
-
183
- // 点击外部关闭菜单
184
- useEffect(() => {
185
- const handleClickOutside = (event: MouseEvent) => {
186
- const target = event.target as HTMLElement
187
- if (!target.closest('.selector')) {
188
- setModeMenuOpen(false)
189
- setModelMenuOpen(false)
190
- }
191
- if (
192
- isMessageVariant &&
193
- containerRef.current &&
194
- !containerRef.current.contains(target)
195
- ) {
196
- setIsFocused(false)
197
- }
198
- }
199
-
200
- document.addEventListener('click', handleClickOutside)
201
- return () => document.removeEventListener('click', handleClickOutside)
202
- }, [isMessageVariant])
203
-
204
- const CurrentModeIcon = currentMode.Icon
205
-
206
- return (
207
- <div className={`chat-input${isMessageVariant ? ' message-variant' : ''}`}>
208
- <div
209
- ref={containerRef}
210
- className={`input-container${isFocused ? ' focused' : ''}`}
211
- >
212
- {/* 附件预览 */}
213
- {selectedImages.length > 0 && (
214
- <div className="attachment-preview">
215
- <div className="preview-images">
216
- {selectedPreview.map((img, index) => (
217
- <div key={`${img}-${index}`} className="preview-item">
218
- <img
219
- src={getImageUrl(img)}
220
- className="preview-thumb"
221
- alt={`附件 ${index + 1}`}
222
- onError={(e) => {
223
- (e.target as HTMLImageElement).style.display = 'none'
224
- }}
225
- />
226
- {!isMessageVariant && (
227
- <button
228
- className="remove-btn"
229
- title={`移除图片 ${index + 1}`}
230
- onClick={() => onRemoveImage?.(index)}
231
- >
232
- <X size={10} />
233
- </button>
234
- )}
235
- </div>
236
- ))}
237
- {selectedImages.length > 3 && (
238
- <div className="preview-more">+{selectedImages.length - 3}</div>
239
- )}
240
- </div>
241
- </div>
242
- )}
243
-
244
- {/* 输入框 */}
245
- <div className="input-field-wrapper">
246
- <textarea
247
- ref={inputRef}
248
- value={inputText}
249
- onChange={(e) => setInputText(e.target.value)}
250
- onKeyDown={handleKeydown}
251
- onInput={adjustTextareaHeight}
252
- onFocus={() => setIsFocused(true)}
253
- placeholder={placeholder}
254
- rows={1}
255
- className="input-field"
256
- />
257
- </div>
258
-
259
- {/* 底部控制栏 */}
260
- {showToolbar && (
261
- <div className="input-controls">
262
- {/* 左侧:模式和模型选择 */}
263
- <div className="input-left">
264
- {/* 模式选择 */}
265
- <div
266
- className="selector mode-selector"
267
- onClick={(e) => {
268
- e.stopPropagation()
269
- setModeMenuOpen(!modeMenuOpen)
270
- setModelMenuOpen(false)
271
- }}
272
- >
273
- <CurrentModeIcon size={12} />
274
- <span>{currentMode.label}</span>
275
- <ChevronDown size={10} className="chevron" />
276
-
277
- {modeMenuOpen && (
278
- <div className="dropdown-menu" onClick={(e) => e.stopPropagation()}>
279
- {MODES.map((m) => (
280
- <button
281
- key={m.value}
282
- className={`dropdown-item${mode === m.value ? ' active' : ''}`}
283
- onClick={() => selectMode(m.value)}
284
- >
285
- <m.Icon size={14} />
286
- <span>{m.label}</span>
287
- {mode === m.value && <Check size={14} className="check-icon" />}
288
- </button>
289
- ))}
290
- </div>
291
- )}
292
- </div>
293
-
294
- {/* 模型选择 */}
295
- <div
296
- className="selector model-selector"
297
- onClick={(e) => {
298
- e.stopPropagation()
299
- setModelMenuOpen(!modelMenuOpen)
300
- setModeMenuOpen(false)
301
- }}
302
- >
303
- <span>{currentModel?.displayName || 'Auto'}</span>
304
- <ChevronDown size={10} className="chevron" />
305
-
306
- {modelMenuOpen && (
307
- <div className="dropdown-menu" onClick={(e) => e.stopPropagation()}>
308
- {models.map((m) => (
309
- <button
310
- key={m.model}
311
- className={`dropdown-item${model === m.model ? ' active' : ''}`}
312
- onClick={() => selectModel(m)}
313
- >
314
- <span>{m.displayName}</span>
315
- {model === m.model && <Check size={14} className="check-icon" />}
316
- </button>
317
- ))}
318
- </div>
319
- )}
320
- </div>
321
- </div>
322
-
323
- {/* 右侧:功能按钮 */}
324
- <div className="input-right">
325
- <button className="icon-btn" title="提及上下文 (@)" onClick={onAtContext}>
326
- <AtSign size={14} />
327
- </button>
328
-
329
- <button
330
- className={`toggle-btn${thinkingEnabled ? ' active' : ''}`}
331
- title="深度思考"
332
- onClick={() => onThinkingChange?.(!thinkingEnabled)}
333
- >
334
- <Sparkles size={14} />
335
- </button>
336
-
337
- <button
338
- className={`toggle-btn${webSearchEnabled ? ' active' : ''}`}
339
- title="联网搜索"
340
- onClick={() => onWebSearchChange?.(!webSearchEnabled)}
341
- >
342
- <Globe size={14} />
343
- </button>
344
-
345
- <button className="icon-btn" title="上传图片" onClick={onUploadImage}>
346
- <ImageIcon size={14} />
347
- </button>
348
-
349
- {inputText.trim() || isLoading ? (
350
- <button
351
- className={`send-btn${isLoading ? ' loading' : ''}`}
352
- title={isLoading ? '停止' : isMessageVariant ? '重新发送' : '发送'}
353
- onClick={handleSendOrCancel}
354
- >
355
- {isLoading ? <Square size={14} /> : <ArrowUp size={14} />}
356
- </button>
357
- ) : (
358
- <button className="icon-btn" title="语音输入">
359
- <Mic size={14} />
360
- </button>
361
- )}
362
- </div>
363
- </div>
364
- )}
365
- </div>
366
- </div>
367
- )
368
- }
@@ -1,234 +0,0 @@
1
- /**
2
- * ExecutionSteps Component
3
- * 与 Vue 版本 ExecutionSteps.vue 保持一致
4
- */
5
-
6
- import { useState, type FC, type ReactNode } from 'react'
7
- import {
8
- ChevronDown,
9
- ChevronUp,
10
- Sparkles,
11
- Globe,
12
- FileText,
13
- FileEdit,
14
- Terminal,
15
- Search,
16
- Folder,
17
- FolderPlus,
18
- Trash2,
19
- Image,
20
- Video,
21
- Wrench,
22
- ExternalLink,
23
- } from 'lucide-react'
24
- import type { SearchResult, ToolCall } from '../../../types'
25
-
26
- interface ExecutionStepsProps {
27
- /** 是否正在加载 */
28
- loading?: boolean
29
- /** 是否有消息内容 */
30
- hasContent?: boolean
31
- /** 思考内容 */
32
- thinking?: string
33
- /** 思考是否完成 */
34
- thinkingComplete?: boolean
35
- /** 思考耗时 */
36
- thinkingDuration?: number
37
- /** 是否正在搜索 */
38
- searching?: boolean
39
- /** 搜索结果 */
40
- searchResults?: SearchResult[]
41
- /** 工具调用列表 */
42
- toolCalls?: ToolCall[]
43
- }
44
-
45
- /** 步骤项组件 */
46
- interface StepItemProps {
47
- icon: ReactNode
48
- title: string
49
- status: 'running' | 'completed' | 'error'
50
- extra?: string
51
- detail?: ReactNode
52
- defaultExpanded?: boolean
53
- }
54
-
55
- const StepItem: FC<StepItemProps> = ({
56
- icon,
57
- title,
58
- status,
59
- extra,
60
- detail,
61
- defaultExpanded = false,
62
- }) => {
63
- const [expanded, setExpanded] = useState(defaultExpanded)
64
- const isRunning = status === 'running'
65
- const hasDetail = !!detail
66
-
67
- return (
68
- <div className="step-item">
69
- <button
70
- className={`step-header${isRunning ? ' running' : ''}`}
71
- onClick={() => hasDetail && setExpanded(!expanded)}
72
- disabled={!hasDetail}
73
- style={{ cursor: hasDetail ? 'pointer' : 'default' }}
74
- >
75
- <span className={`step-icon${isRunning ? ' pulse' : ''}`}>{icon}</span>
76
- <span className="step-title">{title}</span>
77
- {hasDetail && (
78
- expanded ? <ChevronUp className="step-chevron" size={12} /> : <ChevronDown className="step-chevron" size={12} />
79
- )}
80
- {extra && <span className="step-extra">{extra}</span>}
81
- </button>
82
-
83
- {expanded && detail && (
84
- <div className="step-detail">
85
- {typeof detail === 'string' ? <pre>{detail}</pre> : detail}
86
- </div>
87
- )}
88
- </div>
89
- )
90
- }
91
-
92
- /** 获取工具调用的显示名称 */
93
- function getToolDisplayName(name: string): string {
94
- const nameMap: Record<string, string> = {
95
- read_file: '读取文件',
96
- write_file: '写入文件',
97
- execute_command: '执行命令',
98
- search_files: '搜索文件',
99
- list_directory: '列出目录',
100
- create_directory: '创建目录',
101
- delete_file: '删除文件',
102
- web_search: '网页搜索',
103
- generate_image: '生成图片',
104
- image_to_video: '图片转视频',
105
- }
106
- return nameMap[name] || name
107
- }
108
-
109
- /** 获取工具调用的图标 */
110
- function getToolIcon(name: string) {
111
- switch (name) {
112
- case 'read_file': return <FileText size={14} />
113
- case 'write_file': return <FileEdit size={14} />
114
- case 'execute_command': return <Terminal size={14} />
115
- case 'search_files': return <Search size={14} />
116
- case 'list_directory': return <Folder size={14} />
117
- case 'create_directory': return <FolderPlus size={14} />
118
- case 'delete_file': return <Trash2 size={14} />
119
- case 'web_search': return <Globe size={14} />
120
- case 'generate_image': return <Image size={14} />
121
- case 'image_to_video': return <Video size={14} />
122
- default: return <Wrench size={14} />
123
- }
124
- }
125
-
126
- /** 格式化工具调用参数 */
127
- function formatToolArgs(args?: Record<string, unknown>): string {
128
- if (!args) return ''
129
- return Object.entries(args)
130
- .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
131
- .join('\n')
132
- }
133
-
134
- export const ExecutionSteps: FC<ExecutionStepsProps> = ({
135
- loading,
136
- hasContent,
137
- thinking,
138
- thinkingComplete = true,
139
- thinkingDuration,
140
- searching,
141
- searchResults,
142
- toolCalls,
143
- }) => {
144
- // 判断是否有任何执行步骤
145
- const hasSteps =
146
- thinking ||
147
- searching ||
148
- (searchResults && searchResults.length > 0) ||
149
- (toolCalls && toolCalls.length > 0) ||
150
- (loading && !hasContent)
151
-
152
- if (!hasSteps) return null
153
-
154
- return (
155
- <div className="execution-steps">
156
- {/* 正在规划 */}
157
- {loading && !hasContent && !thinking && !searching && (!toolCalls || toolCalls.length === 0) && (
158
- <StepItem
159
- icon={<Sparkles size={14} />}
160
- title="正在规划下一步..."
161
- status="running"
162
- />
163
- )}
164
-
165
- {/* 思考过程 */}
166
- {thinking && (
167
- <StepItem
168
- icon={<Sparkles size={14} />}
169
- title={thinkingComplete ? '思考完成' : '思考中...'}
170
- status={thinkingComplete ? 'completed' : 'running'}
171
- extra={thinkingDuration ? `${thinkingDuration}s` : undefined}
172
- detail={thinking}
173
- />
174
- )}
175
-
176
- {/* 搜索 */}
177
- {(searching || (searchResults && searchResults.length > 0)) && (
178
- <StepItem
179
- icon={<Globe size={14} />}
180
- title={searching ? '搜索中...' : `搜索完成 ${searchResults?.length || 0} 条结果`}
181
- status={searching ? 'running' : 'completed'}
182
- detail={
183
- searchResults && searchResults.length > 0 ? (
184
- <div>
185
- {searchResults.map((result, i) => (
186
- <a
187
- key={i}
188
- href={result.url}
189
- target="_blank"
190
- rel="noopener noreferrer"
191
- className="search-result-item"
192
- >
193
- <div className="search-result-title">
194
- <span>{result.title}</span>
195
- <ExternalLink size={12} style={{ opacity: 0.5 }} />
196
- </div>
197
- <div className="search-result-snippet">{result.snippet}</div>
198
- </a>
199
- ))}
200
- </div>
201
- ) : undefined
202
- }
203
- />
204
- )}
205
-
206
- {/* 工具调用 */}
207
- {toolCalls &&
208
- toolCalls.map((call, index) => (
209
- <StepItem
210
- key={`tool-${index}`}
211
- icon={getToolIcon(call.name)}
212
- title={`${getToolDisplayName(call.name)}${call.status === 'running' ? '...' : ''}`}
213
- status={call.status === 'running' ? 'running' : call.status === 'error' ? 'error' : 'completed'}
214
- detail={
215
- <div>
216
- {call.args && (
217
- <div style={{ marginBottom: call.result ? 8 : 0 }}>
218
- <div style={{ fontSize: 10, color: 'var(--chat-text-muted)', marginBottom: 4 }}>参数</div>
219
- <pre style={{ margin: 0 }}>{formatToolArgs(call.args)}</pre>
220
- </div>
221
- )}
222
- {call.result && (
223
- <div>
224
- <div style={{ fontSize: 10, color: 'var(--chat-text-muted)', marginBottom: 4 }}>结果</div>
225
- <pre style={{ margin: 0, maxHeight: 160, overflow: 'auto' }}>{call.result}</pre>
226
- </div>
227
- )}
228
- </div>
229
- }
230
- />
231
- ))}
232
- </div>
233
- )
234
- }