@huyooo/ai-chat-frontend-vue 0.1.6 → 0.1.7

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 (159) hide show
  1. package/README.md +367 -0
  2. package/dist/adapter.d.ts +7 -7
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/components/ChatPanel.vue.d.ts +120 -9
  5. package/dist/components/ChatPanel.vue.d.ts.map +1 -1
  6. package/dist/components/common/ConfirmDialog.vue.d.ts +30 -0
  7. package/dist/components/common/ConfirmDialog.vue.d.ts.map +1 -0
  8. package/dist/components/common/CopyButton.vue.d.ts +18 -0
  9. package/dist/components/common/CopyButton.vue.d.ts.map +1 -0
  10. package/dist/components/common/IndexingSettings.vue.d.ts +3 -0
  11. package/dist/components/common/IndexingSettings.vue.d.ts.map +1 -0
  12. package/dist/components/common/SettingsPanel.vue.d.ts +16 -0
  13. package/dist/components/common/SettingsPanel.vue.d.ts.map +1 -0
  14. package/dist/components/common/Toast.vue.d.ts +18 -0
  15. package/dist/components/common/Toast.vue.d.ts.map +1 -0
  16. package/dist/components/common/ToggleSwitch.vue.d.ts +10 -0
  17. package/dist/components/common/ToggleSwitch.vue.d.ts.map +1 -0
  18. package/dist/components/{chat/ui → header}/ChatHeader.vue.d.ts +5 -3
  19. package/dist/components/header/ChatHeader.vue.d.ts.map +1 -0
  20. package/dist/components/input/AtFilePicker.vue.d.ts +21 -0
  21. package/dist/components/input/AtFilePicker.vue.d.ts.map +1 -0
  22. package/dist/components/{ChatInput.vue.d.ts → input/ChatInput.vue.d.ts} +16 -14
  23. package/dist/components/input/ChatInput.vue.d.ts.map +1 -0
  24. package/dist/components/input/DropdownSelector.vue.d.ts +42 -0
  25. package/dist/components/input/DropdownSelector.vue.d.ts.map +1 -0
  26. package/dist/components/input/ImagePreviewModal.vue.d.ts +17 -0
  27. package/dist/components/input/ImagePreviewModal.vue.d.ts.map +1 -0
  28. package/dist/components/input/at-views/AtBranchView.vue.d.ts +18 -0
  29. package/dist/components/input/at-views/AtBranchView.vue.d.ts.map +1 -0
  30. package/dist/components/input/at-views/AtBrowserView.vue.d.ts +18 -0
  31. package/dist/components/input/at-views/AtBrowserView.vue.d.ts.map +1 -0
  32. package/dist/components/input/at-views/AtChatsView.vue.d.ts +18 -0
  33. package/dist/components/input/at-views/AtChatsView.vue.d.ts.map +1 -0
  34. package/dist/components/input/at-views/AtDocsView.vue.d.ts +18 -0
  35. package/dist/components/input/at-views/AtDocsView.vue.d.ts.map +1 -0
  36. package/dist/components/input/at-views/AtFilesView.vue.d.ts +23 -0
  37. package/dist/components/input/at-views/AtFilesView.vue.d.ts.map +1 -0
  38. package/dist/components/input/at-views/AtTerminalsView.vue.d.ts +18 -0
  39. package/dist/components/input/at-views/AtTerminalsView.vue.d.ts.map +1 -0
  40. package/dist/components/message/MessageBubble.vue.d.ts +45 -0
  41. package/dist/components/message/MessageBubble.vue.d.ts.map +1 -0
  42. package/dist/components/message/PartsRenderer.vue.d.ts +15 -0
  43. package/dist/components/message/PartsRenderer.vue.d.ts.map +1 -0
  44. package/dist/components/message/WelcomeMessage.vue.d.ts +14 -0
  45. package/dist/components/message/WelcomeMessage.vue.d.ts.map +1 -0
  46. package/dist/components/message/blocks/CodeBlock.vue.d.ts +11 -0
  47. package/dist/components/message/blocks/CodeBlock.vue.d.ts.map +1 -0
  48. package/dist/components/{chat/SearchResultBlock.vue.d.ts → message/blocks/TextBlock.vue.d.ts} +3 -4
  49. package/dist/components/message/blocks/TextBlock.vue.d.ts.map +1 -0
  50. package/dist/components/message/blocks/index.d.ts +6 -0
  51. package/dist/components/message/blocks/index.d.ts.map +1 -0
  52. package/dist/components/message/parts/CollapsibleCard.vue.d.ts +45 -0
  53. package/dist/components/message/parts/CollapsibleCard.vue.d.ts.map +1 -0
  54. package/dist/components/{chat/ToolCallBlock.vue.d.ts → message/parts/ErrorPart.vue.d.ts} +4 -5
  55. package/dist/components/message/parts/ErrorPart.vue.d.ts.map +1 -0
  56. package/dist/components/{chat/ThinkingBlock.vue.d.ts → message/parts/ImagePart.vue.d.ts} +3 -3
  57. package/dist/components/message/parts/ImagePart.vue.d.ts.map +1 -0
  58. package/dist/components/message/parts/SearchPart.vue.d.ts +12 -0
  59. package/dist/components/message/parts/SearchPart.vue.d.ts.map +1 -0
  60. package/dist/components/{chat/messages/ExecutionSteps.vue.d.ts → message/parts/TextPart.vue.d.ts} +2 -9
  61. package/dist/components/message/parts/TextPart.vue.d.ts.map +1 -0
  62. package/dist/components/message/parts/ThinkingPart.vue.d.ts +12 -0
  63. package/dist/components/message/parts/ThinkingPart.vue.d.ts.map +1 -0
  64. package/dist/components/message/parts/ToolCallPart.vue.d.ts +19 -0
  65. package/dist/components/message/parts/ToolCallPart.vue.d.ts.map +1 -0
  66. package/dist/components/message/parts/ToolResultPart.vue.d.ts +14 -0
  67. package/dist/components/message/parts/ToolResultPart.vue.d.ts.map +1 -0
  68. package/dist/components/message/parts/index.d.ts +12 -0
  69. package/dist/components/message/parts/index.d.ts.map +1 -0
  70. package/dist/components/message/tool-results/DefaultToolResult.vue.d.ts +4 -0
  71. package/dist/components/message/tool-results/DefaultToolResult.vue.d.ts.map +1 -0
  72. package/dist/components/message/tool-results/SearchResults.vue.d.ts +4 -0
  73. package/dist/components/message/tool-results/SearchResults.vue.d.ts.map +1 -0
  74. package/dist/components/message/tool-results/WeatherCard.vue.d.ts +4 -0
  75. package/dist/components/message/tool-results/WeatherCard.vue.d.ts.map +1 -0
  76. package/dist/components/message/tool-results/index.d.ts +7 -0
  77. package/dist/components/message/tool-results/index.d.ts.map +1 -0
  78. package/dist/components/message/welcome-types.d.ts +28 -0
  79. package/dist/components/message/welcome-types.d.ts.map +1 -0
  80. package/dist/composables/useChat.d.ts +99 -44
  81. package/dist/composables/useChat.d.ts.map +1 -1
  82. package/dist/composables/useImageUpload.d.ts +55 -0
  83. package/dist/composables/useImageUpload.d.ts.map +1 -0
  84. package/dist/index.d.ts +25 -26
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +55871 -1252
  87. package/dist/style.css +1 -1
  88. package/dist/types/index.d.ts +113 -53
  89. package/dist/types/index.d.ts.map +1 -1
  90. package/dist/utils/fileIcon.d.ts +13 -0
  91. package/dist/utils/fileIcon.d.ts.map +1 -0
  92. package/package.json +12 -6
  93. package/src/adapter.ts +12 -70
  94. package/src/components/ChatPanel.vue +329 -110
  95. package/src/components/common/ConfirmDialog.vue +208 -0
  96. package/src/components/common/CopyButton.vue +71 -0
  97. package/src/components/common/IndexingSettings.vue +580 -0
  98. package/src/components/common/SettingsPanel.vue +293 -0
  99. package/src/components/common/Toast.vue +90 -0
  100. package/src/components/common/ToggleSwitch.vue +75 -0
  101. package/src/components/{chat/ui → header}/ChatHeader.vue +170 -93
  102. package/src/components/input/AtFilePicker.vue +657 -0
  103. package/src/components/input/ChatInput.vue +653 -0
  104. package/src/components/input/DropdownSelector.vue +322 -0
  105. package/src/components/input/ImagePreviewModal.vue +238 -0
  106. package/src/components/input/at-views/AtBranchView.vue +63 -0
  107. package/src/components/input/at-views/AtBrowserView.vue +63 -0
  108. package/src/components/input/at-views/AtChatsView.vue +63 -0
  109. package/src/components/input/at-views/AtDocsView.vue +63 -0
  110. package/src/components/input/at-views/AtFilesView.vue +255 -0
  111. package/src/components/input/at-views/AtTerminalsView.vue +63 -0
  112. package/src/components/message/ContentRenderer.vue +61 -0
  113. package/src/components/message/MessageBubble.vue +411 -0
  114. package/src/components/message/PartsRenderer.vue +101 -0
  115. package/src/components/message/ToolResultRenderer.vue +27 -0
  116. package/src/components/message/WelcomeMessage.vue +308 -0
  117. package/src/components/message/blocks/CodeBlock.vue +113 -0
  118. package/src/components/message/blocks/TextBlock.vue +21 -0
  119. package/src/components/message/blocks/index.ts +6 -0
  120. package/src/components/message/parts/CollapsibleCard.vue +135 -0
  121. package/src/components/message/parts/ErrorPart.vue +51 -0
  122. package/src/components/message/parts/ImagePart.vue +98 -0
  123. package/src/components/message/parts/SearchPart.vue +101 -0
  124. package/src/components/message/parts/TextPart.vue +28 -0
  125. package/src/components/message/parts/ThinkingPart.vue +54 -0
  126. package/src/components/message/parts/ToolCallPart.vue +460 -0
  127. package/src/components/message/parts/ToolResultPart.vue +78 -0
  128. package/src/components/message/parts/index.ts +13 -0
  129. package/src/components/message/tool-results/DefaultToolResult.vue +43 -0
  130. package/src/components/message/tool-results/SearchResults.vue +133 -0
  131. package/src/components/message/tool-results/WeatherCard.vue +139 -0
  132. package/src/components/message/tool-results/index.ts +7 -0
  133. package/src/components/message/welcome-types.ts +47 -0
  134. package/src/composables/useChat.ts +807 -155
  135. package/src/composables/useImageUpload.ts +228 -0
  136. package/src/index.ts +93 -46
  137. package/src/styles.css +47 -0
  138. package/src/types/index.ts +146 -98
  139. package/src/utils/fileIcon.ts +49 -0
  140. package/dist/components/ChatInput.vue.d.ts.map +0 -1
  141. package/dist/components/chat/SearchResultBlock.vue.d.ts.map +0 -1
  142. package/dist/components/chat/ThinkingBlock.vue.d.ts.map +0 -1
  143. package/dist/components/chat/ToolCallBlock.vue.d.ts.map +0 -1
  144. package/dist/components/chat/messages/ExecutionSteps.vue.d.ts.map +0 -1
  145. package/dist/components/chat/messages/MessageBubble.vue.d.ts +0 -28
  146. package/dist/components/chat/messages/MessageBubble.vue.d.ts.map +0 -1
  147. package/dist/components/chat/ui/ChatHeader.vue.d.ts.map +0 -1
  148. package/dist/components/chat/ui/WelcomeMessage.vue.d.ts +0 -7
  149. package/dist/components/chat/ui/WelcomeMessage.vue.d.ts.map +0 -1
  150. package/dist/preload/preload.d.ts +0 -6
  151. package/dist/preload/preload.d.ts.map +0 -1
  152. package/src/components/ChatInput.vue +0 -649
  153. package/src/components/chat/SearchResultBlock.vue +0 -155
  154. package/src/components/chat/ThinkingBlock.vue +0 -109
  155. package/src/components/chat/ToolCallBlock.vue +0 -213
  156. package/src/components/chat/messages/ExecutionSteps.vue +0 -281
  157. package/src/components/chat/messages/MessageBubble.vue +0 -272
  158. package/src/components/chat/ui/WelcomeMessage.vue +0 -135
  159. package/src/preload/preload.ts +0 -79
@@ -0,0 +1,411 @@
1
+ <template>
2
+ <div :class="['message-bubble', role]">
3
+ <!-- 用户消息 - 复用 ChatInput 组件 -->
4
+ <template v-if="role === 'user'">
5
+ <ChatInput
6
+ v-if="onSend && inputState"
7
+ variant="message"
8
+ :value="userText"
9
+ :mode="inputState.mode.value"
10
+ :model="inputState.model.value"
11
+ :models="inputState.models.value"
12
+ :web-search-enabled="inputState.webSearch.value"
13
+ :thinking-enabled="inputState.thinking.value"
14
+ :is-loading="inputState.isLoading.value"
15
+ @send="(text) => $emit('send', text)"
16
+ @update:mode="inputState.setMode"
17
+ @update:model="inputState.setModel"
18
+ @update:webSearch="inputState.setWebSearch"
19
+ @update:thinking="inputState.setThinking"
20
+ />
21
+ <div v-else class="user-content">
22
+ <div class="user-text">{{ userText }}</div>
23
+ <div v-if="images && images.length > 0" class="user-images">
24
+ <img
25
+ v-for="(img, i) in images"
26
+ :key="i"
27
+ :src="img"
28
+ class="user-image"
29
+ @click="$emit('view-image', img)"
30
+ />
31
+ </div>
32
+ <div v-if="formattedTime" class="message-time user-time">{{ formattedTime }}</div>
33
+ </div>
34
+ </template>
35
+
36
+ <!-- 助手消息 - 使用 PartsRenderer 渲染 -->
37
+ <template v-else>
38
+ <div class="assistant-content">
39
+ <!-- 使用新架构:PartsRenderer 渲染所有 parts -->
40
+ <PartsRenderer
41
+ :parts="parts"
42
+ :expanded-type="stepsExpandedType"
43
+ :adapter="adapter"
44
+ :auto-run-config="autoRunConfig"
45
+ :on-save-config="onSaveConfig"
46
+ />
47
+
48
+ <!-- 加载指示器:等待状态时显示 -->
49
+ <div v-if="loadingState.type === 'text'" class="loading-indicator">
50
+ <span class="loading-text">{{ loadingState.text }}</span>
51
+ <span class="loading-shimmer"></span>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- 操作按钮 -->
56
+ <div v-if="hasContent && loading === false" class="message-actions">
57
+ <!-- 左侧:模型信息 -->
58
+ <div class="message-meta">
59
+ <span v-if="model" class="model-name">{{ getModelDisplayName(model) }}</span>
60
+ <span v-if="mode" class="mode-badge">{{ mode === 'ask' ? 'Ask' : 'Agent' }}</span>
61
+ </div>
62
+ <!-- 右侧:时间和按钮 -->
63
+ <div class="action-buttons">
64
+ <span v-if="formattedTime" class="message-time assistant-time">{{ formattedTime }}</span>
65
+ <button class="action-btn" title="复制" @click="$emit('copy')">
66
+ <Icon :icon="copied ? 'lucide:check' : 'lucide:copy'" width="14" />
67
+ </button>
68
+ <button class="action-btn" title="重新生成" @click="$emit('regenerate')">
69
+ <Icon icon="lucide:refresh-cw" width="14" />
70
+ </button>
71
+ </div>
72
+ </div>
73
+ </template>
74
+ </div>
75
+ </template>
76
+
77
+ <script setup lang="ts">
78
+ import { computed, inject, type Ref } from 'vue'
79
+ import { Icon } from '@iconify/vue'
80
+ import ChatInput from '../input/ChatInput.vue'
81
+ import PartsRenderer from './PartsRenderer.vue'
82
+ import type { ContentPart, TextPart, ToolCallPart, SearchPart, ThinkingPart, ChatMode, ModelOption } from '../../types'
83
+ import type { ChatAdapter } from '../../adapter'
84
+ import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer'
85
+
86
+ // 注入全局 input 状态
87
+ interface ChatInputState {
88
+ mode: Ref<ChatMode>
89
+ model: Ref<string>
90
+ webSearch: Ref<boolean>
91
+ thinking: Ref<boolean>
92
+ models: Ref<ModelOption[]>
93
+ isLoading: Ref<boolean>
94
+ setMode: (value: ChatMode) => void
95
+ setModel: (value: string) => void
96
+ setWebSearch: (value: boolean) => void
97
+ setThinking: (value: boolean) => void
98
+ }
99
+
100
+ const inputState = inject<ChatInputState>('chatInputState')
101
+
102
+ const props = withDefaults(defineProps<{
103
+ role: 'user' | 'assistant'
104
+ /** 内容 parts 数组 - 新架构核心 */
105
+ parts: ContentPart[]
106
+ /** 生成此消息时使用的模型 */
107
+ model?: string
108
+ /** 生成此消息时使用的模式 (ask/agent) */
109
+ mode?: string
110
+ /** 用户上传的图片 */
111
+ images?: string[]
112
+ /** 是否正在加载 */
113
+ loading?: boolean
114
+ /** 是否已复制 */
115
+ copied?: boolean
116
+ /** 消息时间戳 */
117
+ timestamp?: Date | string | number
118
+ /** 是否支持重新发送(仅用户消息) */
119
+ onSend?: (text: string) => void
120
+ /** 步骤折叠模式 */
121
+ stepsExpandedType?: 'open' | 'close' | 'auto'
122
+ /** 工具调用相关 - 通过 props 传递 */
123
+ adapter?: ChatAdapter
124
+ autoRunConfig?: AutoRunConfig
125
+ onSaveConfig?: (config: AutoRunConfig) => Promise<void>
126
+ }>(), {
127
+ loading: false,
128
+ copied: false,
129
+ stepsExpandedType: 'auto',
130
+ })
131
+
132
+ // 提取用户消息的文本内容
133
+ const userText = computed(() => {
134
+ return props.parts
135
+ .filter((p): p is TextPart => p.type === 'text')
136
+ .map(p => p.text)
137
+ .join('')
138
+ })
139
+
140
+ // 是否有内容(用于显示操作按钮)
141
+ const hasContent = computed(() => {
142
+ return props.parts.some(p =>
143
+ (p.type === 'text' && p.text) ||
144
+ p.type === 'tool_result' ||
145
+ p.type === 'thinking' ||
146
+ p.type === 'search'
147
+ )
148
+ })
149
+
150
+ // loading 状态:决定显示什么类型的指示器
151
+ // cursor: 闪烁光标(文本流式输出时)
152
+ // text: 文字提示(等待状态时)
153
+ // none: 不显示
154
+ const loadingState = computed<{ type: 'cursor' | 'text' | 'none'; text?: string }>(() => {
155
+ if (!props.loading) {
156
+ return { type: 'none' }
157
+ }
158
+
159
+ if (props.parts.length === 0) {
160
+ return { type: 'text', text: '正在思考...' }
161
+ }
162
+
163
+ const lastPart = props.parts[props.parts.length - 1]
164
+
165
+ // 文本流式输出 → 不需要额外指示,用户能看到文字在增加
166
+ if (lastPart.type === 'text') {
167
+ return { type: 'none' }
168
+ }
169
+
170
+ // 工具调用完成后 → 显示规划提示
171
+ if (lastPart.type === 'tool_call') {
172
+ const status = (lastPart as ToolCallPart).status
173
+ if (status === 'done' || status === 'error' || status === 'skipped') {
174
+ return { type: 'text', text: '正在规划下一步...' }
175
+ }
176
+ // 工具正在执行时,卡片本身有状态,不需要额外指示
177
+ return { type: 'none' }
178
+ }
179
+
180
+ // 搜索完成后 → 显示规划提示
181
+ if (lastPart.type === 'search') {
182
+ if ((lastPart as SearchPart).status === 'done') {
183
+ return { type: 'text', text: '正在规划下一步...' }
184
+ }
185
+ return { type: 'none' }
186
+ }
187
+
188
+ // 思考完成后 → 显示规划提示
189
+ if (lastPart.type === 'thinking') {
190
+ if ((lastPart as ThinkingPart).status === 'done') {
191
+ return { type: 'text', text: '正在规划下一步...' }
192
+ }
193
+ return { type: 'none' }
194
+ }
195
+
196
+ return { type: 'none' }
197
+ })
198
+
199
+ /** 获取模型显示名称 */
200
+ function getModelDisplayName(modelId: string): string {
201
+ // 优先从注入的 models 中查找
202
+ const models = inputState?.models.value ?? []
203
+ const found = models.find((m) => m.modelId === modelId)
204
+ if (found) return found.displayName
205
+ // 后备:提取模型名称(前端不应该依赖后端包)
206
+ return modelId.split('/').pop() || modelId
207
+ }
208
+
209
+ // 格式化时间显示
210
+ const formattedTime = computed(() => {
211
+ if (!props.timestamp) return ''
212
+ const date = new Date(props.timestamp)
213
+ const year = date.getFullYear()
214
+ const month = String(date.getMonth() + 1).padStart(2, '0')
215
+ const day = String(date.getDate()).padStart(2, '0')
216
+ const hours = String(date.getHours()).padStart(2, '0')
217
+ const minutes = String(date.getMinutes()).padStart(2, '0')
218
+ return `${year}-${month}-${day} ${hours}:${minutes}`
219
+ })
220
+
221
+ defineEmits<{
222
+ copy: []
223
+ regenerate: []
224
+ 'view-image': [url: string]
225
+ send: [text: string]
226
+ }>()
227
+ </script>
228
+
229
+ <style scoped>
230
+ .message-bubble {
231
+ padding: 8px 0;
232
+ animation: fadeIn 0.2s ease;
233
+ }
234
+
235
+ @keyframes fadeIn {
236
+ from {
237
+ opacity: 0;
238
+ transform: translateY(4px);
239
+ }
240
+ to {
241
+ opacity: 1;
242
+ transform: translateY(0);
243
+ }
244
+ }
245
+
246
+ /* 用户消息 */
247
+ .message-bubble.user {
248
+ width: 100%;
249
+ }
250
+
251
+ .user-content {
252
+ width: 100%;
253
+ background: var(--chat-muted, #2d2d2d);
254
+ color: var(--chat-text, #ccc);
255
+ padding: 12px;
256
+ border-radius: 12px;
257
+ border: 1px solid var(--chat-border, #444);
258
+ }
259
+
260
+ .user-text {
261
+ font-size: 14px;
262
+ line-height: 1.5;
263
+ white-space: pre-wrap;
264
+ word-break: break-word;
265
+ }
266
+
267
+ .user-images {
268
+ display: flex;
269
+ gap: 8px;
270
+ margin-top: 8px;
271
+ flex-wrap: wrap;
272
+ }
273
+
274
+ .user-image {
275
+ width: 80px;
276
+ height: 80px;
277
+ object-fit: cover;
278
+ border-radius: 8px;
279
+ cursor: pointer;
280
+ transition: transform 0.15s;
281
+ }
282
+
283
+ .user-image:hover {
284
+ transform: scale(1.05);
285
+ }
286
+
287
+ /* 助手消息 */
288
+ .message-bubble.assistant {
289
+ position: relative;
290
+ }
291
+
292
+ .assistant-content {
293
+ max-width: 100%;
294
+ }
295
+
296
+ /* 加载提示 - 等待状态时 */
297
+ .loading-indicator {
298
+ position: relative;
299
+ display: flex;
300
+ align-items: center;
301
+ width: 100%;
302
+ padding: 10px 16px;
303
+ background: var(--chat-muted, #2a2a2a);
304
+ border-radius: 8px;
305
+ overflow: hidden;
306
+ margin: 8px 0;
307
+ }
308
+
309
+ .loading-text {
310
+ font-size: 13px;
311
+ color: var(--chat-text-muted, #888);
312
+ position: relative;
313
+ z-index: 1;
314
+ }
315
+
316
+ .loading-shimmer {
317
+ position: absolute;
318
+ top: 0;
319
+ left: -100%;
320
+ width: 100%;
321
+ height: 100%;
322
+ background: linear-gradient(
323
+ 90deg,
324
+ transparent 0%,
325
+ rgba(255, 255, 255, 0.08) 50%,
326
+ transparent 100%
327
+ );
328
+ animation: shimmer 1.5s ease-in-out infinite;
329
+ }
330
+
331
+ @keyframes shimmer {
332
+ 0% {
333
+ left: -100%;
334
+ }
335
+ 100% {
336
+ left: 100%;
337
+ }
338
+ }
339
+
340
+ /* 操作按钮 */
341
+ .message-actions {
342
+ display: flex;
343
+ align-items: center;
344
+ justify-content: space-between;
345
+ margin-top: 8px;
346
+ }
347
+
348
+ .message-meta {
349
+ display: flex;
350
+ align-items: center;
351
+ gap: 6px;
352
+ }
353
+
354
+ .model-name {
355
+ font-size: 11px;
356
+ color: var(--chat-text-muted, #888);
357
+ background: var(--chat-muted, #2a2a2a);
358
+ padding: 2px 8px;
359
+ border-radius: 10px;
360
+ border: 1px solid var(--chat-border, #333);
361
+ }
362
+
363
+ .mode-badge {
364
+ font-size: 10px;
365
+ color: var(--chat-text-muted, #888);
366
+ background: var(--chat-muted, #2a2a2a);
367
+ padding: 2px 6px;
368
+ border-radius: 10px;
369
+ border: 1px solid var(--chat-border, #333);
370
+ }
371
+
372
+ .action-buttons {
373
+ display: flex;
374
+ align-items: center;
375
+ gap: 4px;
376
+ }
377
+
378
+ .action-btn {
379
+ display: flex;
380
+ align-items: center;
381
+ justify-content: center;
382
+ width: 24px;
383
+ height: 24px;
384
+ border: none;
385
+ background: transparent;
386
+ border-radius: 4px;
387
+ color: var(--chat-text-muted, #666);
388
+ cursor: pointer;
389
+ transition: all 0.15s;
390
+ }
391
+
392
+ .action-btn:hover {
393
+ background: var(--chat-muted, #3c3c3c);
394
+ color: var(--chat-text, #ccc);
395
+ }
396
+
397
+ /* 消息时间 */
398
+ .message-time {
399
+ font-size: 12px;
400
+ color: var(--chat-text-muted, #666);
401
+ }
402
+
403
+ .user-time {
404
+ text-align: right;
405
+ margin-top: 8px;
406
+ }
407
+
408
+ .assistant-time {
409
+ margin-right: 8px;
410
+ }
411
+ </style>
@@ -0,0 +1,101 @@
1
+ <template>
2
+ <div class="parts-renderer">
3
+ <template v-for="(part, index) in parts" :key="index">
4
+ <!-- 文本 -->
5
+ <TextPart
6
+ v-if="part.type === 'text'"
7
+ :text="part.text"
8
+ />
9
+
10
+ <!-- 思考 -->
11
+ <ThinkingPart
12
+ v-else-if="part.type === 'thinking'"
13
+ :text="part.text"
14
+ :status="part.status"
15
+ :duration="part.duration"
16
+ :expanded-type="expandedType"
17
+ />
18
+
19
+ <!-- 搜索 -->
20
+ <SearchPart
21
+ v-else-if="part.type === 'search'"
22
+ :query="part.query"
23
+ :results="part.results"
24
+ :status="part.status"
25
+ :expanded-type="expandedType"
26
+ />
27
+
28
+ <!-- 工具调用(包含执行结果) -->
29
+ <ToolCallPart
30
+ v-else-if="part.type === 'tool_call'"
31
+ :id="part.id"
32
+ :name="part.name"
33
+ :args="part.args"
34
+ :status="part.status"
35
+ :result="part.result"
36
+ :expanded-type="expandedType"
37
+ :adapter="adapter"
38
+ :auto-run-config="autoRunConfig"
39
+ :on-save-config="onSaveConfig"
40
+ />
41
+
42
+ <!-- 工具结果(仅在没有对应 tool_call 时显示) -->
43
+ <ToolResultPart
44
+ v-else-if="part.type === 'tool_result'"
45
+ :id="part.id"
46
+ :name="part.name"
47
+ :args="part.args"
48
+ :result="part.result"
49
+ :status="part.status"
50
+ :expanded-type="expandedType"
51
+ />
52
+
53
+ <!-- 图片 -->
54
+ <ImagePart
55
+ v-else-if="part.type === 'image'"
56
+ :url="part.url"
57
+ />
58
+
59
+ <!-- 错误 -->
60
+ <ErrorPart
61
+ v-else-if="part.type === 'error'"
62
+ :message="part.message"
63
+ :category="part.category"
64
+ :retryable="part.retryable"
65
+ />
66
+ </template>
67
+ </div>
68
+ </template>
69
+
70
+ <script setup lang="ts">
71
+ import type { ContentPart, StepsExpandedType } from '../../types'
72
+ import type { ChatAdapter } from '../../adapter'
73
+ import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer'
74
+ import {
75
+ TextPart,
76
+ ThinkingPart,
77
+ SearchPart,
78
+ ToolCallPart,
79
+ ToolResultPart,
80
+ ImagePart,
81
+ ErrorPart
82
+ } from './parts'
83
+
84
+ withDefaults(defineProps<{
85
+ parts: ContentPart[]
86
+ expandedType?: StepsExpandedType
87
+ // 工具调用相关
88
+ adapter?: ChatAdapter
89
+ autoRunConfig?: AutoRunConfig
90
+ onSaveConfig?: (config: AutoRunConfig) => Promise<void>
91
+ }>(), {
92
+ expandedType: 'auto'
93
+ })
94
+ </script>
95
+
96
+ <style scoped>
97
+ .parts-renderer {
98
+ display: flex;
99
+ flex-direction: column;
100
+ }
101
+ </style>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <component :is="resolvedComponent" v-bind="rendererProps" />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { computed, inject, type Component } from 'vue'
7
+ import type { ToolRendererProps } from '@huyooo/ai-chat-shared'
8
+ import { DefaultToolResult } from './tool-results'
9
+
10
+ const props = defineProps<ToolRendererProps>()
11
+
12
+ // 从上层注入的自定义工具渲染器
13
+ const customRenderers = inject<Record<string, Component>>('toolRenderers', {})
14
+
15
+ // 解析使用哪个组件
16
+ const resolvedComponent = computed(() =>
17
+ customRenderers[props.toolName] || DefaultToolResult
18
+ )
19
+
20
+ // 传递给渲染器的 props
21
+ const rendererProps = computed<ToolRendererProps>(() => ({
22
+ toolName: props.toolName,
23
+ toolArgs: props.toolArgs,
24
+ toolResult: props.toolResult,
25
+ status: props.status,
26
+ }))
27
+ </script>