@huyooo/ai-chat-frontend-react 0.1.4 → 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 +3954 -1044
  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 +99 -42
  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
@@ -0,0 +1,48 @@
1
+ import { useState, useEffect, type FC } from 'react'
2
+ import { CollapsibleCard } from './CollapsibleCard'
3
+ import type { StepsExpandedType } from '../../../types'
4
+ import './ThinkingPart.css'
5
+
6
+ interface ThinkingPartProps {
7
+ text: string
8
+ status: 'running' | 'done'
9
+ duration?: number
10
+ expandedType?: StepsExpandedType
11
+ }
12
+
13
+ export const ThinkingPart: FC<ThinkingPartProps> = ({
14
+ text,
15
+ status,
16
+ duration,
17
+ expandedType = 'auto'
18
+ }) => {
19
+ // 根据模式计算初始状态
20
+ const getInitialExpanded = () => {
21
+ if (expandedType === 'open') return true
22
+ if (expandedType === 'close') return false
23
+ // auto: 运行时展开
24
+ return status === 'running'
25
+ }
26
+
27
+ const [expanded, setExpanded] = useState(getInitialExpanded)
28
+
29
+ // auto 模式下:状态变化时自动折叠/展开
30
+ useEffect(() => {
31
+ if (expandedType === 'auto') {
32
+ setExpanded(status === 'running')
33
+ }
34
+ }, [status, expandedType])
35
+
36
+ return (
37
+ <CollapsibleCard
38
+ icon="lucide:lightbulb"
39
+ iconColor="var(--chat-accent, #f59e0b)"
40
+ title={status === 'running' ? '思考中...' : '思考完成'}
41
+ subtitle={duration && status === 'done' ? `(${duration}s)` : undefined}
42
+ expanded={expanded}
43
+ onExpandedChange={setExpanded}
44
+ >
45
+ <div className="thinking-content">{text}</div>
46
+ </CollapsibleCard>
47
+ )
48
+ }
@@ -0,0 +1,220 @@
1
+ .tool-call-content {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
5
+
6
+ .tool-call-command {
7
+ margin-bottom: 12px;
8
+ }
9
+
10
+ .command-header,
11
+ .result-header {
12
+ display: block;
13
+ margin-bottom: 8px;
14
+ padding: 0;
15
+ cursor: default;
16
+ user-select: none;
17
+ font-size: 12px;
18
+ line-height: 1.5;
19
+ }
20
+
21
+ /* 确保没有 hover 效果 - 明确覆盖其他地方的样式 */
22
+ .command-header:hover,
23
+ .result-header:hover,
24
+ .command-label:hover,
25
+ .result-label:hover,
26
+ .command-name:hover,
27
+ .result-name:hover {
28
+ background: transparent !important;
29
+ }
30
+
31
+ .command-code {
32
+ display: block;
33
+ padding: 10px;
34
+ background: rgba(0, 0, 0, 0.3);
35
+ border: 1px solid var(--chat-border, #333);
36
+ border-radius: 6px;
37
+ font-family: "Monaco", "Menlo", "Courier New", monospace;
38
+ font-size: 13px;
39
+ color: var(--chat-text, #ccc);
40
+ white-space: pre-wrap;
41
+ word-break: break-all;
42
+ overflow-x: auto;
43
+ }
44
+
45
+ .tool-call-result {
46
+ margin-bottom: 12px;
47
+ }
48
+
49
+ .result-label,
50
+ .command-label {
51
+ color: var(--chat-text-muted, #888);
52
+ font-weight: 500;
53
+ }
54
+
55
+ .result-name,
56
+ .command-name {
57
+ color: var(--chat-text, #ccc);
58
+ }
59
+
60
+ .result-content {
61
+ display: block;
62
+ padding: 10px;
63
+ background: rgba(0, 0, 0, 0.3);
64
+ border: 1px solid var(--chat-border, #333);
65
+ border-radius: 6px;
66
+ font-family: "Monaco", "Menlo", "Courier New", monospace;
67
+ font-size: 13px;
68
+ color: var(--chat-text, #ccc);
69
+ white-space: pre-wrap;
70
+ word-break: break-all;
71
+ overflow-x: auto;
72
+ }
73
+
74
+ .execution-stats {
75
+ display: flex;
76
+ gap: 16px;
77
+ margin-bottom: 12px;
78
+ padding-top: 8px;
79
+ border-top: 1px solid var(--chat-border, #333);
80
+ }
81
+
82
+ .stat-item {
83
+ display: flex;
84
+ gap: 4px;
85
+ font-size: 12px;
86
+ }
87
+
88
+ .stat-label {
89
+ color: var(--chat-text-muted, #888);
90
+ }
91
+
92
+ .stat-value {
93
+ color: var(--chat-text, #ccc);
94
+ }
95
+
96
+ .tool-call-footer {
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: space-between;
100
+ gap: 12px;
101
+ padding-top: 12px;
102
+ border-top: 1px solid var(--chat-border, #333);
103
+ }
104
+
105
+ .footer-left {
106
+ flex-shrink: 0;
107
+ position: relative;
108
+ z-index: 10;
109
+ }
110
+
111
+ .footer-right {
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 12px;
115
+ }
116
+
117
+ .execution-status {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 6px;
121
+ font-size: 12px;
122
+ font-weight: 500;
123
+ }
124
+
125
+ .execution-status.success {
126
+ color: var(--chat-success, #22c55e);
127
+ }
128
+
129
+ .execution-status.error {
130
+ color: #ef4444;
131
+ }
132
+
133
+ .execution-status.running {
134
+ color: var(--chat-accent, #3b82f6);
135
+ }
136
+
137
+ .execution-status.pending {
138
+ color: var(--chat-text-muted, #888);
139
+ }
140
+
141
+ .mode-display {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 6px;
145
+ font-size: 12px;
146
+ }
147
+
148
+ .mode-label {
149
+ color: var(--chat-text-muted, #888);
150
+ }
151
+
152
+ .mode-value {
153
+ color: var(--chat-text, #ccc);
154
+ }
155
+
156
+ .spinning {
157
+ animation: spin 1s linear infinite;
158
+ }
159
+
160
+ @keyframes spin {
161
+ from {
162
+ transform: rotate(0deg);
163
+ }
164
+ to {
165
+ transform: rotate(360deg);
166
+ }
167
+ }
168
+
169
+ .action-buttons {
170
+ display: flex;
171
+ gap: 8px;
172
+ }
173
+
174
+ .btn {
175
+ padding: 6px 12px;
176
+ border-radius: 6px;
177
+ font-size: 12px;
178
+ font-weight: 500;
179
+ cursor: pointer;
180
+ border: none;
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 6px;
184
+ transition: all 0.2s;
185
+ }
186
+
187
+ .btn-skip {
188
+ background: var(--chat-muted, #2a2a2a);
189
+ color: var(--chat-text, #ccc);
190
+ border: 1px solid var(--chat-border, #333);
191
+ }
192
+
193
+ .btn-skip:hover {
194
+ background: var(--chat-muted-hover, #333);
195
+ }
196
+
197
+ .btn-run {
198
+ background: var(--chat-accent, #3b82f6);
199
+ color: #fff;
200
+ }
201
+
202
+ .btn-run:hover {
203
+ background: var(--chat-accent-hover, #2563eb);
204
+ }
205
+
206
+ .btn-cancel {
207
+ background: transparent;
208
+ color: var(--chat-text-muted, #888);
209
+ border: 1px solid var(--chat-border, #333);
210
+ }
211
+
212
+ .btn-cancel:hover {
213
+ background: rgba(239, 68, 68, 0.1);
214
+ color: #ef4444;
215
+ border-color: #ef4444;
216
+ }
217
+
218
+ .execution-status.cancelled {
219
+ color: var(--chat-warning, #f59e0b);
220
+ }
@@ -0,0 +1,285 @@
1
+ import { useState, useMemo, useEffect, useCallback, useRef } from 'react'
2
+ import type { FC } from 'react'
3
+ import { Icon } from '@iconify/react'
4
+ import { DropdownSelector } from '../../input/DropdownSelector'
5
+ import { CollapsibleCard } from './CollapsibleCard'
6
+ import { CopyButton } from '../../common/CopyButton'
7
+ import type { ChatAdapter } from '../../../adapter'
8
+ import type { AutoRunConfig, AutoRunMode } from '@huyooo/ai-chat-bridge-electron/renderer'
9
+ import type { StepsExpandedType } from '../../../types'
10
+ import './ToolCallPart.css'
11
+
12
+ interface ToolCallPartProps {
13
+ id: string
14
+ name: string
15
+ args?: Record<string, unknown>
16
+ status: 'pending' | 'running' | 'done' | 'error' | 'cancelled' | 'skipped'
17
+ result: unknown | null
18
+ expandedType?: StepsExpandedType
19
+ // 通过 props 传递,不使用 context
20
+ adapter?: ChatAdapter
21
+ autoRunConfig?: AutoRunConfig
22
+ onSaveConfig?: (config: AutoRunConfig) => Promise<void>
23
+ }
24
+
25
+ // 模式选项
26
+ const modeOptions = [
27
+ { value: 'manual', label: '执行前询问我' },
28
+ { value: 'run-everything', label: '自动执行' },
29
+ ]
30
+
31
+ // 状态图标映射
32
+ const statusIcons: Record<string, string> = {
33
+ error: 'lucide:x-circle',
34
+ done: 'lucide:check-circle',
35
+ running: 'lucide:loader-2',
36
+ pending: 'lucide:clock',
37
+ cancelled: 'lucide:ban',
38
+ skipped: 'lucide:skip-forward',
39
+ }
40
+
41
+ // 状态颜色映射
42
+ const statusColors: Record<string, string> = {
43
+ error: 'var(--chat-error, #ef4444)',
44
+ done: 'var(--chat-success, #22c55e)',
45
+ cancelled: 'var(--chat-warning, #f59e0b)',
46
+ skipped: 'var(--chat-text-muted, #888)',
47
+ running: 'var(--chat-accent, #3b82f6)',
48
+ pending: 'var(--chat-text-muted, #888)',
49
+ }
50
+
51
+ // 状态显示图标
52
+ const statusDisplayIcons: Record<string, string> = {
53
+ done: 'lucide:check-circle-2',
54
+ error: 'lucide:x-circle',
55
+ running: 'lucide:loader-2',
56
+ pending: 'lucide:clock',
57
+ cancelled: 'lucide:ban',
58
+ skipped: 'lucide:skip-forward',
59
+ }
60
+
61
+ // 状态文本
62
+ const statusTexts: Record<string, string> = {
63
+ done: '成功',
64
+ error: '错误',
65
+ running: '运行中',
66
+ pending: '等待中',
67
+ cancelled: '已取消',
68
+ skipped: '已跳过',
69
+ }
70
+
71
+ export const ToolCallPart: FC<ToolCallPartProps> = ({
72
+ id,
73
+ name,
74
+ args,
75
+ status,
76
+ result,
77
+ expandedType = 'auto',
78
+ adapter,
79
+ autoRunConfig,
80
+ onSaveConfig,
81
+ }) => {
82
+ // 当前模式:直接从配置读取
83
+ const currentMode = autoRunConfig?.mode ?? 'run-everything'
84
+
85
+ // 命令显示
86
+ const commandDisplay = useMemo(() => {
87
+ if (name === 'execute_command' && args?.command) {
88
+ const cmd = String(args.command)
89
+ const parts = cmd.trim().split(/\s+/)
90
+ return parts.slice(0, 3).join(' ') + (parts.length > 3 ? '...' : '')
91
+ }
92
+ return name
93
+ }, [name, args])
94
+
95
+ const commandText = useMemo(() => {
96
+ if (name === 'execute_command' && args?.command) {
97
+ return String(args.command)
98
+ }
99
+ if (args && Object.keys(args).length > 0) {
100
+ try {
101
+ return JSON.stringify(args, null, 2)
102
+ } catch {
103
+ return String(args)
104
+ }
105
+ }
106
+ return name
107
+ }, [name, args])
108
+
109
+ // 格式化执行结果
110
+ const formattedResult = useMemo(() => {
111
+ if (result === null) return ''
112
+ if (typeof result === 'string') return result
113
+ try {
114
+ return JSON.stringify(result, null, 2)
115
+ } catch {
116
+ return String(result)
117
+ }
118
+ }, [result])
119
+
120
+ // 折叠状态
121
+ const getInitialExpanded = (): boolean => {
122
+ if (expandedType === 'open') return true
123
+ if (expandedType === 'close') return false
124
+ return status === 'pending' || status === 'running'
125
+ }
126
+
127
+ const [expanded, setExpanded] = useState(getInitialExpanded)
128
+
129
+ // auto 模式下:状态变化时自动折叠/展开
130
+ useEffect(() => {
131
+ if (expandedType === 'auto') {
132
+ setExpanded(status === 'pending' || status === 'running')
133
+ }
134
+ }, [status, expandedType])
135
+
136
+ // 标题
137
+ const title = useMemo(() => {
138
+ const suffixes: Record<string, string> = {
139
+ pending: ' - 等待确认',
140
+ running: ' - 执行中...',
141
+ error: ' - 执行失败',
142
+ done: ' - 执行完成',
143
+ cancelled: ' - 已取消',
144
+ skipped: ' - 已跳过',
145
+ }
146
+ return name + (suffixes[status] ?? '')
147
+ }, [name, status])
148
+
149
+ /** 处理模式变化 */
150
+ const handleModeChange = useCallback(async (value: string) => {
151
+ if (!onSaveConfig || !autoRunConfig) return
152
+
153
+ try {
154
+ await onSaveConfig({
155
+ ...autoRunConfig,
156
+ mode: value as AutoRunMode,
157
+ })
158
+ } catch (error) {
159
+ console.error('[ToolCallPart] 保存配置失败:', error)
160
+ }
161
+ }, [onSaveConfig, autoRunConfig])
162
+
163
+ /** 跳过执行:只拒绝当前工具,AI 继续思考 */
164
+ const handleSkip = useCallback(async () => {
165
+ if (!adapter?.respondToolApproval) return
166
+
167
+ try {
168
+ // 只拒绝工具执行,不取消整个请求
169
+ // AI 会收到"用户跳过了此工具"的反馈,然后可能:
170
+ // 1. 尝试其他工具/方法
171
+ // 2. 直接回复用户
172
+ // 3. 询问用户想要什么
173
+ await adapter.respondToolApproval(id, false)
174
+ } catch (error) {
175
+ console.error('[ToolCallPart] 跳过失败:', error)
176
+ }
177
+ }, [adapter, id])
178
+
179
+ /** 运行执行 */
180
+ const handleRun = useCallback(async () => {
181
+ if (!adapter?.respondToolApproval) return
182
+
183
+ try {
184
+ await adapter.respondToolApproval(id, true)
185
+ } catch (error) {
186
+ console.error('[ToolCallPart] 运行失败:', error)
187
+ }
188
+ }, [adapter, id])
189
+
190
+ /** 取消执行 */
191
+ const handleCancel = useCallback(() => {
192
+ adapter?.cancel?.()
193
+ }, [adapter])
194
+
195
+ // 监听模式变化:从 manual 切换到 run-everything 时,自动执行 pending 的工具
196
+ const prevModeRef = useRef(currentMode)
197
+ useEffect(() => {
198
+ const prevMode = prevModeRef.current
199
+ if (prevMode === 'manual' && currentMode === 'run-everything' && status === 'pending') {
200
+ handleRun()
201
+ }
202
+ prevModeRef.current = currentMode
203
+ }, [currentMode, status, handleRun])
204
+
205
+
206
+ return (
207
+ <CollapsibleCard
208
+ icon={statusIcons[status] ?? 'lucide:clock'}
209
+ iconColor={statusColors[status] ?? 'var(--chat-text-muted, #888)'}
210
+ title={title}
211
+ expanded={expanded}
212
+ onExpandedChange={setExpanded}
213
+ spinning={status === 'running'}
214
+ headerActions={
215
+ <CopyButton text={commandText} title="复制" />
216
+ }
217
+ >
218
+ <div className="tool-call-content">
219
+ {/* 命令显示区域 */}
220
+ <div className="tool-call-command">
221
+ <div className="command-header">
222
+ <span className="command-label">执行:</span>
223
+ <span className="command-name">{commandDisplay}</span>
224
+ </div>
225
+ <code className="command-code">{commandText}</code>
226
+ </div>
227
+
228
+ {/* 执行结果(如果有) */}
229
+ {result !== null && (
230
+ <div className="tool-call-result">
231
+ <div className="result-header">
232
+ <span className="result-label">结果:</span>
233
+ <span className="result-name"></span>
234
+ </div>
235
+ <code className="result-content">{formattedResult}</code>
236
+ </div>
237
+ )}
238
+
239
+ {/* 底部操作区域 */}
240
+ <div className="tool-call-footer">
241
+ {/* 左侧:模式选择(始终可用,切换后影响后续操作) */}
242
+ <div className="footer-left">
243
+ <DropdownSelector
244
+ value={currentMode}
245
+ options={modeOptions}
246
+ onSelect={handleModeChange}
247
+ />
248
+ </div>
249
+
250
+ {/* 右侧:执行状态或按钮 */}
251
+ <div className="footer-right">
252
+ {/* pending 状态:显示跳过/运行按钮 */}
253
+ {status === 'pending' ? (
254
+ <div className="action-buttons">
255
+ <button className="btn btn-skip" onClick={handleSkip}>跳过</button>
256
+ <button className="btn btn-run" onClick={handleRun}>
257
+ 运行
258
+ <Icon icon="lucide:arrow-right" width={14} />
259
+ </button>
260
+ </div>
261
+ ) : status === 'running' ? (
262
+ /* running 状态:显示运行中状态 + 取消按钮 */
263
+ <>
264
+ <div className="execution-status running">
265
+ <Icon icon="lucide:loader-2" width={14} className="spinning" />
266
+ <span>运行中</span>
267
+ </div>
268
+ <button className="btn btn-cancel" onClick={handleCancel}>
269
+ <Icon icon="lucide:x" width={14} />
270
+ 取消
271
+ </button>
272
+ </>
273
+ ) : (
274
+ /* done/error 状态:只显示状态 */
275
+ <div className={`execution-status ${status}`}>
276
+ <Icon icon={statusDisplayIcons[status] ?? 'lucide:clock'} width={14} />
277
+ <span>{statusTexts[status] ?? ''}</span>
278
+ </div>
279
+ )}
280
+ </div>
281
+ </div>
282
+ </div>
283
+ </CollapsibleCard>
284
+ )
285
+ }
@@ -0,0 +1,68 @@
1
+ .tool-result-part {
2
+ margin: 8px 0;
3
+ }
4
+
5
+ .default-result {
6
+ border-radius: 8px;
7
+ background: var(--chat-muted, #2a2a2a);
8
+ border: 1px solid var(--chat-border, #333);
9
+ overflow: hidden;
10
+ }
11
+
12
+ /* 将样式限定在 .default-result 内部,避免污染其他组件 */
13
+ .default-result .result-header {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: 8px;
17
+ padding: 8px 12px;
18
+ cursor: pointer;
19
+ user-select: none;
20
+ }
21
+
22
+ .default-result .result-header:hover {
23
+ background: var(--chat-hover, #333);
24
+ }
25
+
26
+ .default-result .result-icon {
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ }
31
+
32
+ .default-result .result-icon .success {
33
+ color: var(--chat-success, #22c55e);
34
+ }
35
+
36
+ .default-result .result-icon .error {
37
+ color: var(--chat-error, #ef4444);
38
+ }
39
+
40
+ .default-result .result-name {
41
+ font-size: 13px;
42
+ font-weight: 500;
43
+ color: var(--chat-text, #ccc);
44
+ }
45
+
46
+ .default-result .result-chevron {
47
+ margin-left: auto;
48
+ color: var(--chat-text-muted, #666);
49
+ transition: transform 0.2s;
50
+ }
51
+
52
+ .default-result.expanded .result-chevron {
53
+ transform: rotate(180deg);
54
+ }
55
+
56
+ .default-result .result-content {
57
+ padding: 12px;
58
+ padding-top: 0;
59
+ }
60
+
61
+ .default-result .result-content pre {
62
+ margin: 0;
63
+ font-size: 12px;
64
+ font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
65
+ color: var(--chat-text-muted, #999);
66
+ white-space: pre-wrap;
67
+ word-break: break-word;
68
+ }
@@ -0,0 +1,96 @@
1
+ import { useState, useContext, useMemo, type FC, type ComponentType } from 'react'
2
+ import { Icon } from '@iconify/react'
3
+ import { ToolRenderersContext } from '../../../context/RenderersContext'
4
+ import type { ToolRendererProps } from '@huyooo/ai-chat-shared'
5
+ import type { StepsExpandedType } from '../../../types'
6
+ import './ToolResultPart.css'
7
+
8
+ interface ToolResultPartProps {
9
+ id: string
10
+ name: string
11
+ args?: Record<string, unknown>
12
+ result: unknown
13
+ status: 'done' | 'error' | 'cancelled' | 'skipped'
14
+ expandedType?: StepsExpandedType
15
+ }
16
+
17
+ export const ToolResultPart: FC<ToolResultPartProps> = ({
18
+ name,
19
+ args,
20
+ result,
21
+ status,
22
+ expandedType = 'auto'
23
+ }) => {
24
+ const toolRenderers = useContext(ToolRenderersContext)
25
+
26
+ // 根据模式计算初始状态(工具结果完成后默认折叠,除非 mode 是 open)
27
+ const getInitialExpanded = () => {
28
+ if (expandedType === 'open') return true
29
+ return false // close 和 auto 模式下默认折叠
30
+ }
31
+
32
+ const [expanded, setExpanded] = useState(getInitialExpanded)
33
+
34
+ // 获取自定义渲染器
35
+ const CustomRenderer = useMemo(() => {
36
+ return toolRenderers[name] as ComponentType<ToolRendererProps> | undefined
37
+ }, [toolRenderers, name])
38
+
39
+ // 格式化结果
40
+ const formattedResult = useMemo(() => {
41
+ if (typeof result === 'string') {
42
+ return result
43
+ }
44
+ return JSON.stringify(result, null, 2)
45
+ }, [result])
46
+
47
+ // 将 ToolResultPart 的 status 映射到 ToolRendererProps 的 status
48
+ const rendererStatus = useMemo((): 'running' | 'completed' | 'error' => {
49
+ if (status === 'error') return 'error'
50
+ if (status === 'cancelled') return 'error'
51
+ // 'done' 和 'skipped' 都映射为 'completed'
52
+ return 'completed'
53
+ }, [status])
54
+
55
+ // 如果有自定义渲染器,使用它
56
+ if (CustomRenderer) {
57
+ return (
58
+ <div className="tool-result-part">
59
+ <CustomRenderer
60
+ toolName={name}
61
+ toolArgs={args || {}}
62
+ toolResult={result}
63
+ status={rendererStatus}
64
+ />
65
+ </div>
66
+ )
67
+ }
68
+
69
+ // 默认渲染:可折叠的 JSON
70
+ return (
71
+ <div className="tool-result-part">
72
+ <div className={`default-result ${expanded ? 'expanded' : ''}`}>
73
+ <div className="result-header" onClick={() => setExpanded(!expanded)}>
74
+ <div className="result-icon">
75
+ {status === 'error' ? (
76
+ <Icon icon="lucide:x-circle" width={14} className="error" />
77
+ ) : (
78
+ <Icon icon="lucide:check-circle" width={14} className="success" />
79
+ )}
80
+ </div>
81
+ <span className="result-name">{name}</span>
82
+ <Icon
83
+ icon={expanded ? 'lucide:chevron-up' : 'lucide:chevron-down'}
84
+ width={14}
85
+ className="result-chevron"
86
+ />
87
+ </div>
88
+ {expanded && (
89
+ <div className="result-content">
90
+ <pre>{formattedResult}</pre>
91
+ </div>
92
+ )}
93
+ </div>
94
+ </div>
95
+ )
96
+ }