@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.
Files changed (34) hide show
  1. package/dist/chat/index.d.ts +2 -2
  2. package/dist/chat/transports/openai.js +52 -6
  3. package/dist/chat/transports/openai.js.map +1 -1
  4. package/dist/chat/types.d.ts +31 -1
  5. package/dist/chat/use-chat.d.ts +31 -1
  6. package/dist/chat/use-chat.js +33 -1
  7. package/dist/chat/use-chat.js.map +1 -1
  8. package/dist/components/ai-chat/ai-chat.js +17 -4
  9. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  10. package/dist/components/ai-chat/chat-input.js +75 -20
  11. package/dist/components/ai-chat/chat-input.js.map +1 -1
  12. package/dist/components/ai-chat/message-item.js +1 -6
  13. package/dist/components/ai-chat/message-item.js.map +1 -1
  14. package/dist/components/ai-chat/message-list.js +15 -0
  15. package/dist/components/ai-chat/message-list.js.map +1 -1
  16. package/dist/components/ai-chat/style.d.ts +1 -0
  17. package/dist/components/ai-chat/style.js +1 -0
  18. package/dist/components/ai-chat/style2.css +38 -11
  19. package/dist/index.d.ts +2 -2
  20. package/dist/types/ai-chat.d.ts +11 -2
  21. package/package.json +5 -5
  22. package/src/chat/__test__/use-chat.test.ts +67 -0
  23. package/src/chat/transports/__test__/openai.test.ts +140 -0
  24. package/src/chat/transports/openai.ts +73 -7
  25. package/src/chat/types.ts +31 -0
  26. package/src/chat/use-chat.ts +39 -1
  27. package/src/components/ai-chat/__test__/ai-chat.test.ts +121 -1
  28. package/src/components/ai-chat/ai-chat.vue +13 -0
  29. package/src/components/ai-chat/chat-input.vue +64 -3
  30. package/src/components/ai-chat/message-item.vue +0 -1
  31. package/src/components/ai-chat/message-list.vue +22 -0
  32. package/src/components/ai-chat/style.scss +50 -15
  33. package/src/components/ai-chat/style.ts +1 -0
  34. 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 '会话进行中,发送的消息将进入待发送队列'
@@ -50,7 +50,6 @@
50
50
  <MarkdownRender
51
51
  v-if="message.content || isStreaming"
52
52
  mode="chat"
53
- :class="cls.e('message-content')"
54
53
  :content="message.content"
55
54
  :final="!isStreaming"
56
55
  :fade="false"
@@ -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
- @include m.e(message-content) {
420
- font-size: 13px;
421
- line-height: 1.6;
422
- // 压缩 markstream 流式排版:正文字号 / 行高 / 标题档位 / 块级间距(变量见 markstream-vue 样式表)
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
- // 相邻工具卡片保持紧凑间距(覆盖 UCollapseItem 的兄弟间距)
555
- & + #{fn.bem($root-name, tool-call)} {
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-shrink: 0;
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'
@@ -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
  /** 立即执行队列中的某条:中断当前会话并插队为下一条 */