@huyooo/ai-chat-frontend-react 0.2.12 → 0.2.14

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 (110) hide show
  1. package/README.md +99 -84
  2. package/dist/KaTeX_AMS-Regular-CYEKBG2K.woff +0 -0
  3. package/dist/KaTeX_AMS-Regular-JKX5W2C4.ttf +0 -0
  4. package/dist/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
  5. package/dist/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
  6. package/dist/KaTeX_Caligraphic-Bold-WZ3QSGD3.woff +0 -0
  7. package/dist/KaTeX_Caligraphic-Bold-ZTS3R3HK.ttf +0 -0
  8. package/dist/KaTeX_Caligraphic-Regular-3LKEU76G.woff +0 -0
  9. package/dist/KaTeX_Caligraphic-Regular-A7XRTZ5Q.ttf +0 -0
  10. package/dist/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
  11. package/dist/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
  12. package/dist/KaTeX_Fraktur-Bold-T4SWXBMT.woff +0 -0
  13. package/dist/KaTeX_Fraktur-Bold-WGHVTYOR.ttf +0 -0
  14. package/dist/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
  15. package/dist/KaTeX_Fraktur-Regular-5U4OPH2X.ttf +0 -0
  16. package/dist/KaTeX_Fraktur-Regular-PQMHCIK6.woff +0 -0
  17. package/dist/KaTeX_Main-Bold-2GA4IZIN.woff +0 -0
  18. package/dist/KaTeX_Main-Bold-W5FBVCZM.ttf +0 -0
  19. package/dist/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
  20. package/dist/KaTeX_Main-BoldItalic-4P4C7HJH.woff +0 -0
  21. package/dist/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
  22. package/dist/KaTeX_Main-BoldItalic-ODMLBJJQ.ttf +0 -0
  23. package/dist/KaTeX_Main-Italic-I43T2HSR.ttf +0 -0
  24. package/dist/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
  25. package/dist/KaTeX_Main-Italic-SASNQFN2.woff +0 -0
  26. package/dist/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
  27. package/dist/KaTeX_Main-Regular-P5I74A2A.woff +0 -0
  28. package/dist/KaTeX_Main-Regular-W74P5G27.ttf +0 -0
  29. package/dist/KaTeX_Math-BoldItalic-6EBV3DK5.woff +0 -0
  30. package/dist/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
  31. package/dist/KaTeX_Math-BoldItalic-VB447A4D.ttf +0 -0
  32. package/dist/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
  33. package/dist/KaTeX_Math-Italic-KKK3USB2.woff +0 -0
  34. package/dist/KaTeX_Math-Italic-SON4MRCA.ttf +0 -0
  35. package/dist/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
  36. package/dist/KaTeX_SansSerif-Bold-STQ6RXC7.ttf +0 -0
  37. package/dist/KaTeX_SansSerif-Bold-X5M5EMOD.woff +0 -0
  38. package/dist/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
  39. package/dist/KaTeX_SansSerif-Italic-PSN4QKYX.woff +0 -0
  40. package/dist/KaTeX_SansSerif-Italic-WTBAZBGY.ttf +0 -0
  41. package/dist/KaTeX_SansSerif-Regular-2TL3USAE.ttf +0 -0
  42. package/dist/KaTeX_SansSerif-Regular-OQCII6EP.woff +0 -0
  43. package/dist/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
  44. package/dist/KaTeX_Script-Regular-72OLXYNA.ttf +0 -0
  45. package/dist/KaTeX_Script-Regular-A5IFOEBS.woff +0 -0
  46. package/dist/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
  47. package/dist/KaTeX_Size1-Regular-4HRHTS65.woff +0 -0
  48. package/dist/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
  49. package/dist/KaTeX_Size1-Regular-7K6AASVL.ttf +0 -0
  50. package/dist/KaTeX_Size2-Regular-222HN3GT.ttf +0 -0
  51. package/dist/KaTeX_Size2-Regular-K5ZHAIS6.woff +0 -0
  52. package/dist/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
  53. package/dist/KaTeX_Size3-Regular-TLFPAHDE.woff +0 -0
  54. package/dist/KaTeX_Size3-Regular-UFCO6WCA.ttf +0 -0
  55. package/dist/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
  56. package/dist/KaTeX_Size4-Regular-7PGNVPQK.ttf +0 -0
  57. package/dist/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
  58. package/dist/KaTeX_Size4-Regular-PKMWZHNC.woff +0 -0
  59. package/dist/KaTeX_Typewriter-Regular-3F5K6SQ6.ttf +0 -0
  60. package/dist/KaTeX_Typewriter-Regular-MJMFSK64.woff +0 -0
  61. package/dist/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
  62. package/dist/index.css +2156 -603
  63. package/dist/index.css.map +1 -1
  64. package/dist/index.d.ts +126 -92
  65. package/dist/index.js +1605 -976
  66. package/dist/index.js.map +1 -1
  67. package/dist/style.css +130 -0
  68. package/package.json +3 -3
  69. package/src/components/ChatPanel.tsx +82 -19
  70. package/src/components/common/SettingsPanel.css +81 -0
  71. package/src/components/common/SettingsPanel.tsx +96 -1
  72. package/src/components/input/ChatInput.css +0 -1
  73. package/src/components/input/ChatInput.tsx +48 -26
  74. package/src/components/input/DropdownSelector.css +66 -0
  75. package/src/components/input/DropdownSelector.tsx +157 -19
  76. package/src/components/message/MessageBubble.css +5 -2
  77. package/src/components/message/MessageBubble.tsx +44 -35
  78. package/src/components/message/PartsRenderer.css +8 -0
  79. package/src/components/message/PartsRenderer.tsx +137 -83
  80. package/src/components/message/parts/CollapsibleCard.css +4 -2
  81. package/src/components/message/parts/CollapsibleCard.tsx +4 -1
  82. package/src/components/message/parts/ImagePart.css +0 -1
  83. package/src/components/message/parts/TextPart.css +574 -5
  84. package/src/components/message/parts/TextPart.tsx +201 -8
  85. package/src/components/message/parts/ToolCallPart.css +139 -115
  86. package/src/components/message/parts/ToolCallPart.tsx +138 -134
  87. package/src/components/message/parts/ToolResultPart.css +0 -1
  88. package/src/components/message/parts/index.ts +3 -1
  89. package/src/components/message/parts/visual-predicate.ts +43 -0
  90. package/src/components/message/parts/visual-render.ts +19 -0
  91. package/src/components/message/parts/visual.ts +12 -0
  92. package/src/context/RenderersContext.tsx +19 -25
  93. package/src/hooks/useChat.ts +567 -79
  94. package/src/hooks/useImageUpload.ts +104 -12
  95. package/src/hooks/useVoiceInput.ts +17 -0
  96. package/src/index.ts +19 -16
  97. package/src/styles.css +130 -0
  98. package/src/types/index.ts +52 -68
  99. package/src/components/message/ContentRenderer.tsx +0 -63
  100. package/src/components/message/ToolResultRenderer.tsx +0 -21
  101. package/src/components/message/blocks/CodeBlock.tsx +0 -60
  102. package/src/components/message/blocks/TextBlock.tsx +0 -15
  103. package/src/components/message/blocks/blocks.css +0 -141
  104. package/src/components/message/blocks/index.ts +0 -6
  105. package/src/components/message/parts/ToolResultPart.tsx +0 -96
  106. package/src/components/message/tool-results/DefaultToolResult.tsx +0 -26
  107. package/src/components/message/tool-results/SearchResults.tsx +0 -69
  108. package/src/components/message/tool-results/WeatherCard.tsx +0 -63
  109. package/src/components/message/tool-results/index.ts +0 -7
  110. package/src/components/message/tool-results/tool-results.css +0 -181
@@ -4,6 +4,7 @@ import { Icon } from '@iconify/react'
4
4
  import { DropdownSelector } from '../../input/DropdownSelector'
5
5
  import { CollapsibleCard } from './CollapsibleCard'
6
6
  import { CopyButton } from '../../common/CopyButton'
7
+ import { highlightCode } from '@huyooo/ai-chat-shared'
7
8
  import type { ChatAdapter } from '../../../adapter'
8
9
  import type { AutoRunConfig, AutoRunMode } from '@huyooo/ai-chat-bridge-electron/renderer'
9
10
  import type { StepsExpandedType } from '../../../types'
@@ -13,22 +14,23 @@ interface ToolCallPartProps {
13
14
  id: string
14
15
  name: string
15
16
  args?: Record<string, unknown>
17
+ output?: {
18
+ stdout?: string
19
+ stderr?: string
20
+ }
16
21
  status: 'pending' | 'running' | 'done' | 'error' | 'cancelled' | 'skipped'
17
- result: unknown | null
18
22
  expandedType?: StepsExpandedType
19
- // 通过 props 传递,不使用 context
20
23
  adapter?: ChatAdapter
24
+ onCancelToolCall?: (toolCallId: string) => void
21
25
  autoRunConfig?: AutoRunConfig
22
26
  onSaveConfig?: (config: AutoRunConfig) => Promise<void>
23
27
  }
24
28
 
25
- // 模式选项
26
29
  const modeOptions = [
27
30
  { value: 'manual', label: '执行前询问我' },
28
31
  { value: 'run-everything', label: '自动执行' },
29
32
  ]
30
33
 
31
- // 状态图标映射
32
34
  const statusIcons: Record<string, string> = {
33
35
  error: 'lucide:x-circle',
34
36
  done: 'lucide:check-circle',
@@ -38,32 +40,25 @@ const statusIcons: Record<string, string> = {
38
40
  skipped: 'lucide:skip-forward',
39
41
  }
40
42
 
41
- // 状态颜色映射
42
43
  const statusColors: Record<string, string> = {
43
44
  error: 'var(--chat-error, #ef4444)',
44
45
  done: 'var(--chat-success, #22c55e)',
45
- cancelled: 'var(--chat-warning, #f59e0b)',
46
- skipped: 'var(--chat-text-muted, #888)',
47
46
  running: 'var(--chat-accent, #3b82f6)',
48
47
  pending: 'var(--chat-text-muted, #888)',
48
+ cancelled: 'var(--chat-warning, #f59e0b)',
49
+ skipped: 'var(--chat-text-muted, #888)',
49
50
  }
50
51
 
51
- // 状态显示图标
52
52
  const statusDisplayIcons: Record<string, string> = {
53
53
  done: 'lucide:check-circle-2',
54
54
  error: 'lucide:x-circle',
55
- running: 'lucide:loader-2',
56
- pending: 'lucide:clock',
57
55
  cancelled: 'lucide:ban',
58
56
  skipped: 'lucide:skip-forward',
59
57
  }
60
58
 
61
- // 状态文本
62
59
  const statusTexts: Record<string, string> = {
63
60
  done: '成功',
64
61
  error: '错误',
65
- running: '运行中',
66
- pending: '等待中',
67
62
  cancelled: '已取消',
68
63
  skipped: '已跳过',
69
64
  }
@@ -72,32 +67,35 @@ export const ToolCallPart: FC<ToolCallPartProps> = ({
72
67
  id,
73
68
  name,
74
69
  args,
70
+ output,
75
71
  status,
76
- result,
77
72
  expandedType = 'auto',
78
73
  adapter,
74
+ onCancelToolCall,
79
75
  autoRunConfig,
80
76
  onSaveConfig,
81
77
  }) => {
82
- // 当前模式:直接从配置读取
83
78
  const currentMode = autoRunConfig?.mode ?? 'run-everything'
84
79
 
85
- // 命令显示
86
- const commandDisplay = useMemo(() => {
80
+ // 参数类型:'json' | 'command' | 'text'
81
+ const argsType = useMemo(() => {
87
82
  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 ? '...' : '')
83
+ return 'command'
91
84
  }
92
- return name
85
+ if (args && Object.keys(args).length > 0) {
86
+ return 'json'
87
+ }
88
+ return 'text'
93
89
  }, [name, args])
94
90
 
95
- const commandText = useMemo(() => {
91
+ // 参数文本(格式化后的)
92
+ const argsText = useMemo(() => {
96
93
  if (name === 'execute_command' && args?.command) {
97
94
  return String(args.command)
98
95
  }
99
96
  if (args && Object.keys(args).length > 0) {
100
97
  try {
98
+ // JSON 格式化:2 空格缩进
101
99
  return JSON.stringify(args, null, 2)
102
100
  } catch {
103
101
  return String(args)
@@ -106,27 +104,21 @@ export const ToolCallPart: FC<ToolCallPartProps> = ({
106
104
  return name
107
105
  }, [name, args])
108
106
 
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)
107
+ // JSON 高亮后的 HTML
108
+ const highlightedJson = useMemo(() => {
109
+ if (argsType === 'json') {
110
+ return highlightCode(argsText, 'json')
117
111
  }
118
- }, [result])
112
+ return ''
113
+ }, [argsType, argsText])
119
114
 
120
115
  // 折叠状态
121
- const getInitialExpanded = (): boolean => {
116
+ const [expanded, setExpanded] = useState(() => {
122
117
  if (expandedType === 'open') return true
123
118
  if (expandedType === 'close') return false
124
119
  return status === 'pending' || status === 'running'
125
- }
126
-
127
- const [expanded, setExpanded] = useState(getInitialExpanded)
120
+ })
128
121
 
129
- // auto 模式下:状态变化时自动折叠/展开
130
122
  useEffect(() => {
131
123
  if (expandedType === 'auto') {
132
124
  setExpanded(status === 'pending' || status === 'running')
@@ -146,53 +138,56 @@ export const ToolCallPart: FC<ToolCallPartProps> = ({
146
138
  return name + (suffixes[status] ?? '')
147
139
  }, [name, status])
148
140
 
149
- /** 处理模式变化 */
141
+ // 操作
150
142
  const handleModeChange = useCallback(async (value: string) => {
151
143
  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
- }
144
+ await onSaveConfig({ ...autoRunConfig, mode: value as AutoRunMode })
161
145
  }, [onSaveConfig, autoRunConfig])
162
146
 
163
- /** 跳过执行:只拒绝当前工具,AI 继续思考 */
164
147
  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
- }
148
+ await adapter?.respondToolApproval?.(id, false)
177
149
  }, [adapter, id])
178
150
 
179
- /** 运行执行 */
180
151
  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
- }
152
+ await adapter?.respondToolApproval?.(id, true)
188
153
  }, [adapter, id])
189
154
 
190
- /** 取消执行 */
191
155
  const handleCancel = useCallback(() => {
156
+ if (onCancelToolCall) {
157
+ onCancelToolCall(id)
158
+ return
159
+ }
192
160
  adapter?.cancel?.()
193
- }, [adapter])
161
+ }, [adapter, id, onCancelToolCall])
194
162
 
195
- // 监听模式变化:从 manual 切换到 run-everything 时,自动执行 pending 的工具
163
+ const hasOutput = Boolean(output?.stdout || output?.stderr)
164
+ const [activeStream, setActiveStream] = useState<'stdout' | 'stderr'>('stdout')
165
+ const activeOutputText = useMemo(() => {
166
+ return activeStream === 'stdout' ? (output?.stdout ?? '') : (output?.stderr ?? '')
167
+ }, [activeStream, output])
168
+
169
+ const saveLog = useCallback(() => {
170
+ const stdout = output?.stdout ?? ''
171
+ const stderr = output?.stderr ?? ''
172
+ const content =
173
+ `# tool: ${name}\n` +
174
+ `# id: ${id}\n` +
175
+ `# time: ${new Date().toISOString()}\n\n` +
176
+ `===== STDOUT =====\n${stdout}\n\n` +
177
+ `===== STDERR =====\n${stderr}\n`
178
+
179
+ const blob = new Blob([content], { type: 'text/plain;charset=utf-8' })
180
+ const url = URL.createObjectURL(blob)
181
+ const a = document.createElement('a')
182
+ a.href = url
183
+ a.download = `tool-${name}-${id}.log`
184
+ document.body.appendChild(a)
185
+ a.click()
186
+ document.body.removeChild(a)
187
+ URL.revokeObjectURL(url)
188
+ }, [id, name, output])
189
+
190
+ // 模式切换时自动执行
196
191
  const prevModeRef = useRef(currentMode)
197
192
  useEffect(() => {
198
193
  const prevMode = prevModeRef.current
@@ -202,7 +197,6 @@ export const ToolCallPart: FC<ToolCallPartProps> = ({
202
197
  prevModeRef.current = currentMode
203
198
  }, [currentMode, status, handleRun])
204
199
 
205
-
206
200
  return (
207
201
  <CollapsibleCard
208
202
  icon={statusIcons[status] ?? 'lucide:clock'}
@@ -211,73 +205,83 @@ export const ToolCallPart: FC<ToolCallPartProps> = ({
211
205
  expanded={expanded}
212
206
  onExpandedChange={setExpanded}
213
207
  spinning={status === 'running'}
214
- headerActions={
215
- <CopyButton text={commandText} title="复制" />
216
- }
208
+ headerActions={<CopyButton text={argsText} title="复制" />}
217
209
  >
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>
210
+ {/* 参数内容 */}
211
+ <pre className={`tool-args ${argsType}`}>
212
+ {argsType === 'json' ? (
213
+ <code dangerouslySetInnerHTML={{ __html: highlightedJson }} />
214
+ ) : (
215
+ argsText
237
216
  )}
217
+ </pre>
238
218
 
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
- )}
219
+ {/* 输出面板(stdout/stderr) */}
220
+ {hasOutput && (
221
+ <div className="output-panel">
222
+ <div className="output-header">
223
+ <div className="output-tabs">
224
+ <button
225
+ className={`tab${activeStream === 'stdout' ? ' active' : ''}`}
226
+ type="button"
227
+ onClick={() => setActiveStream('stdout')}
228
+ >
229
+ 正常输出
230
+ </button>
231
+ <button
232
+ className={`tab${activeStream === 'stderr' ? ' active' : ''}`}
233
+ type="button"
234
+ onClick={() => setActiveStream('stderr')}
235
+ >
236
+ 异常/告警输出
237
+ </button>
238
+ </div>
239
+ <div className="output-actions">
240
+ <button className="btn btn-save" type="button" onClick={saveLog}>
241
+ <Icon icon="lucide:download" width={14} />
242
+ 保存日志
243
+ </button>
244
+ <CopyButton text={activeOutputText} title="复制输出" />
245
+ </div>
280
246
  </div>
247
+ <pre className="output-body chat-scrollbar"><code>{activeOutputText || '(无输出)'}</code></pre>
248
+ </div>
249
+ )}
250
+
251
+ {/* 底部操作区 */}
252
+ <div className="tool-footer">
253
+ <DropdownSelector
254
+ value={currentMode}
255
+ options={modeOptions}
256
+ onSelect={handleModeChange}
257
+ />
258
+
259
+ <div className="tool-actions">
260
+ {status === 'pending' ? (
261
+ <>
262
+ <button className="btn btn-skip" onClick={handleSkip}>跳过</button>
263
+ <button className="btn btn-run" onClick={handleRun}>
264
+ 运行
265
+ <Icon icon="lucide:arrow-right" width={14} />
266
+ </button>
267
+ </>
268
+ ) : status === 'running' ? (
269
+ <>
270
+ <span className="status-text running">
271
+ <Icon icon="lucide:loader-2" width={14} className="spinning" />
272
+ 运行中
273
+ </span>
274
+ <button className="btn btn-cancel" onClick={handleCancel}>
275
+ <Icon icon="lucide:x" width={14} />
276
+ 取消
277
+ </button>
278
+ </>
279
+ ) : (
280
+ <span className={`status-text ${status}`}>
281
+ <Icon icon={statusDisplayIcons[status] ?? 'lucide:clock'} width={14} />
282
+ {statusTexts[status] ?? ''}
283
+ </span>
284
+ )}
281
285
  </div>
282
286
  </div>
283
287
  </CollapsibleCard>
@@ -1,5 +1,4 @@
1
1
  .tool-result-part {
2
- margin: 8px 0;
3
2
  }
4
3
 
5
4
  .default-result {
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Part 渲染组件导出
3
+ *
4
+ * 内置 Part 类型:text, thinking, search, tool_call, image, error
5
+ * 自定义 Part 类型(如 weather, stock)通过 partRenderers 注册
3
6
  */
4
7
  export { CollapsibleCard } from './CollapsibleCard'
5
8
  export { TextPart } from './TextPart'
6
9
  export { ThinkingPart } from './ThinkingPart'
7
10
  export { SearchPart } from './SearchPart'
8
11
  export { ToolCallPart } from './ToolCallPart'
9
- export { ToolResultPart } from './ToolResultPart'
10
12
  export { ImagePart } from './ImagePart'
11
13
  export { ErrorPart } from './ErrorPart'
@@ -0,0 +1,43 @@
1
+ function normalizeLanguage(lang?: string): string {
2
+ return (lang || '').trim().toLowerCase()
3
+ }
4
+
5
+ export function isMermaidLanguage(lang?: string): boolean {
6
+ return normalizeLanguage(lang) === 'mermaid'
7
+ }
8
+
9
+ export function isLatexLanguage(lang?: string): boolean {
10
+ const l = normalizeLanguage(lang)
11
+ return l === 'latex' || l === 'katex' || l === 'tex'
12
+ }
13
+
14
+ export function isLatexDocument(code: string): boolean {
15
+ const t = (code || '').trim()
16
+ return /\\documentclass\b|\\usepackage\b|\\begin\{document\}|\\end\{document\}/.test(t)
17
+ }
18
+
19
+ export function canVisualizeLatex(code: string): boolean {
20
+ const t = (code || '').trim()
21
+ if (!t) return false
22
+ if (isLatexDocument(t)) return false
23
+ return true
24
+ }
25
+
26
+ export function hasLatexDelimiters(code: string): boolean {
27
+ const t = (code || '').trim()
28
+ if (!t) return false
29
+ return (
30
+ /\$\$[\s\S]*\$\$/.test(t) ||
31
+ /\\\[[\s\S]*\\\]/.test(t) ||
32
+ /\\\([\s\S]*\\\)/.test(t) ||
33
+ /(?<!\$)\$(?!\$)[\s\S]*?\$(?!\$)/.test(t)
34
+ )
35
+ }
36
+
37
+ export function shouldShowVisualToggle(lang?: string, code?: string): boolean {
38
+ if (isMermaidLanguage(lang)) return true
39
+ if (isLatexLanguage(lang)) return canVisualizeLatex(code || '')
40
+ return false
41
+ }
42
+
43
+
@@ -0,0 +1,19 @@
1
+ import { renderMarkdown } from '@huyooo/ai-chat-shared'
2
+ import { canVisualizeLatex, hasLatexDelimiters } from './visual-predicate'
3
+
4
+ export function renderFencedLatexToHtml(code: string): string {
5
+ const t = (code || '').trim()
6
+ if (!t) return ''
7
+
8
+ if (!canVisualizeLatex(t)) {
9
+ return renderMarkdown(
10
+ `> 不支持将完整 LaTeX 文档作为公式渲染,请切换到代码视图查看。\n\n\`\`\`latex\n${t}\n\`\`\``
11
+ )
12
+ }
13
+
14
+ if (hasLatexDelimiters(t)) return renderMarkdown(t)
15
+
16
+ return renderMarkdown(`$$\n${t}\n$$`)
17
+ }
18
+
19
+
@@ -0,0 +1,12 @@
1
+ export {
2
+ isMermaidLanguage,
3
+ isLatexLanguage,
4
+ isLatexDocument,
5
+ canVisualizeLatex,
6
+ hasLatexDelimiters,
7
+ shouldShowVisualToggle,
8
+ } from './visual-predicate'
9
+
10
+ export { renderFencedLatexToHtml } from './visual-render'
11
+
12
+
@@ -1,41 +1,35 @@
1
1
  /**
2
- * 渲染器上下文
3
- * 用于注入自定义块渲染器和工具结果渲染器
2
+ * Part 渲染器上下文
3
+ * 用于注入自定义 Part 类型渲染器(如 weather, stock 等)
4
4
  */
5
5
 
6
6
  import { createContext, type ComponentType, type ReactNode, type FC } from 'react'
7
- import type { ContentBlock, ToolRendererProps } from '@huyooo/ai-chat-shared'
8
7
 
9
- /** 块渲染器映射 */
10
- export type BlockRenderers = Record<string, ComponentType<{ block: ContentBlock }>>
11
-
12
- /** 工具结果渲染器映射 */
13
- export type ToolRenderers = Record<string, ComponentType<ToolRendererProps>>
8
+ /** Part 渲染器 Props(每个自定义渲染器接收的 props) */
9
+ export interface PartRendererProps {
10
+ [key: string]: unknown
11
+ }
14
12
 
15
- /** 块渲染器上下文 */
16
- export const BlockRenderersContext = createContext<BlockRenderers>({})
13
+ /** Part 渲染器映射(key: part.type, value: 渲染组件) */
14
+ export type PartRenderers = Record<string, ComponentType<PartRendererProps>>
17
15
 
18
- /** 工具结果渲染器上下文 */
19
- export const ToolRenderersContext = createContext<ToolRenderers>({})
16
+ /** Part 渲染器上下文 */
17
+ export const PartRenderersContext = createContext<PartRenderers>({})
20
18
 
21
- /** 渲染器 Provider Props */
22
- interface RenderersProviderProps {
23
- blockRenderers?: BlockRenderers
24
- toolRenderers?: ToolRenderers
19
+ /** Part 渲染器 Provider Props */
20
+ interface PartRenderersProviderProps {
21
+ partRenderers?: PartRenderers
25
22
  children: ReactNode
26
23
  }
27
24
 
28
- /** 渲染器 Provider */
29
- export const RenderersProvider: FC<RenderersProviderProps> = ({
30
- blockRenderers = {},
31
- toolRenderers = {},
25
+ /** Part 渲染器 Provider */
26
+ export const PartRenderersProvider: FC<PartRenderersProviderProps> = ({
27
+ partRenderers = {},
32
28
  children,
33
29
  }) => {
34
30
  return (
35
- <BlockRenderersContext.Provider value={blockRenderers}>
36
- <ToolRenderersContext.Provider value={toolRenderers}>
37
- {children}
38
- </ToolRenderersContext.Provider>
39
- </BlockRenderersContext.Provider>
31
+ <PartRenderersContext.Provider value={partRenderers}>
32
+ {children}
33
+ </PartRenderersContext.Provider>
40
34
  )
41
35
  }