@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.
- package/README.md +85 -77
- package/dist/_baseUniq-Berj5otS.js +477 -0
- package/dist/arc-D4b-De5i.js +83 -0
- package/dist/architecture-U656AL7Q-B1R4o7wt.js +5 -0
- package/dist/architectureDiagram-VXUJARFQ-0H1LzSrf.js +4668 -0
- package/dist/blockDiagram-VD42YOAC-Dd6DMjLf.js +2265 -0
- package/dist/c4Diagram-YG6GDRKO-c6mpIonb.js +1581 -0
- package/dist/channel-CQSBI1fo.js +5 -0
- package/dist/chunk-4BX2VUAB-CfWC7lTG.js +9 -0
- package/dist/chunk-55IACEB6-joM8m2Zv.js +8 -0
- package/dist/chunk-B4BG7PRW-DN94Y3mS.js +1376 -0
- package/dist/chunk-DI55MBZ5-CPRGoFPG.js +1382 -0
- package/dist/chunk-FMBD7UC4-dUKl2TcN.js +19 -0
- package/dist/chunk-QN33PNHL-o3WmqquO.js +20 -0
- package/dist/chunk-QZHKN3VN-Btf62lRF.js +15 -0
- package/dist/chunk-TZMSLE5B-QCPpnm8Y.js +64 -0
- package/dist/classDiagram-2ON5EDUG-ERF9RE9K.js +16 -0
- package/dist/classDiagram-v2-WZHVMYZB-ERF9RE9K.js +16 -0
- package/dist/clone-B_B1yyxj.js +8 -0
- package/dist/components/ChatPanel.vue.d.ts +11 -7
- package/dist/components/ChatPanel.vue.d.ts.map +1 -1
- package/dist/components/common/SettingsPanel.vue.d.ts +10 -1
- package/dist/components/common/SettingsPanel.vue.d.ts.map +1 -1
- package/dist/components/common/ToggleSwitch.vue.d.ts.map +1 -1
- package/dist/components/input/ChatInput.vue.d.ts +4 -1
- package/dist/components/input/ChatInput.vue.d.ts.map +1 -1
- package/dist/components/input/DropdownSelector.vue.d.ts +10 -0
- package/dist/components/input/DropdownSelector.vue.d.ts.map +1 -1
- package/dist/components/message/MessageBubble.vue.d.ts +2 -2
- package/dist/components/message/MessageBubble.vue.d.ts.map +1 -1
- package/dist/components/message/PartsRenderer.vue.d.ts +6 -0
- package/dist/components/message/PartsRenderer.vue.d.ts.map +1 -1
- package/dist/components/message/parts/CollapsibleCard.vue.d.ts.map +1 -1
- package/dist/components/message/parts/ImagePart.vue.d.ts.map +1 -1
- package/dist/components/message/parts/SearchPart.vue.d.ts.map +1 -1
- package/dist/components/message/parts/TextPart.vue.d.ts +3 -1
- package/dist/components/message/parts/TextPart.vue.d.ts.map +1 -1
- package/dist/components/message/parts/ToolCallPart.vue.d.ts +9 -4
- package/dist/components/message/parts/ToolCallPart.vue.d.ts.map +1 -1
- package/dist/components/message/parts/index.d.ts +5 -1
- package/dist/components/message/parts/index.d.ts.map +1 -1
- package/dist/components/message/parts/visual-predicate.d.ts +7 -0
- package/dist/components/message/parts/visual-predicate.d.ts.map +1 -0
- package/dist/components/message/parts/visual-render.d.ts +2 -0
- package/dist/components/message/parts/visual-render.d.ts.map +1 -0
- package/dist/components/message/parts/visual.d.ts +3 -0
- package/dist/components/message/parts/visual.d.ts.map +1 -0
- package/dist/composables/useChat.d.ts +34 -10
- package/dist/composables/useChat.d.ts.map +1 -1
- package/dist/composables/useImageUpload.d.ts +7 -0
- package/dist/composables/useImageUpload.d.ts.map +1 -1
- package/dist/cose-bilkent-S5V4N54A-C-CkEnIH.js +2608 -0
- package/dist/cytoscape.esm-CjI2IsL8.js +18735 -0
- package/dist/dagre-6UL2VRFP-BbmZysl9.js +446 -0
- package/dist/defaultLocale-D7EN2tov.js +171 -0
- package/dist/diagram-PSM6KHXK-CMnope5e.js +533 -0
- package/dist/diagram-QEK2KX5R-Bc87skpo.js +221 -0
- package/dist/diagram-S2PKOQOG-BtkzC5-o.js +143 -0
- package/dist/erDiagram-Q2GNP2WA-CwQLdjh9.js +842 -0
- package/dist/flowDiagram-NV44I4VS-8ehMD0M4.js +1627 -0
- package/dist/ganttDiagram-JELNMOA3-dLX8Pbvy.js +2670 -0
- package/dist/gitGraph-F6HP7TQM-BBk5qfnx.js +5 -0
- package/dist/gitGraphDiagram-NY62KEGX-DG_fvWrq.js +712 -0
- package/dist/graph-B7BnGhuR.js +381 -0
- package/dist/index-Cr3tTH72.js +83883 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -56433
- package/dist/info-NVLQJR56-eZr6nVr1.js +5 -0
- package/dist/infoDiagram-WHAUD3N6-BcVYTft_.js +24 -0
- package/dist/init-DjUOC4st.js +16 -0
- package/dist/journeyDiagram-XKPGCS4Q-D4C72gYB.js +834 -0
- package/dist/kanban-definition-3W4ZIXB7-BuSjAlm6.js +721 -0
- package/dist/layout-LjYblgUu.js +1441 -0
- package/dist/linear-DtPqj295.js +259 -0
- package/dist/mermaid-parser.core-DdOZxjrw.js +15189 -0
- package/dist/min-DYbgfxu0.js +38 -0
- package/dist/mindmap-definition-VGOIOE7T-BZxsr4M_.js +787 -0
- package/dist/ordinal-DfAQgscy.js +61 -0
- package/dist/packet-BFZMPI3H-XfNBjRzf.js +5 -0
- package/dist/pie-7BOR55EZ-pJDdOv19.js +5 -0
- package/dist/pieDiagram-ADFJNKIX-DDwfWBNn.js +161 -0
- package/dist/quadrantDiagram-AYHSOK5B-Ddpxuto0.js +1024 -0
- package/dist/radar-NHE76QYJ-jE07d-4Z.js +5 -0
- package/dist/requirementDiagram-UZGBJVZJ-B_TLWQ-N.js +852 -0
- package/dist/sankeyDiagram-TZEHDZUN-C1TWrUpG.js +810 -0
- package/dist/sequenceDiagram-WL72ISMW-B8pqb5YX.js +2518 -0
- package/dist/stateDiagram-FKZM4ZOC-DwNT1ej6.js +263 -0
- package/dist/stateDiagram-v2-4FDKWEC3-CZQUitzd.js +16 -0
- package/dist/style.css +1 -1
- package/dist/timeline-definition-IT6M3QCI-BGdS1wvf.js +799 -0
- package/dist/treemap-KMMF4GRG-B_eEU0Q4.js +5 -0
- package/dist/types/index.d.ts +36 -37
- package/dist/types/index.d.ts.map +1 -1
- package/dist/xychartDiagram-PRI3JC2R-DtwXQqN4.js +1340 -0
- package/package.json +3 -3
- package/src/components/ChatPanel.vue +75 -13
- package/src/components/common/SettingsPanel.vue +169 -0
- package/src/components/input/ChatInput.vue +49 -34
- package/src/components/input/DropdownSelector.vue +175 -18
- package/src/components/message/MessageBubble.vue +61 -40
- package/src/components/message/PartsRenderer.vue +106 -27
- package/src/components/message/parts/CollapsibleCard.vue +8 -3
- package/src/components/message/parts/ImagePart.vue +0 -1
- package/src/components/message/parts/SearchPart.vue +2 -0
- package/src/components/message/parts/TextPart.vue +753 -11
- package/src/components/message/parts/ToolCallPart.vue +315 -246
- package/src/components/message/parts/index.ts +5 -1
- package/src/components/message/parts/visual-predicate.ts +43 -0
- package/src/components/message/parts/visual-render.ts +19 -0
- package/src/components/message/parts/visual.ts +12 -0
- package/src/composables/useChat.ts +560 -92
- package/src/composables/useImageUpload.ts +115 -11
- package/src/index.ts +6 -8
- package/src/styles.css +124 -0
- package/src/types/index.ts +52 -44
- package/dist/components/message/blocks/CodeBlock.vue.d.ts +0 -11
- package/dist/components/message/blocks/CodeBlock.vue.d.ts.map +0 -1
- package/dist/components/message/blocks/TextBlock.vue.d.ts +0 -7
- package/dist/components/message/blocks/TextBlock.vue.d.ts.map +0 -1
- package/dist/components/message/blocks/index.d.ts +0 -6
- package/dist/components/message/blocks/index.d.ts.map +0 -1
- package/dist/components/message/parts/ToolResultPart.vue.d.ts +0 -14
- package/dist/components/message/parts/ToolResultPart.vue.d.ts.map +0 -1
- package/dist/components/message/tool-results/DefaultToolResult.vue.d.ts +0 -4
- package/dist/components/message/tool-results/DefaultToolResult.vue.d.ts.map +0 -1
- package/dist/components/message/tool-results/SearchResults.vue.d.ts +0 -4
- package/dist/components/message/tool-results/SearchResults.vue.d.ts.map +0 -1
- package/dist/components/message/tool-results/WeatherCard.vue.d.ts +0 -4
- package/dist/components/message/tool-results/WeatherCard.vue.d.ts.map +0 -1
- package/dist/components/message/tool-results/index.d.ts +0 -7
- package/dist/components/message/tool-results/index.d.ts.map +0 -1
- package/src/components/message/ContentRenderer.vue +0 -61
- package/src/components/message/ToolResultRenderer.vue +0 -27
- package/src/components/message/blocks/CodeBlock.vue +0 -113
- package/src/components/message/blocks/TextBlock.vue +0 -21
- package/src/components/message/blocks/index.ts +0 -6
- package/src/components/message/parts/ToolResultPart.vue +0 -78
- package/src/components/message/tool-results/DefaultToolResult.vue +0 -45
- package/src/components/message/tool-results/SearchResults.vue +0 -133
- package/src/components/message/tool-results/WeatherCard.vue +0 -139
- 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
|
-
<
|
|
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
|
-
</
|
|
59
|
+
</div>
|
|
32
60
|
</template>
|
|
61
|
+
</div>
|
|
33
62
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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: '
|
|
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
|
-
|
|
161
|
-
|
|
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 (
|
|
168
|
-
return { type: '
|
|
173
|
+
// 没有任何 parts 时,显示等待
|
|
174
|
+
if (props.parts.length === 0) {
|
|
175
|
+
return { type: 'text', text: waitingText }
|
|
169
176
|
}
|
|
170
177
|
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
|
11
|
+
v-if="isText(part)"
|
|
7
12
|
:text="part.text"
|
|
8
13
|
/>
|
|
9
14
|
|
|
10
15
|
<!-- 思考 -->
|
|
11
16
|
<ThinkingPart
|
|
12
|
-
v-else-if="part
|
|
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
|
|
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
|
|
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
|
|
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
|
|
56
|
+
v-else-if="isError(part)"
|
|
62
57
|
:message="part.message"
|
|
63
58
|
:category="part.category"
|
|
64
59
|
:retryable="part.retryable"
|
|
65
60
|
/>
|
|
66
|
-
|
|
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
|
|
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
|
|
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
|
}
|