@huyooo/ai-chat-frontend-vue 0.2.13 → 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 (142) hide show
  1. package/README.md +85 -77
  2. package/dist/_baseUniq-Berj5otS.js +477 -0
  3. package/dist/arc-D4b-De5i.js +83 -0
  4. package/dist/architecture-U656AL7Q-B1R4o7wt.js +5 -0
  5. package/dist/architectureDiagram-VXUJARFQ-0H1LzSrf.js +4668 -0
  6. package/dist/blockDiagram-VD42YOAC-Dd6DMjLf.js +2265 -0
  7. package/dist/c4Diagram-YG6GDRKO-c6mpIonb.js +1581 -0
  8. package/dist/channel-CQSBI1fo.js +5 -0
  9. package/dist/chunk-4BX2VUAB-CfWC7lTG.js +9 -0
  10. package/dist/chunk-55IACEB6-joM8m2Zv.js +8 -0
  11. package/dist/chunk-B4BG7PRW-DN94Y3mS.js +1376 -0
  12. package/dist/chunk-DI55MBZ5-CPRGoFPG.js +1382 -0
  13. package/dist/chunk-FMBD7UC4-dUKl2TcN.js +19 -0
  14. package/dist/chunk-QN33PNHL-o3WmqquO.js +20 -0
  15. package/dist/chunk-QZHKN3VN-Btf62lRF.js +15 -0
  16. package/dist/chunk-TZMSLE5B-QCPpnm8Y.js +64 -0
  17. package/dist/classDiagram-2ON5EDUG-ERF9RE9K.js +16 -0
  18. package/dist/classDiagram-v2-WZHVMYZB-ERF9RE9K.js +16 -0
  19. package/dist/clone-B_B1yyxj.js +8 -0
  20. package/dist/components/ChatPanel.vue.d.ts +11 -7
  21. package/dist/components/ChatPanel.vue.d.ts.map +1 -1
  22. package/dist/components/common/SettingsPanel.vue.d.ts +10 -1
  23. package/dist/components/common/SettingsPanel.vue.d.ts.map +1 -1
  24. package/dist/components/common/ToggleSwitch.vue.d.ts.map +1 -1
  25. package/dist/components/input/ChatInput.vue.d.ts +4 -1
  26. package/dist/components/input/ChatInput.vue.d.ts.map +1 -1
  27. package/dist/components/input/DropdownSelector.vue.d.ts +10 -0
  28. package/dist/components/input/DropdownSelector.vue.d.ts.map +1 -1
  29. package/dist/components/message/MessageBubble.vue.d.ts +2 -2
  30. package/dist/components/message/MessageBubble.vue.d.ts.map +1 -1
  31. package/dist/components/message/PartsRenderer.vue.d.ts +6 -0
  32. package/dist/components/message/PartsRenderer.vue.d.ts.map +1 -1
  33. package/dist/components/message/parts/CollapsibleCard.vue.d.ts.map +1 -1
  34. package/dist/components/message/parts/ImagePart.vue.d.ts.map +1 -1
  35. package/dist/components/message/parts/SearchPart.vue.d.ts.map +1 -1
  36. package/dist/components/message/parts/TextPart.vue.d.ts +3 -1
  37. package/dist/components/message/parts/TextPart.vue.d.ts.map +1 -1
  38. package/dist/components/message/parts/ToolCallPart.vue.d.ts +9 -4
  39. package/dist/components/message/parts/ToolCallPart.vue.d.ts.map +1 -1
  40. package/dist/components/message/parts/index.d.ts +5 -1
  41. package/dist/components/message/parts/index.d.ts.map +1 -1
  42. package/dist/components/message/parts/visual-predicate.d.ts +7 -0
  43. package/dist/components/message/parts/visual-predicate.d.ts.map +1 -0
  44. package/dist/components/message/parts/visual-render.d.ts +2 -0
  45. package/dist/components/message/parts/visual-render.d.ts.map +1 -0
  46. package/dist/components/message/parts/visual.d.ts +3 -0
  47. package/dist/components/message/parts/visual.d.ts.map +1 -0
  48. package/dist/composables/useChat.d.ts +34 -10
  49. package/dist/composables/useChat.d.ts.map +1 -1
  50. package/dist/composables/useImageUpload.d.ts +7 -0
  51. package/dist/composables/useImageUpload.d.ts.map +1 -1
  52. package/dist/cose-bilkent-S5V4N54A-C-CkEnIH.js +2608 -0
  53. package/dist/cytoscape.esm-CjI2IsL8.js +18735 -0
  54. package/dist/dagre-6UL2VRFP-BbmZysl9.js +446 -0
  55. package/dist/defaultLocale-D7EN2tov.js +171 -0
  56. package/dist/diagram-PSM6KHXK-CMnope5e.js +533 -0
  57. package/dist/diagram-QEK2KX5R-Bc87skpo.js +221 -0
  58. package/dist/diagram-S2PKOQOG-BtkzC5-o.js +143 -0
  59. package/dist/erDiagram-Q2GNP2WA-CwQLdjh9.js +842 -0
  60. package/dist/flowDiagram-NV44I4VS-8ehMD0M4.js +1627 -0
  61. package/dist/ganttDiagram-JELNMOA3-dLX8Pbvy.js +2670 -0
  62. package/dist/gitGraph-F6HP7TQM-BBk5qfnx.js +5 -0
  63. package/dist/gitGraphDiagram-NY62KEGX-DG_fvWrq.js +712 -0
  64. package/dist/graph-B7BnGhuR.js +381 -0
  65. package/dist/index-Cr3tTH72.js +83883 -0
  66. package/dist/index.d.ts +7 -7
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +19 -56433
  69. package/dist/info-NVLQJR56-eZr6nVr1.js +5 -0
  70. package/dist/infoDiagram-WHAUD3N6-BcVYTft_.js +24 -0
  71. package/dist/init-DjUOC4st.js +16 -0
  72. package/dist/journeyDiagram-XKPGCS4Q-D4C72gYB.js +834 -0
  73. package/dist/kanban-definition-3W4ZIXB7-BuSjAlm6.js +721 -0
  74. package/dist/layout-LjYblgUu.js +1441 -0
  75. package/dist/linear-DtPqj295.js +259 -0
  76. package/dist/mermaid-parser.core-DdOZxjrw.js +15189 -0
  77. package/dist/min-DYbgfxu0.js +38 -0
  78. package/dist/mindmap-definition-VGOIOE7T-BZxsr4M_.js +787 -0
  79. package/dist/ordinal-DfAQgscy.js +61 -0
  80. package/dist/packet-BFZMPI3H-XfNBjRzf.js +5 -0
  81. package/dist/pie-7BOR55EZ-pJDdOv19.js +5 -0
  82. package/dist/pieDiagram-ADFJNKIX-DDwfWBNn.js +161 -0
  83. package/dist/quadrantDiagram-AYHSOK5B-Ddpxuto0.js +1024 -0
  84. package/dist/radar-NHE76QYJ-jE07d-4Z.js +5 -0
  85. package/dist/requirementDiagram-UZGBJVZJ-B_TLWQ-N.js +852 -0
  86. package/dist/sankeyDiagram-TZEHDZUN-C1TWrUpG.js +810 -0
  87. package/dist/sequenceDiagram-WL72ISMW-B8pqb5YX.js +2518 -0
  88. package/dist/stateDiagram-FKZM4ZOC-DwNT1ej6.js +263 -0
  89. package/dist/stateDiagram-v2-4FDKWEC3-CZQUitzd.js +16 -0
  90. package/dist/style.css +1 -1
  91. package/dist/timeline-definition-IT6M3QCI-BGdS1wvf.js +799 -0
  92. package/dist/treemap-KMMF4GRG-B_eEU0Q4.js +5 -0
  93. package/dist/types/index.d.ts +36 -37
  94. package/dist/types/index.d.ts.map +1 -1
  95. package/dist/xychartDiagram-PRI3JC2R-DtwXQqN4.js +1340 -0
  96. package/package.json +3 -3
  97. package/src/components/ChatPanel.vue +75 -13
  98. package/src/components/common/SettingsPanel.vue +169 -0
  99. package/src/components/input/ChatInput.vue +49 -34
  100. package/src/components/input/DropdownSelector.vue +175 -18
  101. package/src/components/message/MessageBubble.vue +61 -40
  102. package/src/components/message/PartsRenderer.vue +106 -27
  103. package/src/components/message/parts/CollapsibleCard.vue +8 -3
  104. package/src/components/message/parts/ImagePart.vue +0 -1
  105. package/src/components/message/parts/SearchPart.vue +2 -0
  106. package/src/components/message/parts/TextPart.vue +753 -11
  107. package/src/components/message/parts/ToolCallPart.vue +315 -246
  108. package/src/components/message/parts/index.ts +5 -1
  109. package/src/components/message/parts/visual-predicate.ts +43 -0
  110. package/src/components/message/parts/visual-render.ts +19 -0
  111. package/src/components/message/parts/visual.ts +12 -0
  112. package/src/composables/useChat.ts +560 -92
  113. package/src/composables/useImageUpload.ts +115 -11
  114. package/src/index.ts +6 -8
  115. package/src/styles.css +124 -0
  116. package/src/types/index.ts +52 -44
  117. package/dist/components/message/blocks/CodeBlock.vue.d.ts +0 -11
  118. package/dist/components/message/blocks/CodeBlock.vue.d.ts.map +0 -1
  119. package/dist/components/message/blocks/TextBlock.vue.d.ts +0 -7
  120. package/dist/components/message/blocks/TextBlock.vue.d.ts.map +0 -1
  121. package/dist/components/message/blocks/index.d.ts +0 -6
  122. package/dist/components/message/blocks/index.d.ts.map +0 -1
  123. package/dist/components/message/parts/ToolResultPart.vue.d.ts +0 -14
  124. package/dist/components/message/parts/ToolResultPart.vue.d.ts.map +0 -1
  125. package/dist/components/message/tool-results/DefaultToolResult.vue.d.ts +0 -4
  126. package/dist/components/message/tool-results/DefaultToolResult.vue.d.ts.map +0 -1
  127. package/dist/components/message/tool-results/SearchResults.vue.d.ts +0 -4
  128. package/dist/components/message/tool-results/SearchResults.vue.d.ts.map +0 -1
  129. package/dist/components/message/tool-results/WeatherCard.vue.d.ts +0 -4
  130. package/dist/components/message/tool-results/WeatherCard.vue.d.ts.map +0 -1
  131. package/dist/components/message/tool-results/index.d.ts +0 -7
  132. package/dist/components/message/tool-results/index.d.ts.map +0 -1
  133. package/src/components/message/ContentRenderer.vue +0 -61
  134. package/src/components/message/ToolResultRenderer.vue +0 -27
  135. package/src/components/message/blocks/CodeBlock.vue +0 -113
  136. package/src/components/message/blocks/TextBlock.vue +0 -21
  137. package/src/components/message/blocks/index.ts +0 -6
  138. package/src/components/message/parts/ToolResultPart.vue +0 -78
  139. package/src/components/message/tool-results/DefaultToolResult.vue +0 -45
  140. package/src/components/message/tool-results/SearchResults.vue +0 -133
  141. package/src/components/message/tool-results/WeatherCard.vue +0 -139
  142. package/src/components/message/tool-results/index.ts +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huyooo/ai-chat-frontend-vue",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "AI Chat Frontend - Vue components with adapter pattern",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,8 +31,8 @@
31
31
  "vue": "^3.4.0"
32
32
  },
33
33
  "dependencies": {
34
- "@huyooo/ai-chat-bridge-electron": "^0.2.13",
35
- "@huyooo/ai-chat-shared": "^0.2.13",
34
+ "@huyooo/ai-chat-bridge-electron": "^0.2.14",
35
+ "@huyooo/ai-chat-shared": "^0.2.14",
36
36
  "@iconify/vue": "^4.1.2",
37
37
  "vscode-uri": "^3.1.0",
38
38
  "marked": "^12.0.0",
@@ -19,7 +19,10 @@
19
19
  <SettingsPanel
20
20
  v-model:visible="settingsPanelVisible"
21
21
  v-model:config="autoRunConfig"
22
+ :all-tools="allTools"
23
+ :enabled-tools="enabledTools"
22
24
  @save="handleSaveSettings"
25
+ @update:enabled-tools="handleUpdateEnabledTools"
23
26
  />
24
27
 
25
28
  <!-- 顶部标题栏 -->
@@ -28,7 +31,7 @@
28
31
  :sessions="sessions"
29
32
  :current-session-id="currentSessionId"
30
33
  :show-close="!!onClose"
31
- @new-session="createNewSession"
34
+ @new-session="handleNewSession"
32
35
  @switch-session="switchSession"
33
36
  @delete-session="deleteSession"
34
37
  @hide-session="hideSession"
@@ -61,6 +64,7 @@
61
64
  :adapter="props.adapter"
62
65
  :auto-run-config="autoRunConfig"
63
66
  :on-save-config="saveAutoRunConfig"
67
+ :on-cancel-tool-call="handleCancelToolCall"
64
68
  @copy="copyMessage(msg.id)"
65
69
  @regenerate="regenerateMessage(index)"
66
70
  @send="(text) => handleResend(index, text)"
@@ -105,7 +109,7 @@
105
109
  <script setup lang="ts">
106
110
  import { ref, computed, watch, nextTick, onMounted, provide, toRef, type Component } from 'vue';
107
111
  import { useChat } from '../composables/useChat';
108
- import type { ChatAdapter } from '../adapter';
112
+ import type { ChatAdapter, ImageData } from '../adapter';
109
113
  import type { ModelOption, ChatMode } from '../types';
110
114
  import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer';
111
115
  import { Icon } from '@iconify/vue';
@@ -137,8 +141,8 @@ interface Props {
137
141
  className?: string;
138
142
  /** 欢迎页配置 */
139
143
  welcomeConfig?: Partial<WelcomeConfig>;
140
- /** 自定义工具结果渲染器 - 根据工具名称选择渲染组件 */
141
- toolRenderers?: Record<string, Component>;
144
+ /** 自定义 Part 渲染器 - 根据 part.type 选择渲染组件(如 weather, stock 等) */
145
+ partRenderers?: Record<string, Component>;
142
146
  /**
143
147
  * 执行步骤折叠模式
144
148
  * - 'open': 始终展开
@@ -156,7 +160,7 @@ const props = withDefaults(defineProps<Props>(), {
156
160
  onClose: undefined,
157
161
  className: '',
158
162
  welcomeConfig: undefined,
159
- toolRenderers: () => ({}),
163
+ partRenderers: () => ({}),
160
164
  stepsExpandedType: 'auto',
161
165
  });
162
166
 
@@ -184,6 +188,9 @@ const {
184
188
  model,
185
189
  webSearch,
186
190
  thinking,
191
+ // 工具管理
192
+ allTools,
193
+ enabledTools,
187
194
  loadSessions,
188
195
  switchSession,
189
196
  createNewSession,
@@ -204,15 +211,24 @@ const {
204
211
  setWorkingDirectory,
205
212
  autoRunConfig,
206
213
  saveAutoRunConfig,
214
+ saveEnabledTools,
207
215
  } = chat;
208
216
 
217
+ function handleCancelToolCall() {
218
+ cancelRequest()
219
+ }
220
+
209
221
  // 消息容器引用
210
222
  const messagesRef = ref<HTMLDivElement | null>(null);
211
223
 
212
224
  // 是否应该自动滚动(用户在底部附近时才自动滚动)
213
225
  const shouldAutoScroll = ref(true);
214
226
  // 距离底部多少像素内算"在底部"
215
- const SCROLL_THRESHOLD = 100;
227
+ const SCROLL_THRESHOLD = 25;
228
+ // 上次滚动位置(用于检测滚动方向)
229
+ let lastScrollTop = 0;
230
+ // 是否正在程序化滚动(用于区分用户手动滚动)
231
+ let isProgrammaticScroll = false;
216
232
 
217
233
  // ChatInput 引用
218
234
  const chatInputRef = ref<InstanceType<typeof ChatInput> | null>(null);
@@ -291,8 +307,8 @@ provide('chatInputState', {
291
307
  setThinking,
292
308
  });
293
309
 
294
- // Provide 自定义工具渲染器(用于 ToolResultPart 组件)
295
- provide('toolRenderers', props.toolRenderers);
310
+ // Provide 自定义 Part 渲染器(用于 PartsRenderer)
311
+ provide('partRenderers', props.partRenderers);
296
312
 
297
313
  // 初始化
298
314
  onMounted(() => {
@@ -308,17 +324,45 @@ function isNearBottom(): boolean {
308
324
  return scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD;
309
325
  }
310
326
 
311
- // 处理滚动事件
327
+ // 处理滚动事件(优化:检测滚动方向,区分程序化滚动)
312
328
  function handleScroll() {
313
- shouldAutoScroll.value = isNearBottom();
329
+ if (!messagesRef.value) return;
330
+
331
+ // 忽略程序化滚动触发的事件
332
+ if (isProgrammaticScroll) return;
333
+
334
+ const { scrollTop } = messagesRef.value;
335
+ const isScrollingUp = scrollTop < lastScrollTop;
336
+ lastScrollTop = scrollTop;
337
+
338
+ // 用户向上滚动时,立即停止自动滚动
339
+ if (isScrollingUp) {
340
+ shouldAutoScroll.value = false;
341
+ return;
342
+ }
343
+
344
+ // 用户向下滚动到底部附近时,恢复自动滚动
345
+ if (isNearBottom()) {
346
+ shouldAutoScroll.value = true;
347
+ }
314
348
  }
315
349
 
316
350
  // 滚动到底部
317
351
  async function scrollToBottom(force = false) {
318
352
  await nextTick();
319
353
  if (messagesRef.value && (force || shouldAutoScroll.value)) {
354
+ // 标记为程序化滚动,避免触发 handleScroll 逻辑
355
+ isProgrammaticScroll = true;
320
356
  messagesRef.value.scrollTop = messagesRef.value.scrollHeight;
321
- shouldAutoScroll.value = true;
357
+ lastScrollTop = messagesRef.value.scrollTop;
358
+ // 强制滚动时才恢复自动滚动
359
+ if (force) {
360
+ shouldAutoScroll.value = true;
361
+ }
362
+ // 延迟重置标志,确保 scroll 事件已处理
363
+ requestAnimationFrame(() => {
364
+ isProgrammaticScroll = false;
365
+ });
322
366
  }
323
367
  }
324
368
 
@@ -340,8 +384,10 @@ watch(isLoading, (loading, prevLoading) => {
340
384
  });
341
385
 
342
386
  // 发送消息
343
- function handleSend(text: string) {
344
- sendMessage(text);
387
+ function handleSend(text: string, images?: ImageData[]) {
388
+ // 将 ImageData[] 转换为 string[] (data URL)
389
+ const imageUrls = images?.map(img => `data:${img.mimeType};base64,${img.base64}`)
390
+ sendMessage(text, imageUrls);
345
391
  }
346
392
 
347
393
  // 快捷操作
@@ -360,6 +406,15 @@ function handleAtContext() {
360
406
  console.log('@ 上下文');
361
407
  }
362
408
 
409
+ // 创建新会话(重置输入框状态)
410
+ async function handleNewSession() {
411
+ await createNewSession();
412
+ // 重置输入框状态
413
+ nextTick(() => {
414
+ chatInputRef.value?.clear();
415
+ });
416
+ }
417
+
363
418
  // 关闭
364
419
  function handleClose() {
365
420
  if (props.onClose) {
@@ -438,6 +493,10 @@ async function handleSaveSettings(config: AutoRunConfig) {
438
493
  }
439
494
  }
440
495
 
496
+ function handleUpdateEnabledTools(tools: string[] | undefined) {
497
+ saveEnabledTools(tools);
498
+ }
499
+
441
500
  // 模式变更现在在 ToolCallPart 中处理,不再需要此函数
442
501
 
443
502
  // 暴露给外部的方法
@@ -487,6 +546,9 @@ defineExpose({
487
546
  overflow-y: auto;
488
547
  padding: 12px;
489
548
  scroll-behavior: smooth;
549
+ display: flex;
550
+ flex-direction: column;
551
+ gap: 8px;
490
552
  }
491
553
 
492
554
  /* 滚动到底部按钮 */
@@ -45,6 +45,47 @@
45
45
  />
46
46
  </div>
47
47
  </div>
48
+
49
+ <!-- 工具管理 -->
50
+ <div class="setting-section">
51
+ <div class="setting-section-header">
52
+ <div class="setting-info">
53
+ <div class="setting-label">工具管理</div>
54
+ <div class="setting-description">控制哪些工具可以被 AI 使用</div>
55
+ </div>
56
+ <button
57
+ class="disable-all-btn"
58
+ @click="isAllToolsDisabled ? handleEnableAllTools() : handleDisableAllTools()"
59
+ :disabled="!allTools || allTools.length === 0"
60
+ >
61
+ {{ isAllToolsDisabled ? '启用所有工具' : '禁用所有工具' }}
62
+ </button>
63
+ </div>
64
+ <div class="tools-list">
65
+ <template v-if="allTools && allTools.length > 0">
66
+ <div
67
+ v-for="tool in allTools"
68
+ :key="tool.name"
69
+ class="tool-item"
70
+ >
71
+ <div class="tool-info">
72
+ <div class="tool-name">{{ tool.name }}</div>
73
+ <div class="tool-description">{{ tool.description }}</div>
74
+ </div>
75
+ <ToggleSwitch
76
+ :checked="isToolEnabled(tool.name)"
77
+ @change="(checked) => handleToolToggle(tool.name, checked)"
78
+ />
79
+ </div>
80
+ </template>
81
+ <div v-else class="no-tools">
82
+ <div>暂无可用工具</div>
83
+ <div style="font-size: 12px; color: var(--chat-text-muted, #888); margin-top: 8px;">
84
+ 工具需要在创建 Electron Bridge 时通过 <code>tools</code> 参数注入
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>
48
89
  </template>
49
90
 
50
91
  <!-- 索引与文档设置 -->
@@ -69,8 +110,15 @@ import IndexingSettings from './IndexingSettings.vue'
69
110
  const visible = defineModel<boolean>('visible', { required: true })
70
111
  const config = defineModel<AutoRunConfig>('config', { required: true })
71
112
 
113
+ // Props:工具相关
114
+ const props = defineProps<{
115
+ allTools?: Array<{ name: string; description: string }>
116
+ enabledTools?: string[] | undefined
117
+ }>()
118
+
72
119
  const emit = defineEmits<{
73
120
  save: [config: AutoRunConfig]
121
+ 'update:enabledTools': [tools: string[] | undefined]
74
122
  }>()
75
123
 
76
124
  // 设置分类
@@ -102,6 +150,52 @@ function updateConfig<K extends keyof AutoRunConfig>(key: K, value: AutoRunConfi
102
150
  function handleModeChange(value: string) {
103
151
  updateConfig('mode', value as AutoRunMode)
104
152
  }
153
+
154
+ /** 判断工具是否启用 */
155
+ function isToolEnabled(toolName: string): boolean {
156
+ // undefined 表示全部启用
157
+ if (props.enabledTools === undefined) return true
158
+ return props.enabledTools.includes(toolName)
159
+ }
160
+
161
+ /** 处理工具开关切换 */
162
+ function handleToolToggle(toolName: string, checked: boolean) {
163
+ const allToolNames = (props.allTools ?? []).map((t) => t.name)
164
+ let newEnabledTools: string[] | undefined
165
+
166
+ // undefined 表示“全部启用”,因此第一次“禁用某个工具”需要从全量集合扣掉该工具
167
+ if (props.enabledTools === undefined) {
168
+ if (checked) return
169
+ newEnabledTools = allToolNames.filter((n) => n !== toolName)
170
+ emit('update:enabledTools', newEnabledTools)
171
+ return
172
+ }
173
+
174
+ const set = new Set(props.enabledTools)
175
+ if (checked) set.add(toolName)
176
+ else set.delete(toolName)
177
+
178
+ // 如果所有工具都启用,回到 undefined(更符合默认语义)
179
+ if (allToolNames.length > 0 && set.size === allToolNames.length) {
180
+ newEnabledTools = undefined
181
+ } else {
182
+ newEnabledTools = Array.from(set)
183
+ }
184
+
185
+ emit('update:enabledTools', newEnabledTools)
186
+ }
187
+
188
+ /** 一键禁用所有工具 */
189
+ function handleDisableAllTools() {
190
+ emit('update:enabledTools', [])
191
+ }
192
+
193
+ /** 一键启用所有工具(恢复默认:undefined 表示全部启用) */
194
+ function handleEnableAllTools() {
195
+ emit('update:enabledTools', undefined)
196
+ }
197
+
198
+ const isAllToolsDisabled = computed(() => props.enabledTools !== undefined && props.enabledTools.length === 0)
105
199
  </script>
106
200
 
107
201
  <style scoped>
@@ -290,4 +384,79 @@ function handleModeChange(value: string) {
290
384
  .setting-control {
291
385
  flex-shrink: 0;
292
386
  }
387
+
388
+ .setting-section {
389
+ margin-top: 24px;
390
+ padding-top: 24px;
391
+ border-top: 1px solid var(--chat-border, #333);
392
+ }
393
+
394
+ .setting-section-header {
395
+ display: flex;
396
+ align-items: center;
397
+ justify-content: space-between;
398
+ margin-bottom: 16px;
399
+ }
400
+
401
+ .disable-all-btn {
402
+ padding: 6px 12px;
403
+ background: transparent;
404
+ border: 1px solid var(--chat-border, #333);
405
+ border-radius: 6px;
406
+ color: var(--chat-text, #fff);
407
+ font-size: 13px;
408
+ cursor: pointer;
409
+ transition: all 0.15s;
410
+ }
411
+
412
+ .disable-all-btn:hover:not(:disabled) {
413
+ background: var(--chat-muted, #2a2a2a);
414
+ border-color: var(--chat-text-muted, #888);
415
+ }
416
+
417
+ .disable-all-btn:disabled {
418
+ opacity: 0.5;
419
+ cursor: not-allowed;
420
+ }
421
+
422
+ .tools-list {
423
+ display: flex;
424
+ flex-direction: column;
425
+ gap: 12px;
426
+ }
427
+
428
+ .tool-item {
429
+ display: flex;
430
+ align-items: center;
431
+ justify-content: space-between;
432
+ padding: 12px;
433
+ background: var(--chat-muted, #2a2a2a);
434
+ border: 1px solid var(--chat-border, #333);
435
+ border-radius: 8px;
436
+ }
437
+
438
+ .tool-info {
439
+ flex: 1;
440
+ margin-right: 16px;
441
+ }
442
+
443
+ .tool-name {
444
+ font-size: 14px;
445
+ font-weight: 500;
446
+ color: var(--chat-text, #fff);
447
+ margin-bottom: 4px;
448
+ }
449
+
450
+ .tool-description {
451
+ font-size: 12px;
452
+ color: var(--chat-text-muted, #888);
453
+ line-height: 1.4;
454
+ }
455
+
456
+ .no-tools {
457
+ padding: 24px;
458
+ text-align: center;
459
+ color: var(--chat-text-muted, #888);
460
+ font-size: 14px;
461
+ }
293
462
  </style>
@@ -43,6 +43,7 @@
43
43
  :placeholder="placeholder"
44
44
  rows="1"
45
45
  class="input-field chat-scrollbar"
46
+ :readonly="voiceIsRecording"
46
47
  spellcheck="false"
47
48
  autocorrect="off"
48
49
  autocomplete="off"
@@ -69,7 +70,7 @@
69
70
  <!-- 模型选择(使用后端返回的分组数据) -->
70
71
  <DropdownSelector
71
72
  :value="model"
72
- :grouped-options="groupedModelOptions"
73
+ :options="modelOptions"
73
74
  :container-ref="containerRef"
74
75
  @select="selectModel"
75
76
  />
@@ -77,8 +78,9 @@
77
78
 
78
79
  <!-- 右侧:功能按钮 -->
79
80
  <div class="input-right">
80
- <!-- 深度思考 -->
81
+ <!-- 深度思考(ask 模式隐藏,仅支持的模型显示) -->
81
82
  <button
83
+ v-if="mode !== 'ask' && currentModelSupportsThinking"
82
84
  :class="['toggle-btn', { active: thinkingEnabled }]"
83
85
  title="深度思考"
84
86
  @click="$emit('update:thinking', !thinkingEnabled)"
@@ -86,8 +88,9 @@
86
88
  <Icon icon="lucide:sparkles" width="18" />
87
89
  </button>
88
90
 
89
- <!-- 联网搜索 -->
91
+ <!-- 联网搜索(ask 模式隐藏) -->
90
92
  <button
93
+ v-if="mode !== 'ask'"
91
94
  :class="['toggle-btn', { active: webSearchEnabled }]"
92
95
  title="联网搜索"
93
96
  @click="$emit('update:webSearch', !webSearchEnabled)"
@@ -131,7 +134,7 @@
131
134
  :class="{ connecting: voiceInput.status.value === 'connecting', recording: voiceIsRecording }"
132
135
  :title="voiceInput.status.value === 'connecting' ? '正在连接,点击取消' : voiceIsRecording ? '点击停止' : '点击录音'"
133
136
  @click.prevent="toggleVoiceInput"
134
- :disabled="isLoading"
137
+ :disabled="isLoading || !props.adapter"
135
138
  >
136
139
  <Icon v-if="voiceInput.status.value === 'connecting'" icon="lucide:loader-2" width="18" class="spin" />
137
140
  <Icon v-else-if="voiceIsRecording" icon="streamline-flex:button-pause-circle-remix" width="18" />
@@ -141,7 +144,7 @@
141
144
  <button
142
145
  class="send-btn"
143
146
  :class="{ loading: isLoading }"
144
- :disabled="(!inputText.trim() && !imageUpload.hasImages.value) || voiceIsRecording || voiceInput.status.value === 'connecting'"
147
+ :disabled="(!inputText.trim() && !imageUpload.hasImages.value && !isLoading) || voiceIsRecording || voiceInput.status.value === 'connecting'"
145
148
  :title="
146
149
  isLoading
147
150
  ? '停止'
@@ -156,7 +159,7 @@
156
159
  @click="handleSendOrCancel"
157
160
  >
158
161
  <Icon v-if="isLoading" icon="streamline-flex:button-pause-circle-remix" width="18" />
159
- <Icon v-else icon="streamline-flex:mail-send-email-message-circle-remix" width="18" />
162
+ <Icon v-else icon="uil:message" width="18" />
160
163
  </button>
161
164
  </div>
162
165
  </div>
@@ -171,7 +174,7 @@ import { Icon } from '@iconify/vue';
171
174
  import type { ChatMode, ModelOption } from '../../types';
172
175
  import AtFilePicker from './AtFilePicker.vue';
173
176
  import ImagePreviewModal from './ImagePreviewModal.vue';
174
- import DropdownSelector, { type GroupedOptions } from './DropdownSelector.vue';
177
+ import DropdownSelector, { type GroupedOptions, type DropdownOption } from './DropdownSelector.vue';
175
178
  import type { ChatAdapter, ImageData } from '../../adapter';
176
179
  import { useImageUpload } from '../../composables/useImageUpload';
177
180
  import { useVoiceToTextInput } from '../../composables/useVoiceToTextInput';
@@ -182,12 +185,14 @@ const props = withDefaults(
182
185
  variant?: 'input' | 'message';
183
186
  /** 受控值(用于历史消息编辑) */
184
187
  value?: string;
188
+ /** 图片数据(用于历史消息编辑) */
189
+ images?: string[];
185
190
  isLoading?: boolean;
186
191
  /** 由宿主显式传入(@ 面板需要) */
187
192
  adapter?: ChatAdapter;
188
193
  mode?: ChatMode;
189
194
  model?: string;
190
- /** 模型列表 */
195
+ /** 模型列表(tooltip 由后端下发,前端仅透传渲染) */
191
196
  models?: ModelOption[];
192
197
  webSearchEnabled?: boolean;
193
198
  thinkingEnabled?: boolean;
@@ -195,6 +200,7 @@ const props = withDefaults(
195
200
  {
196
201
  variant: 'input',
197
202
  value: '',
203
+ images: () => [],
198
204
  isLoading: false,
199
205
  adapter: undefined,
200
206
  mode: 'agent',
@@ -326,37 +332,39 @@ watch(
326
332
  }
327
333
  );
328
334
 
335
+ // 同步外部 images(用于历史消息编辑)
336
+ watch(
337
+ () => props.images,
338
+ (newImages) => {
339
+ if (newImages.length > 0) {
340
+ imageUpload.initImages(newImages);
341
+ } else {
342
+ imageUpload.clearImages();
343
+ }
344
+ },
345
+ { immediate: true }
346
+ );
347
+
329
348
  // 模式选项(用于 DropdownSelector)
330
349
  const modeOptions = computed(() => [
331
350
  { value: 'agent', label: 'Agent', icon: 'lucide:zap' },
332
351
  { value: 'ask', label: 'Ask', icon: 'lucide:message-circle' },
333
352
  ]);
334
353
 
335
- // 模型选项(用于 DropdownSelector,支持分组显示和搜索)
336
- // 将模型列表转换为分组格式(完全使用后端返回的 group 字段)
337
- const groupedModelOptions = computed(() => {
338
- const groups: Record<string, Array<{ value: string; label: string; icon?: string }>> = {};
339
-
340
- props.models.forEach((m) => {
341
- // 完全使用后端返回的 group 字段
342
- const groupName = m.group;
343
-
344
- if (!groups[groupName]) {
345
- groups[groupName] = [];
346
- }
347
-
348
- groups[groupName].push({
349
- value: m.modelId,
350
- label: m.displayName,
351
- });
352
- });
353
-
354
- // 对每个分组内的选项进行排序
355
- Object.keys(groups).forEach(groupName => {
356
- groups[groupName].sort((a, b) => a.label.localeCompare(b.label));
357
- });
358
-
359
- return groups;
354
+ // 模型选项(用于 DropdownSelector
355
+ // 模型选项:tooltip/能力由后端(ai-chat-core)统一提供,前端只负责渲染
356
+ const modelOptions = computed<DropdownOption[]>(() =>
357
+ props.models.map((m) => ({
358
+ value: m.modelId,
359
+ label: m.displayName,
360
+ tooltip: m.tooltip,
361
+ }))
362
+ );
363
+
364
+ // 当前模型是否支持深度思考
365
+ const currentModelSupportsThinking = computed(() => {
366
+ const currentModel = props.models.find(m => m.modelId === props.model);
367
+ return currentModel?.supportsThinking ?? false;
360
368
  });
361
369
 
362
370
  // 选择模式
@@ -494,6 +502,14 @@ defineExpose({
494
502
  clear: () => {
495
503
  inputText.value = '';
496
504
  imageUpload.clearImages();
505
+ // 重置语音输入状态
506
+ if (voiceInput.status.value !== 'idle') {
507
+ voiceInput.cancel();
508
+ }
509
+ // 重置 @ 选择器
510
+ atPickerVisible.value = false;
511
+ replaceRange.value = null;
512
+ // 重置输入框高度
497
513
  if (inputRef.value) {
498
514
  inputRef.value.style.height = 'auto';
499
515
  }
@@ -512,7 +528,6 @@ defineExpose({
512
528
 
513
529
  .chat-input.message-variant {
514
530
  padding: 0;
515
- margin-bottom: 16px;
516
531
  }
517
532
 
518
533
  .input-container {