@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.
- package/README.md +99 -84
- package/dist/KaTeX_AMS-Regular-CYEKBG2K.woff +0 -0
- package/dist/KaTeX_AMS-Regular-JKX5W2C4.ttf +0 -0
- package/dist/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
- package/dist/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
- package/dist/KaTeX_Caligraphic-Bold-WZ3QSGD3.woff +0 -0
- package/dist/KaTeX_Caligraphic-Bold-ZTS3R3HK.ttf +0 -0
- package/dist/KaTeX_Caligraphic-Regular-3LKEU76G.woff +0 -0
- package/dist/KaTeX_Caligraphic-Regular-A7XRTZ5Q.ttf +0 -0
- package/dist/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
- package/dist/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
- package/dist/KaTeX_Fraktur-Bold-T4SWXBMT.woff +0 -0
- package/dist/KaTeX_Fraktur-Bold-WGHVTYOR.ttf +0 -0
- package/dist/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
- package/dist/KaTeX_Fraktur-Regular-5U4OPH2X.ttf +0 -0
- package/dist/KaTeX_Fraktur-Regular-PQMHCIK6.woff +0 -0
- package/dist/KaTeX_Main-Bold-2GA4IZIN.woff +0 -0
- package/dist/KaTeX_Main-Bold-W5FBVCZM.ttf +0 -0
- package/dist/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
- package/dist/KaTeX_Main-BoldItalic-4P4C7HJH.woff +0 -0
- package/dist/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
- package/dist/KaTeX_Main-BoldItalic-ODMLBJJQ.ttf +0 -0
- package/dist/KaTeX_Main-Italic-I43T2HSR.ttf +0 -0
- package/dist/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
- package/dist/KaTeX_Main-Italic-SASNQFN2.woff +0 -0
- package/dist/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
- package/dist/KaTeX_Main-Regular-P5I74A2A.woff +0 -0
- package/dist/KaTeX_Main-Regular-W74P5G27.ttf +0 -0
- package/dist/KaTeX_Math-BoldItalic-6EBV3DK5.woff +0 -0
- package/dist/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
- package/dist/KaTeX_Math-BoldItalic-VB447A4D.ttf +0 -0
- package/dist/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
- package/dist/KaTeX_Math-Italic-KKK3USB2.woff +0 -0
- package/dist/KaTeX_Math-Italic-SON4MRCA.ttf +0 -0
- package/dist/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
- package/dist/KaTeX_SansSerif-Bold-STQ6RXC7.ttf +0 -0
- package/dist/KaTeX_SansSerif-Bold-X5M5EMOD.woff +0 -0
- package/dist/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
- package/dist/KaTeX_SansSerif-Italic-PSN4QKYX.woff +0 -0
- package/dist/KaTeX_SansSerif-Italic-WTBAZBGY.ttf +0 -0
- package/dist/KaTeX_SansSerif-Regular-2TL3USAE.ttf +0 -0
- package/dist/KaTeX_SansSerif-Regular-OQCII6EP.woff +0 -0
- package/dist/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
- package/dist/KaTeX_Script-Regular-72OLXYNA.ttf +0 -0
- package/dist/KaTeX_Script-Regular-A5IFOEBS.woff +0 -0
- package/dist/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
- package/dist/KaTeX_Size1-Regular-4HRHTS65.woff +0 -0
- package/dist/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
- package/dist/KaTeX_Size1-Regular-7K6AASVL.ttf +0 -0
- package/dist/KaTeX_Size2-Regular-222HN3GT.ttf +0 -0
- package/dist/KaTeX_Size2-Regular-K5ZHAIS6.woff +0 -0
- package/dist/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
- package/dist/KaTeX_Size3-Regular-TLFPAHDE.woff +0 -0
- package/dist/KaTeX_Size3-Regular-UFCO6WCA.ttf +0 -0
- package/dist/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
- package/dist/KaTeX_Size4-Regular-7PGNVPQK.ttf +0 -0
- package/dist/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
- package/dist/KaTeX_Size4-Regular-PKMWZHNC.woff +0 -0
- package/dist/KaTeX_Typewriter-Regular-3F5K6SQ6.ttf +0 -0
- package/dist/KaTeX_Typewriter-Regular-MJMFSK64.woff +0 -0
- package/dist/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
- package/dist/index.css +2156 -603
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +126 -92
- package/dist/index.js +1605 -976
- package/dist/index.js.map +1 -1
- package/dist/style.css +130 -0
- package/package.json +3 -3
- package/src/components/ChatPanel.tsx +82 -19
- package/src/components/common/SettingsPanel.css +81 -0
- package/src/components/common/SettingsPanel.tsx +96 -1
- package/src/components/input/ChatInput.css +0 -1
- package/src/components/input/ChatInput.tsx +48 -26
- package/src/components/input/DropdownSelector.css +66 -0
- package/src/components/input/DropdownSelector.tsx +157 -19
- package/src/components/message/MessageBubble.css +5 -2
- package/src/components/message/MessageBubble.tsx +44 -35
- package/src/components/message/PartsRenderer.css +8 -0
- package/src/components/message/PartsRenderer.tsx +137 -83
- package/src/components/message/parts/CollapsibleCard.css +4 -2
- package/src/components/message/parts/CollapsibleCard.tsx +4 -1
- package/src/components/message/parts/ImagePart.css +0 -1
- package/src/components/message/parts/TextPart.css +574 -5
- package/src/components/message/parts/TextPart.tsx +201 -8
- package/src/components/message/parts/ToolCallPart.css +139 -115
- package/src/components/message/parts/ToolCallPart.tsx +138 -134
- package/src/components/message/parts/ToolResultPart.css +0 -1
- package/src/components/message/parts/index.ts +3 -1
- package/src/components/message/parts/visual-predicate.ts +43 -0
- package/src/components/message/parts/visual-render.ts +19 -0
- package/src/components/message/parts/visual.ts +12 -0
- package/src/context/RenderersContext.tsx +19 -25
- package/src/hooks/useChat.ts +567 -79
- package/src/hooks/useImageUpload.ts +104 -12
- package/src/hooks/useVoiceInput.ts +17 -0
- package/src/index.ts +19 -16
- package/src/styles.css +130 -0
- package/src/types/index.ts +52 -68
- package/src/components/message/ContentRenderer.tsx +0 -63
- package/src/components/message/ToolResultRenderer.tsx +0 -21
- package/src/components/message/blocks/CodeBlock.tsx +0 -60
- package/src/components/message/blocks/TextBlock.tsx +0 -15
- package/src/components/message/blocks/blocks.css +0 -141
- package/src/components/message/blocks/index.ts +0 -6
- package/src/components/message/parts/ToolResultPart.tsx +0 -96
- package/src/components/message/tool-results/DefaultToolResult.tsx +0 -26
- package/src/components/message/tool-results/SearchResults.tsx +0 -69
- package/src/components/message/tool-results/WeatherCard.tsx +0 -63
- package/src/components/message/tool-results/index.ts +0 -7
- 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
|
|
80
|
+
// 参数类型:'json' | 'command' | 'text'
|
|
81
|
+
const argsType = useMemo(() => {
|
|
87
82
|
if (name === 'execute_command' && args?.command) {
|
|
88
|
-
|
|
89
|
-
const parts = cmd.trim().split(/\s+/)
|
|
90
|
-
return parts.slice(0, 3).join(' ') + (parts.length > 3 ? '...' : '')
|
|
83
|
+
return 'command'
|
|
91
84
|
}
|
|
92
|
-
|
|
85
|
+
if (args && Object.keys(args).length > 0) {
|
|
86
|
+
return 'json'
|
|
87
|
+
}
|
|
88
|
+
return 'text'
|
|
93
89
|
}, [name, args])
|
|
94
90
|
|
|
95
|
-
|
|
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
|
|
111
|
-
if (
|
|
112
|
-
|
|
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
|
-
|
|
112
|
+
return ''
|
|
113
|
+
}, [argsType, argsText])
|
|
119
114
|
|
|
120
115
|
// 折叠状态
|
|
121
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
<div className="
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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,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
|
+
|
|
@@ -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
|
|
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
|
|
13
|
+
/** Part 渲染器映射(key: part.type, value: 渲染组件) */
|
|
14
|
+
export type PartRenderers = Record<string, ComponentType<PartRendererProps>>
|
|
17
15
|
|
|
18
|
-
/**
|
|
19
|
-
export const
|
|
16
|
+
/** Part 渲染器上下文 */
|
|
17
|
+
export const PartRenderersContext = createContext<PartRenderers>({})
|
|
20
18
|
|
|
21
|
-
/** 渲染器 Provider Props */
|
|
22
|
-
interface
|
|
23
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
toolRenderers = {},
|
|
25
|
+
/** Part 渲染器 Provider */
|
|
26
|
+
export const PartRenderersProvider: FC<PartRenderersProviderProps> = ({
|
|
27
|
+
partRenderers = {},
|
|
32
28
|
children,
|
|
33
29
|
}) => {
|
|
34
30
|
return (
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
</ToolRenderersContext.Provider>
|
|
39
|
-
</BlockRenderersContext.Provider>
|
|
31
|
+
<PartRenderersContext.Provider value={partRenderers}>
|
|
32
|
+
{children}
|
|
33
|
+
</PartRenderersContext.Provider>
|
|
40
34
|
)
|
|
41
35
|
}
|