@veltra/ai 2.1.2 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/index.d.ts +2 -2
- package/dist/chat/transports/openai.js +52 -6
- package/dist/chat/transports/openai.js.map +1 -1
- package/dist/chat/types.d.ts +31 -1
- package/dist/chat/use-chat.d.ts +31 -1
- package/dist/chat/use-chat.js +33 -1
- package/dist/chat/use-chat.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js +17 -4
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/chat-input.js +75 -20
- package/dist/components/ai-chat/chat-input.js.map +1 -1
- package/dist/components/ai-chat/message-item.js +1 -6
- package/dist/components/ai-chat/message-item.js.map +1 -1
- package/dist/components/ai-chat/message-list.js +15 -0
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/style.d.ts +1 -0
- package/dist/components/ai-chat/style.js +1 -0
- package/dist/components/ai-chat/style2.css +38 -11
- package/dist/index.d.ts +2 -2
- package/dist/types/ai-chat.d.ts +11 -2
- package/package.json +5 -5
- package/src/chat/__test__/use-chat.test.ts +67 -0
- package/src/chat/transports/__test__/openai.test.ts +140 -0
- package/src/chat/transports/openai.ts +73 -7
- package/src/chat/types.ts +31 -0
- package/src/chat/use-chat.ts +39 -1
- package/src/components/ai-chat/__test__/ai-chat.test.ts +121 -1
- package/src/components/ai-chat/ai-chat.vue +13 -0
- package/src/components/ai-chat/chat-input.vue +64 -3
- package/src/components/ai-chat/message-item.vue +0 -1
- package/src/components/ai-chat/message-list.vue +22 -0
- package/src/components/ai-chat/style.scss +50 -15
- package/src/components/ai-chat/style.ts +1 -0
- package/src/types/ai-chat.ts +11 -1
|
@@ -30,6 +30,26 @@
|
|
|
30
30
|
<Attach />
|
|
31
31
|
</UIcon>
|
|
32
32
|
</UFilePicker>
|
|
33
|
+
|
|
34
|
+
<span :class="[cls.e('input-clear-wrap'), bem.is('disabled', !clearable)]">
|
|
35
|
+
<UPopConfirm
|
|
36
|
+
title="清空当前对话?进行中的生成将被中止。"
|
|
37
|
+
confirm-text="清空"
|
|
38
|
+
direction="top"
|
|
39
|
+
alignment="start"
|
|
40
|
+
@confirm="emit('clear')"
|
|
41
|
+
>
|
|
42
|
+
<template #reference>
|
|
43
|
+
<UIcon :class="cls.e('input-clear')" title="清除会话">
|
|
44
|
+
<Clear />
|
|
45
|
+
</UIcon>
|
|
46
|
+
</template>
|
|
47
|
+
</UPopConfirm>
|
|
48
|
+
</span>
|
|
49
|
+
|
|
50
|
+
<span v-if="tokenUsage" :class="cls.e('input-usage')" :title="usageText">
|
|
51
|
+
{{ usageText }}
|
|
52
|
+
</span>
|
|
33
53
|
</div>
|
|
34
54
|
|
|
35
55
|
<!-- 右簇:模型/推理选择 → 发送或停止(互斥) -->
|
|
@@ -69,12 +89,12 @@
|
|
|
69
89
|
</template>
|
|
70
90
|
|
|
71
91
|
<script lang="ts" setup>
|
|
72
|
-
import { UButton, UFilePicker, UIcon } from '@veltra/desktop'
|
|
73
|
-
import { Attach, Close, Up } from '@veltra/icons/normal'
|
|
92
|
+
import { UButton, UFilePicker, UIcon, UPopConfirm } from '@veltra/desktop'
|
|
93
|
+
import { Attach, Clear, Close, Up } from '@veltra/icons/normal'
|
|
74
94
|
import { bem } from '@veltra/utils'
|
|
75
95
|
import { computed, inject, nextTick, ref, shallowRef } from 'vue'
|
|
76
96
|
|
|
77
|
-
import type { ChatAttachment } from '../../chat/types'
|
|
97
|
+
import type { ChatAttachment, ChatTokenUsage } from '../../chat/types'
|
|
78
98
|
import type { ChatModelOption } from '../../providers'
|
|
79
99
|
import { AiChatDIKey } from './di'
|
|
80
100
|
import ModelPicker from './model-picker.vue'
|
|
@@ -92,6 +112,12 @@ const props = defineProps<{
|
|
|
92
112
|
accept?: string
|
|
93
113
|
/** 单个附件最大字节数 */
|
|
94
114
|
maxAttachmentSize?: number
|
|
115
|
+
/** 是否可清除(有消息 / 队列 / 生成中) */
|
|
116
|
+
clearable?: boolean
|
|
117
|
+
/** 会话累计 token;无 usage 时为 null,不展示 */
|
|
118
|
+
tokenUsage?: ChatTokenUsage | null
|
|
119
|
+
/** 是否展示缓存命中 / 未命中(默认只显示总 token) */
|
|
120
|
+
tokenUsageDetail?: boolean
|
|
95
121
|
}>()
|
|
96
122
|
|
|
97
123
|
const model = defineModel<string>('model')
|
|
@@ -100,6 +126,7 @@ const reasoningLevel = defineModel<string>('reasoningLevel')
|
|
|
100
126
|
const emit = defineEmits<{
|
|
101
127
|
(e: 'send', content: string, attachments: ChatAttachment[]): void
|
|
102
128
|
(e: 'abort'): void
|
|
129
|
+
(e: 'clear'): void
|
|
103
130
|
}>()
|
|
104
131
|
|
|
105
132
|
const di = inject(AiChatDIKey)
|
|
@@ -114,6 +141,40 @@ const hasContent = computed(() => !!text.value.trim() || attachments.value.lengt
|
|
|
114
141
|
/** 生成中且输入为空时显示停止;有内容则显示发送(入队) */
|
|
115
142
|
const showStop = computed(() => props.running && !hasContent.value)
|
|
116
143
|
|
|
144
|
+
/**
|
|
145
|
+
* 用量数字:不到 1000 原样;≥1000 用 K;≥100 万用 M。
|
|
146
|
+
* 最多 1 位小数,整数不写 `.0`(1000 → 1K,1500 → 1.5K)。
|
|
147
|
+
*/
|
|
148
|
+
const formatTokenCount = (n: number) => {
|
|
149
|
+
const scaled = (value: number, unit: 'K' | 'M') => {
|
|
150
|
+
const rounded = Math.round(value * 10) / 10
|
|
151
|
+
return `${Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)}${unit}`
|
|
152
|
+
}
|
|
153
|
+
if (n < 1000) return String(n)
|
|
154
|
+
if (n < 1_000_000) {
|
|
155
|
+
const k = Math.round((n / 1000) * 10) / 10
|
|
156
|
+
// 四舍五入后会变成 1000K 时改用 M
|
|
157
|
+
return k >= 1000 ? scaled(n / 1_000_000, 'M') : scaled(n / 1000, 'K')
|
|
158
|
+
}
|
|
159
|
+
return scaled(n / 1_000_000, 'M')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 会话累计:总 token;明细再拼有数据的缓存项,缺字段不写 0 */
|
|
163
|
+
const usageText = computed(() => {
|
|
164
|
+
const usage = props.tokenUsage
|
|
165
|
+
if (!usage) return ''
|
|
166
|
+
const parts = [`总 token ${formatTokenCount(usage.totalTokens)}`]
|
|
167
|
+
if (props.tokenUsageDetail) {
|
|
168
|
+
if (usage.cacheHitTokens != null) {
|
|
169
|
+
parts.push(`缓存命中 ${formatTokenCount(usage.cacheHitTokens)}`)
|
|
170
|
+
}
|
|
171
|
+
if (usage.cacheMissTokens != null) {
|
|
172
|
+
parts.push(`缓存未命中 ${formatTokenCount(usage.cacheMissTokens)}`)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return parts.join(' · ')
|
|
176
|
+
})
|
|
177
|
+
|
|
117
178
|
/** 生成中提示用户消息将进入队列 */
|
|
118
179
|
const placeholderText = computed(() => {
|
|
119
180
|
if (props.running) return '会话进行中,发送的消息将进入待发送队列'
|
|
@@ -317,6 +317,15 @@ watch(
|
|
|
317
317
|
}
|
|
318
318
|
if (!prev) return
|
|
319
319
|
|
|
320
|
+
// 清除会话后列表已空:跳过结束停留,立刻回到欢迎区
|
|
321
|
+
if (visibleMessages.value.length === 0) {
|
|
322
|
+
finishing.value = false
|
|
323
|
+
workingLeaving.value = false
|
|
324
|
+
idleLeaving.value = false
|
|
325
|
+
idleEnter.value = false
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
320
329
|
finishing.value = true
|
|
321
330
|
idleLeaving.value = false
|
|
322
331
|
const last = visibleMessages.value[visibleMessages.value.length - 1]
|
|
@@ -341,6 +350,19 @@ watch(
|
|
|
341
350
|
}
|
|
342
351
|
)
|
|
343
352
|
|
|
353
|
+
// 消息被清空(clear):立刻收掉工作球,欢迎区恢复
|
|
354
|
+
watch(
|
|
355
|
+
() => visibleMessages.value.length,
|
|
356
|
+
(len) => {
|
|
357
|
+
if (len > 0) return
|
|
358
|
+
clearJumpTimers()
|
|
359
|
+
finishing.value = false
|
|
360
|
+
workingLeaving.value = false
|
|
361
|
+
idleLeaving.value = false
|
|
362
|
+
idleEnter.value = false
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
|
|
344
366
|
// 空闲欢迎区展示期间自动轮换;跳到工作位后停止
|
|
345
367
|
watch(
|
|
346
368
|
() => [welcomeItems.value.length, showIdleWelcome.value] as const,
|
|
@@ -416,10 +416,11 @@ $root-name: ai-chat;
|
|
|
416
416
|
display: block;
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
419
|
+
// 信息密度必须写在 .markstream-vue 上:该库把 --ms-flow-* / --ms-text-* 定义在根节点,
|
|
420
|
+
// 父级(如 __message-content)的 0.55em 会被同元素上的 1.5em 默认值盖掉。
|
|
421
|
+
// 选择器带 .u-ai-chat 前缀,特异性高于库默认,不依赖 CSS 加载顺序
|
|
422
|
+
//(产物里 markstream-vue/index.css 仍排在我们的 css 之后)。
|
|
423
|
+
.markstream-vue {
|
|
423
424
|
--ms-text-body: 13px;
|
|
424
425
|
--ms-leading-body: 1.6;
|
|
425
426
|
--ms-text-h1: 1.55em;
|
|
@@ -444,6 +445,13 @@ $root-name: ai-chat;
|
|
|
444
445
|
--ms-flow-hr-y: 0.9em;
|
|
445
446
|
--ms-flow-admonition-y: 0.7em;
|
|
446
447
|
--ms-flow-diagram-y: 0.7em;
|
|
448
|
+
|
|
449
|
+
// markstream 在 ≤1023px 给 ul.list-disc 写死了 ~1.33em 边距,不走 --ms-flow-list-*
|
|
450
|
+
.list-disc {
|
|
451
|
+
margin-top: var(--ms-flow-list-y);
|
|
452
|
+
margin-bottom: var(--ms-flow-list-y);
|
|
453
|
+
padding-left: var(--ms-flow-list-indent);
|
|
454
|
+
}
|
|
447
455
|
}
|
|
448
456
|
|
|
449
457
|
@include m.e(message-status) {
|
|
@@ -551,20 +559,21 @@ $root-name: ai-chat;
|
|
|
551
559
|
margin: 4px 0;
|
|
552
560
|
font-size: fn.use-var(font-size-main, small);
|
|
553
561
|
|
|
554
|
-
//
|
|
555
|
-
|
|
562
|
+
// 产物里 desktop collapse 样式在本文件之后加载;
|
|
563
|
+
// `.u-collapse__item + .u-collapse__item` 与单层兄弟选择器同特异性,必须带 .u-ai-chat 前缀。
|
|
564
|
+
.u-#{$root-name} & + #{fn.bem($root-name, tool-call)} {
|
|
556
565
|
margin-top: 4px;
|
|
557
566
|
}
|
|
558
567
|
|
|
559
568
|
// 覆盖 UCollapseItem 默认头部/内容区节奏,保持紧凑单行头
|
|
560
|
-
.u-collapse__header {
|
|
569
|
+
.u-#{$root-name} & .u-collapse__header {
|
|
561
570
|
gap: 6px;
|
|
562
571
|
padding: 5px fn.use-var(gap, small);
|
|
563
572
|
font-weight: 400;
|
|
564
573
|
letter-spacing: normal;
|
|
565
574
|
}
|
|
566
575
|
|
|
567
|
-
.u-collapse__content {
|
|
576
|
+
.u-#{$root-name} & .u-collapse__content {
|
|
568
577
|
padding: fn.use-var(gap, small);
|
|
569
578
|
line-height: 1.5;
|
|
570
579
|
}
|
|
@@ -857,7 +866,7 @@ $root-name: ai-chat;
|
|
|
857
866
|
}
|
|
858
867
|
}
|
|
859
868
|
|
|
860
|
-
//
|
|
869
|
+
// 输入区:无边框卡片,靠浅阴影区分层级;聚焦不换蓝描边,避免变成另一种框
|
|
861
870
|
@include m.e(input-area) {
|
|
862
871
|
flex-shrink: 0;
|
|
863
872
|
width: 100%;
|
|
@@ -866,12 +875,8 @@ $root-name: ai-chat;
|
|
|
866
875
|
margin: 0 0 fn.use-var(gap, default);
|
|
867
876
|
border-radius: fn.use-var(radius, large);
|
|
868
877
|
background-color: fn.use-var(bg-color, top);
|
|
878
|
+
box-shadow: fn.use-var(shadow, sm);
|
|
869
879
|
padding: fn.use-var(gap, small);
|
|
870
|
-
transition: box-shadow 0.15s ease;
|
|
871
|
-
|
|
872
|
-
&:focus-within {
|
|
873
|
-
box-shadow: 0 0 0 1px fn.use-var(color, primary);
|
|
874
|
-
}
|
|
875
880
|
}
|
|
876
881
|
|
|
877
882
|
@include m.e(input) {
|
|
@@ -901,7 +906,8 @@ $root-name: ai-chat;
|
|
|
901
906
|
@include m.e(input-toolbar-left) {
|
|
902
907
|
@include m.flex(flex, flex-start, center);
|
|
903
908
|
gap: fn.use-var(gap, small);
|
|
904
|
-
flex
|
|
909
|
+
flex: 1;
|
|
910
|
+
min-width: 0;
|
|
905
911
|
}
|
|
906
912
|
|
|
907
913
|
@include m.e(input-toolbar-right) {
|
|
@@ -955,6 +961,35 @@ $root-name: ai-chat;
|
|
|
955
961
|
}
|
|
956
962
|
}
|
|
957
963
|
|
|
964
|
+
@include m.e(input-clear-wrap) {
|
|
965
|
+
flex-shrink: 0;
|
|
966
|
+
display: inline-flex;
|
|
967
|
+
line-height: 0;
|
|
968
|
+
|
|
969
|
+
@include m.is(disabled) {
|
|
970
|
+
opacity: 0.4;
|
|
971
|
+
pointer-events: none;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
@include m.e(input-clear) {
|
|
976
|
+
color: fn.use-var(text-color, assist);
|
|
977
|
+
cursor: pointer;
|
|
978
|
+
font-size: 18px;
|
|
979
|
+
|
|
980
|
+
&:hover {
|
|
981
|
+
color: fn.use-var(color, danger);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
@include m.e(input-usage) {
|
|
986
|
+
min-width: 0;
|
|
987
|
+
@include m.ellipsis;
|
|
988
|
+
font-size: fn.use-var(font-size-main, small);
|
|
989
|
+
color: fn.use-var(text-color, assist);
|
|
990
|
+
line-height: 1.4;
|
|
991
|
+
}
|
|
992
|
+
|
|
958
993
|
@include m.e(input-stop) {
|
|
959
994
|
display: block;
|
|
960
995
|
width: 10px;
|
|
@@ -5,6 +5,7 @@ import '@veltra/desktop/components/icon/style'
|
|
|
5
5
|
import '@veltra/desktop/components/input/style'
|
|
6
6
|
import '@veltra/desktop/components/layout/style'
|
|
7
7
|
import '@veltra/desktop/components/message/style'
|
|
8
|
+
import '@veltra/desktop/components/pop-confirm/style'
|
|
8
9
|
import '@veltra/desktop/components/scroll/style'
|
|
9
10
|
import '@veltra/desktop/components/select/style'
|
|
10
11
|
import '@veltra/styles/animations/shine.scss'
|
package/src/types/ai-chat.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ChatAttachment,
|
|
6
6
|
ChatMessage,
|
|
7
7
|
ChatQueuedMessage,
|
|
8
|
+
ChatTokenUsage,
|
|
8
9
|
ChatTool,
|
|
9
10
|
ChatToolCall,
|
|
10
11
|
ChatTransport
|
|
@@ -44,6 +45,11 @@ export interface AiChatProps {
|
|
|
44
45
|
maxAttachmentSize?: number
|
|
45
46
|
/** 透传给内部 MarkdownRender 的属性 */
|
|
46
47
|
rendererProps?: Record<string, unknown>
|
|
48
|
+
/**
|
|
49
|
+
* 是否展示 token 用量明细(缓存命中 / 未命中;缺字段不显示)。
|
|
50
|
+
* 默认 false:仅在拿到 usage 时显示会话累计「总 token」。接口未返回 usage 时不展示。
|
|
51
|
+
*/
|
|
52
|
+
tokenUsageDetail?: boolean
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
export interface AiChatEmits {
|
|
@@ -70,8 +76,12 @@ export interface _AiChatExposed {
|
|
|
70
76
|
abort: () => void
|
|
71
77
|
/** 重新生成最后一条 assistant 回复 */
|
|
72
78
|
regenerate: () => void
|
|
73
|
-
/**
|
|
79
|
+
/** 清空消息、待发送队列与 token 统计(生成中会先中止) */
|
|
74
80
|
clear: () => void
|
|
81
|
+
/** 当前会话累计 token(从未收到 usage 时为 null) */
|
|
82
|
+
tokenUsage: Ref<ChatTokenUsage | null>
|
|
83
|
+
/** 最近一轮用户对话的 token(含工具多轮请求;该轮无 usage 时为 null) */
|
|
84
|
+
lastTurnUsage: Ref<ChatTokenUsage | null>
|
|
75
85
|
/** 待发送消息队列(会话进行中提交的消息按序排队,收尾后 FIFO 自动接续) */
|
|
76
86
|
queue: Ref<ChatQueuedMessage[]>
|
|
77
87
|
/** 立即执行队列中的某条:中断当前会话并插队为下一条 */
|