@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
@@ -16,9 +16,37 @@
16
16
  <template v-else-if="groupedOptions && Object.keys(groupedOptions).length > 0">
17
17
  <template v-for="(groupItems, groupName) in groupedOptions" :key="groupName">
18
18
  <div class="group-title">{{ groupName }}</div>
19
- <button
19
+ <div
20
20
  v-for="opt in groupItems"
21
21
  :key="opt.value"
22
+ class="dropdown-item-wrapper"
23
+ @mouseenter="(e) => handleItemHover(e, opt)"
24
+ @mouseleave="hoveredOption = null"
25
+ >
26
+ <button
27
+ :class="['dropdown-item', { active: value === opt.value }]"
28
+ @click="selectOption(opt.value)"
29
+ >
30
+ <Icon v-if="opt.icon" :icon="opt.icon" width="14" />
31
+ <span class="option-label">{{ opt.label }}</span>
32
+ <span class="option-right">
33
+ <Icon v-if="value === opt.value" icon="lucide:check" width="14" class="check-icon" />
34
+ </span>
35
+ </button>
36
+ </div>
37
+ </template>
38
+ </template>
39
+
40
+ <!-- 扁平列表模式:无分组数据时使用 -->
41
+ <template v-else>
42
+ <div
43
+ v-for="opt in sortedOptions"
44
+ :key="opt.value"
45
+ class="dropdown-item-wrapper"
46
+ @mouseenter="(e) => handleItemHover(e, opt)"
47
+ @mouseleave="hoveredOption = null"
48
+ >
49
+ <button
22
50
  :class="['dropdown-item', { active: value === opt.value }]"
23
51
  @click="selectOption(opt.value)"
24
52
  >
@@ -28,30 +56,46 @@
28
56
  <Icon v-if="value === opt.value" icon="lucide:check" width="14" class="check-icon" />
29
57
  </span>
30
58
  </button>
31
- </template>
59
+ </div>
32
60
  </template>
61
+ </div>
33
62
 
34
- <!-- 扁平列表模式:无分组数据时使用 -->
35
- <template v-else>
36
- <button
37
- v-for="opt in sortedOptions"
38
- :key="opt.value"
39
- :class="['dropdown-item', { active: value === opt.value }]"
40
- @click="selectOption(opt.value)"
41
- >
42
- <Icon v-if="opt.icon" :icon="opt.icon" width="14" />
43
- <span class="option-label">{{ opt.label }}</span>
44
- <span class="option-right">
45
- <Icon v-if="value === opt.value" icon="lucide:check" width="14" class="check-icon" />
46
- </span>
47
- </button>
48
- </template>
63
+ <!-- Hover 提示:不使用 Teleport,放在 dropdown-menu 外侧 + fixed 定位,避免被 overflow 裁剪 -->
64
+ <div
65
+ v-if="hoveredOption?.tooltip"
66
+ ref="tooltipRef"
67
+ class="ai-chat-model-tooltip"
68
+ :style="{
69
+ position: 'fixed',
70
+ top: `${tooltipPosition.top}px`,
71
+ left: `${tooltipPosition.left}px`,
72
+ zIndex: 10000,
73
+ }"
74
+ >
75
+ <div v-if="hoveredOption.tooltip.features && hoveredOption.tooltip.features.length > 0" class="ai-chat-model-tooltip__section">
76
+ <div class="ai-chat-model-tooltip__title">可用功能</div>
77
+ <div class="ai-chat-model-tooltip__features">
78
+ <div v-for="feature in hoveredOption.tooltip.features" :key="feature" class="ai-chat-model-tooltip__feature">
79
+ <Icon icon="lucide:check" width="12" class="ai-chat-model-tooltip__check" />
80
+ <span>{{ feature }}</span>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ <div v-if="hoveredOption.tooltip.cost && hoveredOption.tooltip.cost.length > 0" class="ai-chat-model-tooltip__section">
85
+ <div class="ai-chat-model-tooltip__title">开销</div>
86
+ <div class="ai-chat-model-tooltip__cost">
87
+ <div v-for="(line, i) in hoveredOption.tooltip.cost" :key="i">{{ line }}</div>
88
+ </div>
89
+ </div>
90
+ <div v-if="hoveredOption.tooltip.description" class="ai-chat-model-tooltip__section">
91
+ <div class="ai-chat-model-tooltip__description">{{ hoveredOption.tooltip.description }}</div>
92
+ </div>
49
93
  </div>
50
94
  </div>
51
95
  </template>
52
96
 
53
97
  <script setup lang="ts">
54
- import { ref, computed, onMounted, onUnmounted } from 'vue';
98
+ import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
55
99
  import { Icon } from '@iconify/vue';
56
100
 
57
101
  /** 下拉选项 */
@@ -61,6 +105,15 @@ export interface DropdownOption {
61
105
  icon?: string;
62
106
  /** 分组名称(由后端决定,前端只负责渲染,必填) */
63
107
  group?: string; // 在 DropdownOption 中保持可选,因为不是所有选项都需要分组
108
+ /** 模型特性信息(用于 hover 提示) */
109
+ tooltip?: {
110
+ /** 可用功能列表 */
111
+ features?: string[];
112
+ /** 开销信息(数组,分行显示) */
113
+ cost?: string[];
114
+ /** 其他描述信息 */
115
+ description?: string;
116
+ };
64
117
  }
65
118
 
66
119
  /** 分组后的选项(由后端提供) */
@@ -100,6 +153,9 @@ const emit = defineEmits<{
100
153
  const selectorRef = ref<HTMLDivElement | null>(null);
101
154
  const menuOpen = ref(false);
102
155
  const dropdownDirection = ref<'up' | 'down'>('up');
156
+ const hoveredOption = ref<DropdownOption | null>(null);
157
+ const tooltipRef = ref<HTMLDivElement | null>(null);
158
+ const tooltipPosition = ref({ top: 0, left: 0 });
103
159
 
104
160
  // 检查是否有选项
105
161
  const hasOptions = computed(() => {
@@ -167,6 +223,46 @@ function handleClickOutside(event: MouseEvent) {
167
223
  }
168
224
  }
169
225
 
226
+ /**
227
+ * 处理选项 hover 事件
228
+ */
229
+ function handleItemHover(event: MouseEvent, opt: DropdownOption) {
230
+ hoveredOption.value = opt;
231
+ if (!opt.tooltip) return;
232
+
233
+ nextTick(() => {
234
+ const target = event.currentTarget as HTMLElement;
235
+ if (!target) return;
236
+
237
+ const rect = target.getBoundingClientRect();
238
+ // 提示框显示在右侧,距离 8px
239
+ let top = rect.top;
240
+ let left = rect.right + 8;
241
+
242
+ // 检查是否会超出视口
243
+ if (tooltipRef.value) {
244
+ const tooltipRect = tooltipRef.value.getBoundingClientRect();
245
+
246
+ // 检查右侧溢出,改为显示在左侧
247
+ if (left + tooltipRect.width > window.innerWidth) {
248
+ left = rect.left - tooltipRect.width - 8;
249
+ }
250
+
251
+ // 检查底部溢出,向上调整
252
+ if (top + tooltipRect.height > window.innerHeight) {
253
+ top = window.innerHeight - tooltipRect.height - 8;
254
+ }
255
+
256
+ // 检查顶部溢出
257
+ if (top < 8) {
258
+ top = 8;
259
+ }
260
+ }
261
+
262
+ tooltipPosition.value = { top, left };
263
+ });
264
+ }
265
+
170
266
  onMounted(() => {
171
267
  document.addEventListener('click', handleClickOutside);
172
268
  });
@@ -325,4 +421,65 @@ onUnmounted(() => {
325
421
  gap: 8px;
326
422
  margin-left: auto;
327
423
  }
424
+
425
+ /* 模型特性提示框(组件内渲染,但使用 fixed 定位) */
426
+ .ai-chat-model-tooltip {
427
+ /* 比之前更窄一点,避免“太宽” */
428
+ min-width: 160px;
429
+ max-width: 240px;
430
+ width: max-content;
431
+ padding: 12px;
432
+ background: var(--chat-dropdown-bg, #252526);
433
+ border: 1px solid rgba(255, 255, 255, 0.15);
434
+ border-radius: 8px;
435
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
436
+ pointer-events: none;
437
+ }
438
+
439
+ .ai-chat-model-tooltip__section {
440
+ margin-bottom: 10px;
441
+ }
442
+
443
+ .ai-chat-model-tooltip__section:last-child {
444
+ margin-bottom: 0;
445
+ }
446
+
447
+ .ai-chat-model-tooltip__title {
448
+ font-size: 11px;
449
+ font-weight: 500;
450
+ color: var(--chat-text-muted, #888);
451
+ margin-bottom: 6px;
452
+ text-transform: uppercase;
453
+ letter-spacing: 0.5px;
454
+ }
455
+
456
+ .ai-chat-model-tooltip__features {
457
+ display: flex;
458
+ flex-direction: column;
459
+ gap: 4px;
460
+ }
461
+
462
+ .ai-chat-model-tooltip__feature {
463
+ display: flex;
464
+ align-items: center;
465
+ gap: 6px;
466
+ font-size: 13px;
467
+ color: var(--chat-text, #ccc);
468
+ }
469
+
470
+ .ai-chat-model-tooltip__check {
471
+ color: #4ade80;
472
+ flex-shrink: 0;
473
+ }
474
+
475
+ .ai-chat-model-tooltip__cost {
476
+ font-size: 13px;
477
+ color: var(--chat-text, #ccc);
478
+ }
479
+
480
+ .ai-chat-model-tooltip__description {
481
+ font-size: 12px;
482
+ color: var(--chat-text-muted, #999);
483
+ line-height: 1.5;
484
+ }
328
485
  </style>
@@ -6,6 +6,7 @@
6
6
  v-if="onSend && inputState"
7
7
  variant="message"
8
8
  :value="userText"
9
+ :images="images"
9
10
  :mode="inputState.mode.value"
10
11
  :model="inputState.model.value"
11
12
  :models="inputState.models.value"
@@ -27,7 +28,6 @@
27
28
  :key="i"
28
29
  :src="img"
29
30
  class="user-image"
30
- @click="$emit('view-image', img)"
31
31
  />
32
32
  </div>
33
33
  <div v-if="formattedTime" class="message-time user-time">{{ formattedTime }}</div>
@@ -41,13 +41,19 @@
41
41
  <PartsRenderer
42
42
  :parts="parts"
43
43
  :expanded-type="stepsExpandedType"
44
+ :part-renderers="partRenderers"
44
45
  :adapter="adapter"
46
+ :on-cancel-tool-call="onCancelToolCall"
45
47
  :auto-run-config="autoRunConfig"
46
48
  :on-save-config="onSaveConfig"
47
49
  />
48
50
 
49
51
  <!-- 加载指示器:等待状态时显示 -->
50
- <div v-if="loadingState.type === 'text'" class="loading-indicator">
52
+ <div
53
+ v-if="loadingState.type === 'text'"
54
+ class="loading-indicator"
55
+ :class="{ 'has-content-above': parts.length > 0 }"
56
+ >
51
57
  <span class="loading-text">{{ loadingState.text }}</span>
52
58
  <span class="loading-shimmer"></span>
53
59
  </div>
@@ -63,7 +69,7 @@
63
69
  <!-- 右侧:时间和按钮 -->
64
70
  <div class="action-buttons">
65
71
  <span v-if="formattedTime" class="message-time assistant-time">{{ formattedTime }}</span>
66
- <button class="action-btn" title="复制" @click="$emit('copy')">
72
+ <button :class="['action-btn', { copied }]" title="复制" @click="$emit('copy')">
67
73
  <Icon :icon="copied ? 'lucide:check' : 'lucide:copy'" width="14" />
68
74
  </button>
69
75
  <button class="action-btn" title="重新生成" @click="$emit('regenerate')">
@@ -76,7 +82,7 @@
76
82
  </template>
77
83
 
78
84
  <script setup lang="ts">
79
- import { computed, inject, type Ref } from 'vue'
85
+ import { computed, inject, type Ref, type Component } from 'vue'
80
86
  import { Icon } from '@iconify/vue'
81
87
  import ChatInput from '../input/ChatInput.vue'
82
88
  import PartsRenderer from './PartsRenderer.vue'
@@ -100,6 +106,9 @@ interface ChatInputState {
100
106
 
101
107
  const inputState = inject<ChatInputState>('chatInputState')
102
108
 
109
+ // 注入自定义 Part 渲染器
110
+ const partRenderers = inject<Record<string, Component>>('partRenderers', {})
111
+
103
112
  const props = withDefaults(defineProps<{
104
113
  role: 'user' | 'assistant'
105
114
  /** 内容 parts 数组 - 新架构核心 */
@@ -122,6 +131,8 @@ const props = withDefaults(defineProps<{
122
131
  stepsExpandedType?: 'open' | 'close' | 'auto'
123
132
  /** 工具调用相关 - 通过 props 传递 */
124
133
  adapter?: ChatAdapter
134
+ /** 取消工具调用(通常会中止当前请求/流式输出) */
135
+ onCancelToolCall?: (toolCallId: string) => void
125
136
  autoRunConfig?: AutoRunConfig
126
137
  onSaveConfig?: (config: AutoRunConfig) => Promise<void>
127
138
  }>(), {
@@ -141,60 +152,64 @@ const userText = computed(() => {
141
152
  // 是否有内容(用于显示操作按钮)
142
153
  const hasContent = computed(() => {
143
154
  return props.parts.some(p =>
144
- (p.type === 'text' && p.text) ||
145
- p.type === 'tool_result' ||
155
+ (p.type === 'text' && (p as TextPart).text) ||
146
156
  p.type === 'thinking' ||
147
- p.type === 'search'
157
+ p.type === 'search' ||
158
+ p.type === 'tool_call'
148
159
  )
149
160
  })
150
161
 
151
162
  // loading 状态:决定显示什么类型的指示器
152
- // cursor: 闪烁光标(文本流式输出时)
153
163
  // text: 文字提示(等待状态时)
154
- // none: 不显示
155
- const loadingState = computed<{ type: 'cursor' | 'text' | 'none'; text?: string }>(() => {
164
+ // none: 不显示(有正在进行的活动)
165
+ const loadingState = computed<{ type: 'text' | 'none'; text?: string }>(() => {
156
166
  if (!props.loading) {
157
167
  return { type: 'none' }
158
168
  }
159
169
 
160
- if (props.parts.length === 0) {
161
- return { type: 'text', text: '正在思考...' }
162
- }
163
-
164
- const lastPart = props.parts[props.parts.length - 1]
170
+ // 分模式显示:ask 模式直接生成回答,agent 模式规划下一步
171
+ const waitingText = props.mode === 'ask' ? '正在生成回答...' : '正在规划下一步...'
165
172
 
166
- // 文本流式输出 不需要额外指示,用户能看到文字在增加
167
- if (lastPart.type === 'text') {
168
- return { type: 'none' }
173
+ // 没有任何 parts 时,显示等待
174
+ if (props.parts.length === 0) {
175
+ return { type: 'text', text: waitingText }
169
176
  }
170
177
 
171
- // 工具调用完成后 → 显示规划提示
172
- if (lastPart.type === 'tool_call') {
173
- const status = (lastPart as ToolCallPart).status
174
- if (status === 'done' || status === 'error' || status === 'skipped') {
175
- return { type: 'text', text: '正在规划下一步...' }
178
+ // 检查是否有正在进行的活动(如果有,不需要显示等待提示)
179
+ const hasActiveActivity = props.parts.some(part => {
180
+ // 思考正在进行
181
+ if (part.type === 'thinking' && (part as ThinkingPart).status === 'running') {
182
+ return true
176
183
  }
177
- // 工具正在执行时,卡片本身有状态,不需要额外指示
178
- return { type: 'none' }
179
- }
180
-
181
- // 搜索完成后 → 显示规划提示
182
- if (lastPart.type === 'search') {
183
- if ((lastPart as SearchPart).status === 'done') {
184
- return { type: 'text', text: '正在规划下一步...' }
184
+ // 搜索正在进行
185
+ if (part.type === 'search' && (part as SearchPart).status === 'running') {
186
+ return true
185
187
  }
188
+ // 工具调用正在进行(running 或 pending)
189
+ if (part.type === 'tool_call') {
190
+ const status = (part as ToolCallPart).status
191
+ if (status === 'running' || status === 'pending') {
192
+ return true
193
+ }
194
+ }
195
+ return false
196
+ })
197
+
198
+ // 有正在进行的活动 → 不显示等待提示(活动本身有状态显示)
199
+ if (hasActiveActivity) {
186
200
  return { type: 'none' }
187
201
  }
188
202
 
189
- // 思考完成后 显示规划提示
190
- if (lastPart.type === 'thinking') {
191
- if ((lastPart as ThinkingPart).status === 'done') {
192
- return { type: 'text', text: '正在规划下一步...' }
193
- }
203
+ // 检查最后一个 part 是否是正在流式输出的文本
204
+ const lastPart = props.parts[props.parts.length - 1]
205
+ if (lastPart.type === 'text') {
206
+ // 文本正在流式输出 不显示等待提示
194
207
  return { type: 'none' }
195
208
  }
196
209
 
197
- return { type: 'none' }
210
+ // 没有任何正在进行的活动,但 loading=true 显示等待提示
211
+ // 这包括:工具执行完成后、思考完成后、搜索完成后等"空窗期"
212
+ return { type: 'text', text: waitingText }
198
213
  })
199
214
 
200
215
  /** 获取模型显示名称 */
@@ -222,14 +237,12 @@ const formattedTime = computed(() => {
222
237
  defineEmits<{
223
238
  copy: []
224
239
  regenerate: []
225
- 'view-image': [url: string]
226
240
  send: [text: string]
227
241
  }>()
228
242
  </script>
229
243
 
230
244
  <style scoped>
231
245
  .message-bubble {
232
- padding: 8px 0;
233
246
  animation: fadeIn 0.2s ease;
234
247
  }
235
248
 
@@ -304,7 +317,11 @@ defineEmits<{
304
317
  background: var(--chat-muted, #2a2a2a);
305
318
  border-radius: 8px;
306
319
  overflow: hidden;
307
- margin: 8px 0;
320
+ }
321
+
322
+ /* 当上方有内容时,添加间距 */
323
+ .loading-indicator.has-content-above {
324
+ margin-top: 8px;
308
325
  }
309
326
 
310
327
  .loading-text {
@@ -395,6 +412,10 @@ defineEmits<{
395
412
  color: var(--chat-text, #ccc);
396
413
  }
397
414
 
415
+ .action-btn.copied {
416
+ color: #22c55e;
417
+ }
418
+
398
419
  /* 消息时间 */
399
420
  .message-time {
400
421
  font-size: 12px;
@@ -1,15 +1,20 @@
1
1
  <template>
2
2
  <div class="parts-renderer">
3
- <template v-for="(part, index) in parts" :key="index">
4
- <!-- 文本 -->
3
+ <div
4
+ v-for="(part, index) in visibleParts"
5
+ :key="index"
6
+ class="part-item"
7
+ :style="{ marginTop: index > 0 ? '8px' : '0' }"
8
+ >
9
+ <!-- 文本(内部已处理代码块渲染) -->
5
10
  <TextPart
6
- v-if="part.type === 'text'"
11
+ v-if="isText(part)"
7
12
  :text="part.text"
8
13
  />
9
14
 
10
15
  <!-- 思考 -->
11
16
  <ThinkingPart
12
- v-else-if="part.type === 'thinking'"
17
+ v-else-if="isThinking(part)"
13
18
  :text="part.text"
14
19
  :status="part.status"
15
20
  :duration="part.duration"
@@ -18,57 +23,70 @@
18
23
 
19
24
  <!-- 搜索 -->
20
25
  <SearchPart
21
- v-else-if="part.type === 'search'"
26
+ v-else-if="isSearch(part)"
22
27
  :query="part.query"
23
28
  :results="part.results"
24
29
  :status="part.status"
25
30
  :expanded-type="expandedType"
26
31
  />
27
32
 
28
- <!-- 工具调用(包含执行结果) -->
33
+ <!-- 工具调用 -->
29
34
  <ToolCallPart
30
- v-else-if="part.type === 'tool_call'"
35
+ v-else-if="isToolCall(part)"
31
36
  :id="part.id"
32
37
  :name="part.name"
33
38
  :args="part.args"
39
+ :output="part.output"
34
40
  :status="part.status"
35
- :result="part.result"
36
41
  :expanded-type="expandedType"
37
42
  :adapter="adapter"
43
+ :on-cancel-tool-call="onCancelToolCall"
38
44
  :auto-run-config="autoRunConfig"
39
45
  :on-save-config="onSaveConfig"
40
46
  />
41
47
 
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
48
  <!-- 图片 -->
54
49
  <ImagePart
55
- v-else-if="part.type === 'image'"
50
+ v-else-if="isImage(part)"
56
51
  :url="part.url"
57
52
  />
58
53
 
59
54
  <!-- 错误 -->
60
55
  <ErrorPart
61
- v-else-if="part.type === 'error'"
56
+ v-else-if="isError(part)"
62
57
  :message="part.message"
63
58
  :category="part.category"
64
59
  :retryable="part.retryable"
65
60
  />
66
- </template>
61
+
62
+ <!-- 自定义类型:使用 partRenderers 渲染 -->
63
+ <component
64
+ v-else-if="partRenderers[part.type]"
65
+ :is="partRenderers[part.type]"
66
+ v-bind="getPartProps(part)"
67
+ />
68
+
69
+ <!-- 未知类型:显示 JSON(开发调试用) -->
70
+ <div v-else class="unknown-part">
71
+ <pre>{{ JSON.stringify(part, null, 2) }}</pre>
72
+ </div>
73
+ </div>
67
74
  </div>
68
75
  </template>
69
76
 
70
77
  <script setup lang="ts">
71
- import type { ContentPart, StepsExpandedType } from '../../types'
78
+ import { computed } from 'vue'
79
+ import type { Component } from 'vue'
80
+ import type {
81
+ ContentPart,
82
+ StepsExpandedType,
83
+ TextPart as TextPartType,
84
+ ThinkingPart as ThinkingPartType,
85
+ SearchPart as SearchPartType,
86
+ ToolCallPart as ToolCallPartType,
87
+ ImagePart as ImagePartType,
88
+ ErrorPart as ErrorPartType,
89
+ } from '../../types'
72
90
  import type { ChatAdapter } from '../../adapter'
73
91
  import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer'
74
92
  import {
@@ -76,21 +94,60 @@ import {
76
94
  ThinkingPart,
77
95
  SearchPart,
78
96
  ToolCallPart,
79
- ToolResultPart,
80
97
  ImagePart,
81
- ErrorPart
98
+ ErrorPart,
82
99
  } from './parts'
83
100
 
84
- withDefaults(defineProps<{
101
+ const props = withDefaults(defineProps<{
85
102
  parts: ContentPart[]
86
103
  expandedType?: StepsExpandedType
104
+ /** 自定义 Part 渲染器(key: part.type, value: 渲染组件) */
105
+ partRenderers?: Record<string, Component>
87
106
  // 工具调用相关
88
107
  adapter?: ChatAdapter
108
+ /** 取消工具调用(透传给 ToolCallPart) */
109
+ onCancelToolCall?: (toolCallId: string) => void
89
110
  autoRunConfig?: AutoRunConfig
90
111
  onSaveConfig?: (config: AutoRunConfig) => Promise<void>
91
112
  }>(), {
92
- expandedType: 'auto'
113
+ expandedType: 'auto',
114
+ partRenderers: () => ({}),
93
115
  })
116
+
117
+ /** 可见的 parts */
118
+ const visibleParts = computed(() => props.parts)
119
+
120
+ // ==================== 类型守卫函数 ====================
121
+
122
+ function isText(part: ContentPart): part is TextPartType {
123
+ return part.type === 'text'
124
+ }
125
+
126
+ function isThinking(part: ContentPart): part is ThinkingPartType {
127
+ return part.type === 'thinking'
128
+ }
129
+
130
+ function isSearch(part: ContentPart): part is SearchPartType {
131
+ return part.type === 'search'
132
+ }
133
+
134
+ function isToolCall(part: ContentPart): part is ToolCallPartType {
135
+ return part.type === 'tool_call'
136
+ }
137
+
138
+ function isImage(part: ContentPart): part is ImagePartType {
139
+ return part.type === 'image'
140
+ }
141
+
142
+ function isError(part: ContentPart): part is ErrorPartType {
143
+ return part.type === 'error'
144
+ }
145
+
146
+ /** 获取 Part 的 props(去掉 type 字段) */
147
+ function getPartProps(part: ContentPart): Record<string, unknown> {
148
+ const { type, ...rest } = part as Record<string, unknown>
149
+ return rest
150
+ }
94
151
  </script>
95
152
 
96
153
  <style scoped>
@@ -98,4 +155,26 @@ withDefaults(defineProps<{
98
155
  display: flex;
99
156
  flex-direction: column;
100
157
  }
158
+
159
+ .part-item {
160
+ margin-top: 8px;
161
+ }
162
+
163
+ .part-item:first-child {
164
+ margin-top: 0;
165
+ }
166
+
167
+ .unknown-part {
168
+ padding: 8px;
169
+ background: var(--chat-muted, #2a2a2a);
170
+ border-radius: 4px;
171
+ font-size: 12px;
172
+ color: var(--chat-text-muted, #999);
173
+ }
174
+
175
+ .unknown-part pre {
176
+ margin: 0;
177
+ white-space: pre-wrap;
178
+ word-break: break-word;
179
+ }
101
180
  </style>
@@ -1,7 +1,10 @@
1
1
  <template>
2
2
  <div :class="['collapsible-card', { expanded: modelValue }]">
3
3
  <div class="card-header">
4
- <div class="card-header-left" @click="emit('update:modelValue', !modelValue)">
4
+ <div
5
+ :class="['card-header-left', { clickable: collapsible }]"
6
+ @click="collapsible && emit('update:modelValue', !modelValue)"
7
+ >
5
8
  <div class="card-icon" :style="{ color: iconColor }">
6
9
  <Icon v-if="spinning" icon="lucide:loader-2" width="14" class="spinning" />
7
10
  <Icon v-else :icon="icon" width="14" />
@@ -59,7 +62,6 @@ const emit = defineEmits<{
59
62
 
60
63
  <style scoped>
61
64
  .collapsible-card {
62
- margin: 8px 0;
63
65
  border-radius: 8px;
64
66
  background: var(--chat-muted, #2a2a2a);
65
67
  border: 1px solid var(--chat-border, #333);
@@ -80,6 +82,9 @@ const emit = defineEmits<{
80
82
  align-items: center;
81
83
  gap: 8px;
82
84
  flex: 1;
85
+ }
86
+
87
+ .card-header-left.clickable {
83
88
  cursor: pointer;
84
89
  }
85
90
 
@@ -128,7 +133,7 @@ const emit = defineEmits<{
128
133
  }
129
134
 
130
135
  .card-content {
131
- padding: 12px;
136
+ padding: 0 12px 12px;
132
137
  min-width: 0;
133
138
  overflow: hidden;
134
139
  }