@veltra/ai 2.1.3 → 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-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 +28 -1
- package/dist/index.d.ts +2 -2
- package/dist/types/ai-chat.d.ts +11 -2
- package/package.json +1 -1
- 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-list.vue +22 -0
- package/src/components/ai-chat/style.scss +31 -1
- 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,
|
|
@@ -906,7 +906,8 @@ $root-name: ai-chat;
|
|
|
906
906
|
@include m.e(input-toolbar-left) {
|
|
907
907
|
@include m.flex(flex, flex-start, center);
|
|
908
908
|
gap: fn.use-var(gap, small);
|
|
909
|
-
flex
|
|
909
|
+
flex: 1;
|
|
910
|
+
min-width: 0;
|
|
910
911
|
}
|
|
911
912
|
|
|
912
913
|
@include m.e(input-toolbar-right) {
|
|
@@ -960,6 +961,35 @@ $root-name: ai-chat;
|
|
|
960
961
|
}
|
|
961
962
|
}
|
|
962
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
|
+
|
|
963
993
|
@include m.e(input-stop) {
|
|
964
994
|
display: block;
|
|
965
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
|
/** 立即执行队列中的某条:中断当前会话并插队为下一条 */
|