@huyooo/ai-chat-frontend-react 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.css +0 -1
- package/dist/index.js +1 -5418
- package/package.json +4 -5
- package/dist/index.css.map +0 -1
- package/dist/index.js.map +0 -1
- package/src/adapter.ts +0 -68
- package/src/components/ChatPanel.tsx +0 -553
- package/src/components/common/ConfirmDialog.css +0 -136
- package/src/components/common/ConfirmDialog.tsx +0 -91
- package/src/components/common/CopyButton.css +0 -22
- package/src/components/common/CopyButton.tsx +0 -46
- package/src/components/common/IndexingSettings.css +0 -207
- package/src/components/common/IndexingSettings.tsx +0 -398
- package/src/components/common/SettingsPanel.css +0 -337
- package/src/components/common/SettingsPanel.tsx +0 -215
- package/src/components/common/Toast.css +0 -50
- package/src/components/common/Toast.tsx +0 -38
- package/src/components/common/ToggleSwitch.css +0 -52
- package/src/components/common/ToggleSwitch.tsx +0 -20
- package/src/components/header/ChatHeader.css +0 -285
- package/src/components/header/ChatHeader.tsx +0 -376
- package/src/components/input/AtFilePicker.css +0 -147
- package/src/components/input/AtFilePicker.tsx +0 -519
- package/src/components/input/ChatInput.css +0 -283
- package/src/components/input/ChatInput.tsx +0 -575
- package/src/components/input/DropdownSelector.css +0 -231
- package/src/components/input/DropdownSelector.tsx +0 -333
- package/src/components/input/ImagePreviewModal.css +0 -124
- package/src/components/input/ImagePreviewModal.tsx +0 -118
- package/src/components/input/at-views/AtBranchView.tsx +0 -34
- package/src/components/input/at-views/AtBrowserView.tsx +0 -34
- package/src/components/input/at-views/AtChatsView.tsx +0 -34
- package/src/components/input/at-views/AtDocsView.tsx +0 -34
- package/src/components/input/at-views/AtFilesView.tsx +0 -168
- package/src/components/input/at-views/AtTerminalsView.tsx +0 -34
- package/src/components/input/at-views/AtViewStyles.css +0 -143
- package/src/components/input/at-views/index.ts +0 -9
- package/src/components/message/ContentRenderer.css +0 -9
- package/src/components/message/MessageBubble.css +0 -193
- package/src/components/message/MessageBubble.tsx +0 -240
- package/src/components/message/PartsRenderer.css +0 -12
- package/src/components/message/PartsRenderer.tsx +0 -168
- package/src/components/message/WelcomeMessage.css +0 -221
- package/src/components/message/WelcomeMessage.tsx +0 -93
- package/src/components/message/parts/CollapsibleCard.css +0 -80
- package/src/components/message/parts/CollapsibleCard.tsx +0 -80
- package/src/components/message/parts/ErrorPart.css +0 -9
- package/src/components/message/parts/ErrorPart.tsx +0 -40
- package/src/components/message/parts/ImagePart.css +0 -49
- package/src/components/message/parts/ImagePart.tsx +0 -54
- package/src/components/message/parts/SearchPart.css +0 -44
- package/src/components/message/parts/SearchPart.tsx +0 -63
- package/src/components/message/parts/TextPart.css +0 -579
- package/src/components/message/parts/TextPart.tsx +0 -213
- package/src/components/message/parts/ThinkingPart.css +0 -9
- package/src/components/message/parts/ThinkingPart.tsx +0 -48
- package/src/components/message/parts/ToolCallPart.css +0 -246
- package/src/components/message/parts/ToolCallPart.tsx +0 -289
- package/src/components/message/parts/ToolResultPart.css +0 -67
- package/src/components/message/parts/index.ts +0 -13
- package/src/components/message/parts/visual-predicate.ts +0 -43
- package/src/components/message/parts/visual-render.ts +0 -19
- package/src/components/message/parts/visual.ts +0 -12
- package/src/components/message/welcome-types.ts +0 -46
- package/src/context/AutoRunConfigContext.tsx +0 -13
- package/src/context/ChatAdapterContext.tsx +0 -8
- package/src/context/ChatInputContext.tsx +0 -40
- package/src/context/RenderersContext.tsx +0 -35
- package/src/hooks/useChat.ts +0 -1569
- package/src/hooks/useImageUpload.ts +0 -345
- package/src/hooks/useVoiceInput.ts +0 -454
- package/src/hooks/useVoiceToTextInput.ts +0 -87
- package/src/index.ts +0 -151
- package/src/styles.css +0 -330
- package/src/types/index.ts +0 -196
- package/src/utils/fileIcon.ts +0 -49
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MessageBubble 组件样式
|
|
3
|
-
* 与 Vue 版本 MessageBubble.vue 保持一致
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
.message-bubble {
|
|
7
|
-
animation: fadeIn 0.2s ease;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
@keyframes fadeIn {
|
|
11
|
-
from {
|
|
12
|
-
opacity: 0;
|
|
13
|
-
transform: translateY(4px);
|
|
14
|
-
}
|
|
15
|
-
to {
|
|
16
|
-
opacity: 1;
|
|
17
|
-
transform: translateY(0);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/* 用户消息 */
|
|
22
|
-
.message-bubble.user {
|
|
23
|
-
width: 100%;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.user-content {
|
|
27
|
-
width: 100%;
|
|
28
|
-
background: var(--chat-muted, #2d2d2d);
|
|
29
|
-
color: var(--chat-text, #ccc);
|
|
30
|
-
padding: 12px;
|
|
31
|
-
border-radius: 12px;
|
|
32
|
-
border: 1px solid var(--chat-border, #444);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.user-text {
|
|
36
|
-
font-size: 14px;
|
|
37
|
-
line-height: 1.5;
|
|
38
|
-
white-space: pre-wrap;
|
|
39
|
-
word-break: break-word;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.user-images {
|
|
43
|
-
display: flex;
|
|
44
|
-
gap: 8px;
|
|
45
|
-
margin-top: 8px;
|
|
46
|
-
flex-wrap: wrap;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.user-image {
|
|
50
|
-
width: 80px;
|
|
51
|
-
height: 80px;
|
|
52
|
-
object-fit: cover;
|
|
53
|
-
border-radius: 8px;
|
|
54
|
-
cursor: pointer;
|
|
55
|
-
transition: transform 0.15s;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.user-image:hover {
|
|
59
|
-
transform: scale(1.05);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/* 助手消息 */
|
|
63
|
-
.message-bubble.assistant {
|
|
64
|
-
position: relative;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.assistant-content {
|
|
68
|
-
max-width: 100%;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/* 加载提示 - 等待状态时 */
|
|
72
|
-
.loading-indicator {
|
|
73
|
-
position: relative;
|
|
74
|
-
display: flex;
|
|
75
|
-
align-items: center;
|
|
76
|
-
width: 100%;
|
|
77
|
-
padding: 10px 16px;
|
|
78
|
-
background: var(--chat-muted, #2a2a2a);
|
|
79
|
-
border-radius: 8px;
|
|
80
|
-
overflow: hidden;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/* 当上方有内容时,添加间距 */
|
|
84
|
-
.loading-indicator.has-content-above {
|
|
85
|
-
margin-top: 8px;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.loading-text {
|
|
89
|
-
font-size: 13px;
|
|
90
|
-
color: var(--chat-text-muted, #888);
|
|
91
|
-
position: relative;
|
|
92
|
-
z-index: 1;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.loading-shimmer {
|
|
96
|
-
position: absolute;
|
|
97
|
-
top: 0;
|
|
98
|
-
left: -100%;
|
|
99
|
-
width: 100%;
|
|
100
|
-
height: 100%;
|
|
101
|
-
background: linear-gradient(
|
|
102
|
-
90deg,
|
|
103
|
-
transparent 0%,
|
|
104
|
-
rgba(255, 255, 255, 0.08) 50%,
|
|
105
|
-
transparent 100%
|
|
106
|
-
);
|
|
107
|
-
animation: shimmer 1.5s ease-in-out infinite;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
@keyframes shimmer {
|
|
111
|
-
0% {
|
|
112
|
-
left: -100%;
|
|
113
|
-
}
|
|
114
|
-
100% {
|
|
115
|
-
left: 100%;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/* 操作按钮 */
|
|
120
|
-
.message-actions {
|
|
121
|
-
display: flex;
|
|
122
|
-
align-items: center;
|
|
123
|
-
justify-content: space-between;
|
|
124
|
-
margin-top: 8px;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.message-meta {
|
|
128
|
-
display: flex;
|
|
129
|
-
align-items: center;
|
|
130
|
-
gap: 6px;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
.model-name {
|
|
134
|
-
font-size: 11px;
|
|
135
|
-
color: var(--chat-text-muted, #888);
|
|
136
|
-
background: var(--chat-muted, #2a2a2a);
|
|
137
|
-
padding: 2px 8px;
|
|
138
|
-
border-radius: 10px;
|
|
139
|
-
border: 1px solid var(--chat-border, #333);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
.mode-badge {
|
|
143
|
-
font-size: 10px;
|
|
144
|
-
color: var(--chat-text-muted, #888);
|
|
145
|
-
background: var(--chat-muted, #2a2a2a);
|
|
146
|
-
padding: 2px 6px;
|
|
147
|
-
border-radius: 10px;
|
|
148
|
-
border: 1px solid var(--chat-border, #333);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.action-buttons {
|
|
152
|
-
display: flex;
|
|
153
|
-
align-items: center;
|
|
154
|
-
gap: 4px;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.action-btn {
|
|
158
|
-
display: flex;
|
|
159
|
-
align-items: center;
|
|
160
|
-
justify-content: center;
|
|
161
|
-
width: 24px;
|
|
162
|
-
height: 24px;
|
|
163
|
-
border: none;
|
|
164
|
-
background: transparent;
|
|
165
|
-
border-radius: 4px;
|
|
166
|
-
color: var(--chat-text-muted, #666);
|
|
167
|
-
cursor: pointer;
|
|
168
|
-
transition: all 0.15s;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.action-btn:hover {
|
|
172
|
-
background: var(--chat-muted, #3c3c3c);
|
|
173
|
-
color: var(--chat-text, #ccc);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
.action-btn.copied {
|
|
177
|
-
color: #22c55e;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/* 消息时间 */
|
|
181
|
-
.message-time {
|
|
182
|
-
font-size: 12px;
|
|
183
|
-
color: var(--chat-text-muted, #666);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
.user-time {
|
|
187
|
-
text-align: right;
|
|
188
|
-
margin-top: 8px;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
.assistant-time {
|
|
192
|
-
margin-right: 8px;
|
|
193
|
-
}
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MessageBubble Component
|
|
3
|
-
* 新架构:使用 ContentPart 数组渲染消息内容
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useMemo, type FC } from 'react'
|
|
7
|
-
import './MessageBubble.css'
|
|
8
|
-
import { Icon } from '@iconify/react'
|
|
9
|
-
import { PartsRenderer } from './PartsRenderer'
|
|
10
|
-
import { ChatInput } from '../input/ChatInput'
|
|
11
|
-
import { useChatInputContext } from '../../context/ChatInputContext'
|
|
12
|
-
import type { ContentPart, TextPart, ToolCallPart, SearchPart, ThinkingPart, ModelOption } from '../../types'
|
|
13
|
-
import type { ChatAdapter } from '../../adapter'
|
|
14
|
-
import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer'
|
|
15
|
-
|
|
16
|
-
interface MessageBubbleProps {
|
|
17
|
-
role: 'user' | 'assistant'
|
|
18
|
-
/** 内容 parts 数组 - 新架构核心 */
|
|
19
|
-
parts: ContentPart[]
|
|
20
|
-
/** 生成此消息时使用的模型 */
|
|
21
|
-
model?: string
|
|
22
|
-
/** 生成此消息时使用的模式 (ask/agent) */
|
|
23
|
-
mode?: string
|
|
24
|
-
/** 用户上传的图片 */
|
|
25
|
-
images?: string[]
|
|
26
|
-
/** 是否正在加载 */
|
|
27
|
-
loading?: boolean
|
|
28
|
-
/** 是否已复制 */
|
|
29
|
-
copied?: boolean
|
|
30
|
-
/** 消息时间戳 */
|
|
31
|
-
timestamp?: Date | string | number
|
|
32
|
-
onCopy?: () => void
|
|
33
|
-
onRegenerate?: () => void
|
|
34
|
-
/** 编辑用户消息后重新发送 */
|
|
35
|
-
onSend?: (text: string) => void
|
|
36
|
-
/** 步骤折叠模式 */
|
|
37
|
-
stepsExpandedType?: 'open' | 'close' | 'auto'
|
|
38
|
-
/** 工具调用相关 - 通过 props 传递 */
|
|
39
|
-
adapter?: ChatAdapter
|
|
40
|
-
/** 取消工具调用(通常会中止当前请求/流式输出) */
|
|
41
|
-
onCancelToolCall?: (toolCallId: string) => void
|
|
42
|
-
autoRunConfig?: AutoRunConfig
|
|
43
|
-
onSaveConfig?: (config: AutoRunConfig) => Promise<void>
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** 获取模型显示名称 */
|
|
47
|
-
function getModelDisplayName(modelId: string, models?: ModelOption[]): string {
|
|
48
|
-
// 优先从传入的 models 中查找
|
|
49
|
-
const found = models?.find((m) => m.modelId === modelId)
|
|
50
|
-
if (found) return found.displayName
|
|
51
|
-
// 后备:提取模型名称(前端不应该依赖后端包)
|
|
52
|
-
return modelId.split('/').pop() || modelId
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** 格式化时间显示 */
|
|
56
|
-
function formatTime(timestamp?: Date | string | number): string {
|
|
57
|
-
if (!timestamp) return ''
|
|
58
|
-
const date = new Date(timestamp)
|
|
59
|
-
const year = date.getFullYear()
|
|
60
|
-
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
61
|
-
const day = String(date.getDate()).padStart(2, '0')
|
|
62
|
-
const hours = String(date.getHours()).padStart(2, '0')
|
|
63
|
-
const minutes = String(date.getMinutes()).padStart(2, '0')
|
|
64
|
-
return `${year}-${month}-${day} ${hours}:${minutes}`
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export const MessageBubble: FC<MessageBubbleProps> = ({
|
|
68
|
-
role,
|
|
69
|
-
parts,
|
|
70
|
-
model,
|
|
71
|
-
mode,
|
|
72
|
-
images,
|
|
73
|
-
loading,
|
|
74
|
-
copied,
|
|
75
|
-
timestamp,
|
|
76
|
-
onCopy,
|
|
77
|
-
onRegenerate,
|
|
78
|
-
onSend,
|
|
79
|
-
stepsExpandedType = 'auto',
|
|
80
|
-
adapter,
|
|
81
|
-
onCancelToolCall,
|
|
82
|
-
autoRunConfig,
|
|
83
|
-
onSaveConfig,
|
|
84
|
-
}) => {
|
|
85
|
-
const isUser = role === 'user'
|
|
86
|
-
const formattedTime = formatTime(timestamp)
|
|
87
|
-
const inputContext = useChatInputContext()
|
|
88
|
-
|
|
89
|
-
// 提取用户消息的文本内容
|
|
90
|
-
const userText = useMemo(() => {
|
|
91
|
-
return parts
|
|
92
|
-
.filter((p): p is TextPart => p.type === 'text')
|
|
93
|
-
.map(p => p.text)
|
|
94
|
-
.join('')
|
|
95
|
-
}, [parts])
|
|
96
|
-
|
|
97
|
-
// 是否有内容(用于显示操作按钮)
|
|
98
|
-
const hasContent = useMemo(() => {
|
|
99
|
-
return parts.some(p =>
|
|
100
|
-
(p.type === 'text' && (p as TextPart).text) ||
|
|
101
|
-
p.type === 'thinking' ||
|
|
102
|
-
p.type === 'search' ||
|
|
103
|
-
p.type === 'tool_call'
|
|
104
|
-
)
|
|
105
|
-
}, [parts])
|
|
106
|
-
|
|
107
|
-
// loading 状态:决定显示什么类型的指示器
|
|
108
|
-
// text: 文字提示(等待状态时)
|
|
109
|
-
// none: 不显示(有正在进行的活动)
|
|
110
|
-
const loadingState = useMemo<{ type: 'text' | 'none'; text?: string }>(() => {
|
|
111
|
-
if (!loading) {
|
|
112
|
-
return { type: 'none' }
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 分模式显示:ask 模式直接生成回答,agent 模式规划下一步
|
|
116
|
-
const waitingText = mode === 'ask' ? '正在生成回答...' : '正在规划下一步...'
|
|
117
|
-
|
|
118
|
-
// 没有任何 parts 时,显示等待
|
|
119
|
-
if (parts.length === 0) {
|
|
120
|
-
return { type: 'text', text: waitingText }
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 检查是否有正在进行的活动(如果有,不需要显示等待提示)
|
|
124
|
-
const hasActiveActivity = parts.some(part => {
|
|
125
|
-
// 思考正在进行
|
|
126
|
-
if (part.type === 'thinking' && (part as ThinkingPart).status === 'running') {
|
|
127
|
-
return true
|
|
128
|
-
}
|
|
129
|
-
// 搜索正在进行
|
|
130
|
-
if (part.type === 'search' && (part as SearchPart).status === 'running') {
|
|
131
|
-
return true
|
|
132
|
-
}
|
|
133
|
-
// 工具调用正在进行(running 或 pending)
|
|
134
|
-
if (part.type === 'tool_call') {
|
|
135
|
-
const status = (part as ToolCallPart).status
|
|
136
|
-
if (status === 'running' || status === 'pending') {
|
|
137
|
-
return true
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return false
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
// 有正在进行的活动 → 不显示等待提示(活动本身有状态显示)
|
|
144
|
-
if (hasActiveActivity) {
|
|
145
|
-
return { type: 'none' }
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 检查最后一个 part 是否是正在流式输出的文本
|
|
149
|
-
const lastPart = parts[parts.length - 1]
|
|
150
|
-
if (lastPart.type === 'text') {
|
|
151
|
-
// 文本正在流式输出 → 不显示等待提示
|
|
152
|
-
return { type: 'none' }
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 没有任何正在进行的活动,但 loading=true → 显示等待提示
|
|
156
|
-
// 这包括:工具执行完成后、思考完成后、搜索完成后等"空窗期"
|
|
157
|
-
return { type: 'text', text: waitingText }
|
|
158
|
-
}, [loading, parts, mode])
|
|
159
|
-
|
|
160
|
-
return (
|
|
161
|
-
<div className={`message-bubble ${role}`}>
|
|
162
|
-
{/* 用户消息 */}
|
|
163
|
-
{isUser ? (
|
|
164
|
-
onSend && inputContext ? (
|
|
165
|
-
<ChatInput
|
|
166
|
-
variant="message"
|
|
167
|
-
value={userText}
|
|
168
|
-
images={images}
|
|
169
|
-
mode={inputContext.mode}
|
|
170
|
-
model={inputContext.model}
|
|
171
|
-
models={inputContext.models}
|
|
172
|
-
webSearchEnabled={inputContext.webSearch}
|
|
173
|
-
thinkingEnabled={inputContext.thinking}
|
|
174
|
-
isLoading={inputContext.isLoading}
|
|
175
|
-
onSend={onSend}
|
|
176
|
-
onModeChange={inputContext.setMode}
|
|
177
|
-
onModelChange={inputContext.setModel}
|
|
178
|
-
onWebSearchChange={inputContext.setWebSearch}
|
|
179
|
-
onThinkingChange={inputContext.setThinking}
|
|
180
|
-
/>
|
|
181
|
-
) : (
|
|
182
|
-
<div className="user-content">
|
|
183
|
-
<div className="user-text">{userText}</div>
|
|
184
|
-
{images && images.length > 0 && (
|
|
185
|
-
<div className="user-images">
|
|
186
|
-
{images.map((img, i) => (
|
|
187
|
-
<img key={i} src={img} className="user-image" alt="" />
|
|
188
|
-
))}
|
|
189
|
-
</div>
|
|
190
|
-
)}
|
|
191
|
-
{formattedTime && <div className="message-time user-time">{formattedTime}</div>}
|
|
192
|
-
</div>
|
|
193
|
-
)
|
|
194
|
-
) : (
|
|
195
|
-
/* 助手消息 - 使用 PartsRenderer 渲染 */
|
|
196
|
-
<>
|
|
197
|
-
<div className="assistant-content">
|
|
198
|
-
<PartsRenderer
|
|
199
|
-
parts={parts}
|
|
200
|
-
expandedType={stepsExpandedType}
|
|
201
|
-
adapter={adapter}
|
|
202
|
-
onCancelToolCall={onCancelToolCall}
|
|
203
|
-
autoRunConfig={autoRunConfig}
|
|
204
|
-
onSaveConfig={onSaveConfig}
|
|
205
|
-
/>
|
|
206
|
-
|
|
207
|
-
{/* 加载指示器:等待状态时显示 */}
|
|
208
|
-
{loadingState.type === 'text' && (
|
|
209
|
-
<div className={`loading-indicator${parts.length > 0 ? ' has-content-above' : ''}`}>
|
|
210
|
-
<span className="loading-text">{loadingState.text}</span>
|
|
211
|
-
<span className="loading-shimmer"></span>
|
|
212
|
-
</div>
|
|
213
|
-
)}
|
|
214
|
-
</div>
|
|
215
|
-
|
|
216
|
-
{/* 操作按钮 */}
|
|
217
|
-
{hasContent && loading === false && (
|
|
218
|
-
<div className="message-actions">
|
|
219
|
-
{/* 左侧:模型信息 */}
|
|
220
|
-
<div className="message-meta">
|
|
221
|
-
{model && <span className="model-name">{getModelDisplayName(model, inputContext?.models)}</span>}
|
|
222
|
-
{mode && <span className="mode-badge">{mode === 'ask' ? 'Ask' : 'Agent'}</span>}
|
|
223
|
-
</div>
|
|
224
|
-
{/* 右侧:时间和按钮 */}
|
|
225
|
-
<div className="action-buttons">
|
|
226
|
-
{formattedTime && <span className="message-time assistant-time">{formattedTime}</span>}
|
|
227
|
-
<button className={`action-btn${copied ? ' copied' : ''}`} onClick={onCopy} title="复制">
|
|
228
|
-
<Icon icon={copied ? 'lucide:check' : 'lucide:copy'} width={14} />
|
|
229
|
-
</button>
|
|
230
|
-
<button className="action-btn" onClick={onRegenerate} title="重新生成">
|
|
231
|
-
<Icon icon="lucide:refresh-cw" width={14} />
|
|
232
|
-
</button>
|
|
233
|
-
</div>
|
|
234
|
-
</div>
|
|
235
|
-
)}
|
|
236
|
-
</>
|
|
237
|
-
)}
|
|
238
|
-
</div>
|
|
239
|
-
)
|
|
240
|
-
}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { useContext, useMemo, type FC, type ComponentType } from 'react'
|
|
2
|
-
import type {
|
|
3
|
-
ContentPart,
|
|
4
|
-
StepsExpandedType,
|
|
5
|
-
TextPart as TextPartType,
|
|
6
|
-
ThinkingPart as ThinkingPartType,
|
|
7
|
-
SearchPart as SearchPartType,
|
|
8
|
-
ToolCallPart as ToolCallPartType,
|
|
9
|
-
ImagePart as ImagePartType,
|
|
10
|
-
ErrorPart as ErrorPartType,
|
|
11
|
-
} from '../../types'
|
|
12
|
-
import type { ChatAdapter } from '../../adapter'
|
|
13
|
-
import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer'
|
|
14
|
-
import { PartRenderersContext } from '../../context/RenderersContext'
|
|
15
|
-
import {
|
|
16
|
-
TextPart,
|
|
17
|
-
ThinkingPart,
|
|
18
|
-
SearchPart,
|
|
19
|
-
ImagePart,
|
|
20
|
-
ErrorPart,
|
|
21
|
-
} from './parts'
|
|
22
|
-
import { ToolCallPart } from './parts/ToolCallPart'
|
|
23
|
-
import './PartsRenderer.css'
|
|
24
|
-
|
|
25
|
-
/** Part 渲染器映射类型 */
|
|
26
|
-
type PartRenderers = Record<string, ComponentType<Record<string, unknown>>>
|
|
27
|
-
|
|
28
|
-
interface PartsRendererProps {
|
|
29
|
-
parts: ContentPart[]
|
|
30
|
-
expandedType?: StepsExpandedType
|
|
31
|
-
/** 自定义 Part 渲染器(props 传入优先级高于 context) */
|
|
32
|
-
partRenderers?: PartRenderers
|
|
33
|
-
// 工具调用相关
|
|
34
|
-
adapter?: ChatAdapter
|
|
35
|
-
onCancelToolCall?: (toolCallId: string) => void
|
|
36
|
-
autoRunConfig?: AutoRunConfig
|
|
37
|
-
onSaveConfig?: (config: AutoRunConfig) => Promise<void>
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ==================== 类型守卫函数 ====================
|
|
41
|
-
|
|
42
|
-
function isText(part: ContentPart): part is TextPartType {
|
|
43
|
-
return part.type === 'text'
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function isThinking(part: ContentPart): part is ThinkingPartType {
|
|
47
|
-
return part.type === 'thinking'
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function isSearch(part: ContentPart): part is SearchPartType {
|
|
51
|
-
return part.type === 'search'
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function isToolCall(part: ContentPart): part is ToolCallPartType {
|
|
55
|
-
return part.type === 'tool_call'
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isImage(part: ContentPart): part is ImagePartType {
|
|
59
|
-
return part.type === 'image'
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function isError(part: ContentPart): part is ErrorPartType {
|
|
63
|
-
return part.type === 'error'
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export const PartsRenderer: FC<PartsRendererProps> = ({
|
|
67
|
-
parts,
|
|
68
|
-
expandedType = 'auto',
|
|
69
|
-
partRenderers: propRenderers,
|
|
70
|
-
adapter,
|
|
71
|
-
onCancelToolCall,
|
|
72
|
-
autoRunConfig,
|
|
73
|
-
onSaveConfig,
|
|
74
|
-
}) => {
|
|
75
|
-
// 从 context 获取渲染器,props 优先
|
|
76
|
-
const contextRenderers = useContext(PartRenderersContext)
|
|
77
|
-
const partRenderers = propRenderers ?? contextRenderers
|
|
78
|
-
|
|
79
|
-
/** 可见的 parts */
|
|
80
|
-
const visibleParts = useMemo(() => parts, [parts])
|
|
81
|
-
|
|
82
|
-
const renderPart = (part: ContentPart) => {
|
|
83
|
-
// 先检查是否有自定义渲染器
|
|
84
|
-
const CustomRenderer = partRenderers[part.type]
|
|
85
|
-
if (CustomRenderer) {
|
|
86
|
-
return <CustomRenderer {...part} />
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// 内置类型渲染(使用类型守卫)
|
|
90
|
-
if (isText(part)) {
|
|
91
|
-
return <TextPart text={part.text} />
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (isThinking(part)) {
|
|
95
|
-
return (
|
|
96
|
-
<ThinkingPart
|
|
97
|
-
text={part.text}
|
|
98
|
-
status={part.status}
|
|
99
|
-
duration={part.duration}
|
|
100
|
-
expandedType={expandedType}
|
|
101
|
-
/>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (isSearch(part)) {
|
|
106
|
-
return (
|
|
107
|
-
<SearchPart
|
|
108
|
-
query={part.query}
|
|
109
|
-
results={part.results}
|
|
110
|
-
status={part.status}
|
|
111
|
-
expandedType={expandedType}
|
|
112
|
-
/>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (isToolCall(part)) {
|
|
117
|
-
return (
|
|
118
|
-
<ToolCallPart
|
|
119
|
-
id={part.id}
|
|
120
|
-
name={part.name}
|
|
121
|
-
args={part.args}
|
|
122
|
-
output={part.output}
|
|
123
|
-
status={part.status}
|
|
124
|
-
expandedType={expandedType}
|
|
125
|
-
adapter={adapter}
|
|
126
|
-
onCancelToolCall={onCancelToolCall}
|
|
127
|
-
autoRunConfig={autoRunConfig}
|
|
128
|
-
onSaveConfig={onSaveConfig}
|
|
129
|
-
/>
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (isImage(part)) {
|
|
134
|
-
return <ImagePart url={part.url} />
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (isError(part)) {
|
|
138
|
-
return (
|
|
139
|
-
<ErrorPart
|
|
140
|
-
message={part.message}
|
|
141
|
-
category={part.category}
|
|
142
|
-
retryable={part.retryable}
|
|
143
|
-
/>
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 未知类型,显示 JSON
|
|
148
|
-
return (
|
|
149
|
-
<div className="unknown-part">
|
|
150
|
-
<pre>{JSON.stringify(part, null, 2)}</pre>
|
|
151
|
-
</div>
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return (
|
|
156
|
-
<div className="parts-renderer">
|
|
157
|
-
{visibleParts.map((part, index) => (
|
|
158
|
-
<div
|
|
159
|
-
key={index}
|
|
160
|
-
className="part-item"
|
|
161
|
-
style={{ marginTop: index > 0 ? '8px' : '0' }}
|
|
162
|
-
>
|
|
163
|
-
{renderPart(part)}
|
|
164
|
-
</div>
|
|
165
|
-
))}
|
|
166
|
-
</div>
|
|
167
|
-
)
|
|
168
|
-
}
|