@veltra/ai 2.1.9 → 2.1.11

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 (49) hide show
  1. package/dist/chat/use-chat.d.ts +15 -0
  2. package/dist/chat/use-chat.js +6 -0
  3. package/dist/chat/use-chat.js.map +1 -1
  4. package/dist/components/ai-chat/ai-chat.js +3 -1
  5. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  6. package/dist/components/ai-chat/chat-input.js +155 -77
  7. package/dist/components/ai-chat/chat-input.js.map +1 -1
  8. package/dist/components/ai-chat/format-duration.js +13 -0
  9. package/dist/components/ai-chat/format-duration.js.map +1 -0
  10. package/dist/components/ai-chat/message-item.js +50 -31
  11. package/dist/components/ai-chat/message-item.js.map +1 -1
  12. package/dist/components/ai-chat/message-list.js +43 -8
  13. package/dist/components/ai-chat/message-list.js.map +1 -1
  14. package/dist/components/ai-chat/style.js +1 -0
  15. package/dist/components/ai-chat/style2.css +89 -18
  16. package/dist/components/ai-chat/tool-call.js +18 -11
  17. package/dist/components/ai-chat/tool-call.js.map +1 -1
  18. package/dist/index.d.ts +1 -3
  19. package/dist/index.js +1 -2
  20. package/dist/providers/types.d.ts +2 -0
  21. package/dist/style.js +0 -1
  22. package/dist/tools/ask-question/ask-question.js +2 -1
  23. package/dist/tools/ask-question/ask-question.js.map +1 -1
  24. package/dist/types/ai-chat.d.ts +2 -1
  25. package/dist/types/index.d.ts +1 -2
  26. package/package.json +1 -1
  27. package/src/chat/use-chat.ts +6 -0
  28. package/src/components/ai-chat/__test__/ai-chat.test.ts +131 -24
  29. package/src/components/ai-chat/ai-chat.vue +2 -0
  30. package/src/components/ai-chat/chat-input.vue +81 -13
  31. package/src/components/ai-chat/format-duration.ts +8 -0
  32. package/src/components/ai-chat/message-item.vue +24 -16
  33. package/src/components/ai-chat/message-list.vue +56 -5
  34. package/src/components/ai-chat/style.scss +101 -18
  35. package/src/components/ai-chat/style.ts +2 -0
  36. package/src/components/ai-chat/tool-call.vue +27 -9
  37. package/src/components/index.ts +0 -1
  38. package/src/providers/types.ts +2 -0
  39. package/src/style.ts +0 -1
  40. package/src/tools/ask-question/__test__/ask-question.test.ts +20 -2
  41. package/src/tools/ask-question/ask-question.ts +2 -1
  42. package/src/types/ai-chat.ts +2 -1
  43. package/src/types/index.ts +0 -1
  44. package/dist/components/ai-orb/style.d.ts +0 -1
  45. package/dist/components/ai-orb/style.js +0 -1
  46. package/dist/types/ai-orb.d.ts +0 -26
  47. package/src/components/ai-orb/index.ts +0 -1
  48. package/src/components/ai-orb/style.ts +0 -1
  49. /package/dist/components/ai-orb/{style2.css → style.css} +0 -0
@@ -178,12 +178,14 @@ $root-name: ai-chat;
178
178
  color: fn.use-var(text-color, assist);
179
179
  }
180
180
 
181
+ // 负边距吃掉主列横向 padding、顶到主列边缘:滚动条出现在容器右侧而非贴着会话列表;
182
+ // 800px 限宽下沉到 UScroll 内容层(__list-content)
181
183
  @include m.e(list-wrap) {
182
184
  position: relative;
183
185
  flex: 1;
184
186
  min-height: 0;
185
- width: 100%;
186
- max-width: 800px;
187
+ width: calc(100% + #{fn.use-var(gap, default)} * 2);
188
+ margin: 0 calc(#{fn.use-var(gap, default)} * -1);
187
189
  display: flex;
188
190
  flex-direction: column;
189
191
  // 纵向留出呼吸位:首条消息不贴容器顶,滚动时内容不顶边;横向由各消息自身 padding 控制
@@ -191,6 +193,16 @@ $root-name: ai-chat;
191
193
  box-sizing: border-box;
192
194
  }
193
195
 
196
+ // UScroll 内容层:左右 gap 补偿 list-wrap 负边距,宽屏内层 800px、窄屏与主列内容盒同宽,
197
+ // 消息区视觉几何与限宽在 list-wrap 时一致
198
+ @include m.e(list-content) {
199
+ width: 100%;
200
+ max-width: calc(800px + #{fn.use-var(gap, default)} * 2);
201
+ margin: 0 auto;
202
+ padding: 0 fn.use-var(gap, default);
203
+ box-sizing: border-box;
204
+ }
205
+
194
206
  @include m.e(list) {
195
207
  flex: 1;
196
208
  min-height: 0;
@@ -200,6 +212,8 @@ $root-name: ai-chat;
200
212
  @include m.e(welcome) {
201
213
  flex-shrink: 0;
202
214
  width: 100%;
215
+ max-width: 800px;
216
+ margin: 0 auto;
203
217
  height: 48px;
204
218
  @include m.flex(flex, flex-start, center);
205
219
  color: fn.use-var(text-color, assist);
@@ -303,11 +317,11 @@ $root-name: ai-chat;
303
317
  transform: translateY(-6px);
304
318
  }
305
319
 
306
- // 生成中的统一工作状态指示(列表末尾)
320
+ // 生成中的统一工作状态指示(列表末尾);横向定位由 __list-content 的 padding 提供
307
321
  @include m.e(working) {
308
322
  @include m.flex(flex, flex-start, center);
309
323
  gap: fn.use-var(gap, small);
310
- padding: 0 fn.use-var(gap, default) fn.use-var(gap, small);
324
+ padding: 0 0 fn.use-var(gap, small);
311
325
  width: fit-content;
312
326
 
313
327
  // 工作开始:立即从 0 放大(无延时),与输入框上方缩小形成对向跳
@@ -334,6 +348,13 @@ $root-name: ai-chat;
334
348
  font-size: fn.use-var(font-size-main, small);
335
349
  }
336
350
 
351
+ // 工作计时 / 收尾停留期间的总用时
352
+ @include m.e(working-time) {
353
+ font-size: fn.use-var(font-size-main, small);
354
+ color: fn.use-var(text-color, assist);
355
+ font-variant-numeric: tabular-nums;
356
+ }
357
+
337
358
  @media (prefers-reduced-motion: reduce) {
338
359
  .u-#{$root-name}__welcome.is-enter .u-ai-orb,
339
360
  .u-#{$root-name}__working .u-ai-orb {
@@ -346,21 +367,22 @@ $root-name: ai-chat;
346
367
  }
347
368
  }
348
369
 
349
- // 「最新消息」悬浮按钮:上翻浏览时出现,点击回到底部
370
+ // 回到底部悬浮按钮:上翻浏览时出现于列表下方,点击回到底部
350
371
  @include m.e(to-latest) {
351
372
  position: absolute;
352
- top: fn.use-var(gap, small);
373
+ bottom: fn.use-var(gap, small);
353
374
  left: 50%;
354
375
  transform: translateX(-50%);
355
376
  z-index: 1;
377
+ width: 28px;
378
+ height: 28px;
379
+ padding: 0;
356
380
  @include m.flex(flex, center, center);
357
- gap: 4px;
358
- padding: 4px 12px;
359
381
  border: none;
360
- border-radius: 999px;
382
+ border-radius: 50%;
361
383
  background-color: fn.use-var(bg-color, top);
362
384
  color: fn.use-var(text-color, second);
363
- font-size: fn.use-var(font-size-main, small);
385
+ font-size: 14px;
364
386
  box-shadow: 0 2px 8px fn.color-a(shadow, 20, color);
365
387
  cursor: pointer;
366
388
  transition: color 0.15s ease;
@@ -384,12 +406,12 @@ $root-name: ai-chat;
384
406
 
385
407
  @include m.em(to-latest, 'fade-enter-from') {
386
408
  opacity: 0;
387
- transform: translateX(-50%) translateY(-4px);
409
+ transform: translateX(-50%) translateY(4px);
388
410
  }
389
411
 
390
412
  @include m.em(to-latest, 'fade-leave-to') {
391
413
  opacity: 0;
392
- transform: translateX(-50%) translateY(-4px);
414
+ transform: translateX(-50%) translateY(4px);
393
415
  }
394
416
 
395
417
  @include m.e(message) {
@@ -479,6 +501,14 @@ $root-name: ai-chat;
479
501
  gap: fn.use-var(gap, small);
480
502
  }
481
503
 
504
+ // 答案操作区前的本轮总用时
505
+ @include m.e(message-duration) {
506
+ align-self: center;
507
+ color: fn.use-var(text-color, assist);
508
+ font-size: fn.use-var(font-size-main, small);
509
+ font-variant-numeric: tabular-nums;
510
+ }
511
+
482
512
  // 思考过程
483
513
  @include m.e(reasoning) {
484
514
  margin-bottom: 4px;
@@ -500,9 +530,19 @@ $root-name: ai-chat;
500
530
  }
501
531
 
502
532
  @include m.e(reasoning-title) {
533
+ flex-shrink: 0;
503
534
  line-height: 1.5;
504
535
  }
505
536
 
537
+ // 折叠态头部右侧滚动展示的最新一行思考
538
+ @include m.e(reasoning-line) {
539
+ flex: 1;
540
+ min-width: 0;
541
+ @include m.ellipsis;
542
+ line-height: 1.5;
543
+ color: fn.use-var(text-color, assist);
544
+ }
545
+
506
546
  @include m.e(reasoning-chevron) {
507
547
  font-size: 12px;
508
548
  transition: transform 0.2s;
@@ -1140,12 +1180,32 @@ $root-name: ai-chat;
1140
1180
  }
1141
1181
  }
1142
1182
 
1143
- @include m.e(input-usage) {
1144
- min-width: 0;
1145
- @include m.ellipsis;
1146
- font-size: fn.use-var(font-size-main, small);
1147
- color: fn.use-var(text-color, assist);
1148
- line-height: 1.4;
1183
+ @include m.e(usage-trigger) {
1184
+ @include m.flex(inline-flex, center, center);
1185
+ flex-shrink: 0;
1186
+ padding: 2px;
1187
+ border: none;
1188
+ border-radius: 50%;
1189
+ background: transparent;
1190
+ }
1191
+
1192
+ @include m.e(usage-ring) {
1193
+ display: block;
1194
+ width: 14px;
1195
+ height: 14px;
1196
+ }
1197
+
1198
+ @include m.e(usage-ring-track) {
1199
+ fill: none;
1200
+ stroke: fn.use-var(border-color);
1201
+ stroke-width: 5;
1202
+ }
1203
+
1204
+ @include m.e(usage-ring-value) {
1205
+ fill: none;
1206
+ stroke: fn.use-var(color, primary);
1207
+ stroke-width: 5;
1208
+ transition: stroke-dasharray 0.3s ease;
1149
1209
  }
1150
1210
 
1151
1211
  @include m.e(input-stop) {
@@ -1190,6 +1250,29 @@ $root-name: ai-chat;
1190
1250
  }
1191
1251
  }
1192
1252
 
1253
+ // 用量明细面板经 teleport 挂载到 popper 容器,不处于 .u-ai-chat 内,需独立定义
1254
+ .u-#{$root-name}__usage-panel {
1255
+ padding: 6px 4px;
1256
+ user-select: none;
1257
+
1258
+ .u-#{$root-name}__usage-row {
1259
+ @include m.flex(flex, space-between, center);
1260
+ gap: 24px;
1261
+ padding: 3px 8px;
1262
+ font-size: fn.use-var(font-size-main, small);
1263
+ line-height: 1.6;
1264
+ }
1265
+
1266
+ .u-#{$root-name}__usage-label {
1267
+ color: fn.use-var(text-color, assist);
1268
+ }
1269
+
1270
+ .u-#{$root-name}__usage-value {
1271
+ color: fn.use-var(text-color, main);
1272
+ font-variant-numeric: tabular-nums;
1273
+ }
1274
+ }
1275
+
1193
1276
  // 模型选择面板经 teleport 挂载到 popper 容器,不处于 .u-ai-chat 内,需独立定义
1194
1277
  .u-#{$root-name}__model-panel {
1195
1278
  padding: 4px;
@@ -10,4 +10,6 @@ import '@veltra/desktop/components/scroll/style'
10
10
  import '@veltra/desktop/components/select/style'
11
11
  import '@veltra/styles/animations/shine.scss'
12
12
  import 'markstream-vue/index.css'
13
+ // 活体球为 ai-chat 内部组件,样式随 ai-chat 一并加载
14
+ import '../ai-orb/style.scss'
13
15
  import './style.scss'
@@ -95,19 +95,39 @@ const markUserToggled = () => {
95
95
  /** 面板工具:render 展示在右侧侧边面板,卡片 body 仅保留「查看面板」入口 */
96
96
  const isPanelTool = computed(() => tool.value?.renderTo === 'panel')
97
97
 
98
- /** 完成后是否自动折叠:缺省面板工具折叠;有 render 时不折叠,否则折叠 */
99
- const autoCollapse = computed(
100
- () => tool.value?.autoCollapse ?? (isPanelTool.value || !tool.value?.render)
98
+ /** 「UI 即答复」的终结工具(session 元信息无 terminal,恒为 false) */
99
+ const isAnswerTool = computed(() => {
100
+ const t = tool.value
101
+ return !!(t && 'terminal' in t && t.terminal)
102
+ })
103
+
104
+ /** 内联自定义 body(render / tool-<name> 插槽):进行中可能是提问表单等交互 UI,需展开可见 */
105
+ const hasInlineBody = computed(
106
+ () => !isPanelTool.value && !!(tool.value?.render || di?.slots[`tool-${props.toolCall.name}`])
101
107
  )
102
108
 
109
+ /** 完成后是否自动折叠:缺省一律折叠;「UI 即答复」的终结工具缺省保持展开 */
110
+ const autoCollapse = computed(() => tool.value?.autoCollapse ?? !isAnswerTool.value)
111
+
112
+ /** 需要用户交互/展示的进行中状态:待确认(看参数再决策)、有内联自定义 body(提问表单等) */
113
+ const needsInteraction = computed(() => {
114
+ if (props.toolCall.status === 'awaiting-confirm') return true
115
+ const active = props.toolCall.status === 'pending' || props.toolCall.status === 'running'
116
+ return active && hasInlineBody.value
117
+ })
118
+
119
+ const isSettledStatus = (status: ChatToolCall['status']) =>
120
+ status === 'success' || status === 'error' || status === 'rejected'
121
+
122
+ // 默认折叠(含进行中);仅交互/答复类自动展开,用户手动切换过后不再自动变化
103
123
  watch(
104
124
  () => props.toolCall.status,
105
125
  (status) => {
106
126
  if (userToggled) return
107
- if (status === 'pending' || status === 'running' || status === 'awaiting-confirm') {
108
- isExpanded.value = true
109
- } else {
127
+ if (isSettledStatus(status)) {
110
128
  isExpanded.value = !autoCollapse.value
129
+ } else {
130
+ isExpanded.value = needsInteraction.value
111
131
  }
112
132
  },
113
133
  { immediate: true }
@@ -130,9 +150,7 @@ const isActive = computed(() => {
130
150
  })
131
151
 
132
152
  /** 终态(成功/失败/拒绝) */
133
- const isSettled = computed(() => {
134
- return ['success', 'error', 'rejected'].includes(props.toolCall.status)
135
- })
153
+ const isSettled = computed(() => isSettledStatus(props.toolCall.status))
136
154
 
137
155
  /**
138
156
  * 折叠后卸载内容 DOM:仅终态且非面板工具时启用——
@@ -1,2 +1 @@
1
1
  export * from './ai-chat'
2
- export * from './ai-orb'
@@ -18,6 +18,8 @@ export interface ChatModel {
18
18
  reasoningLevels?: ChatReasoningLevel[]
19
19
  /** 默认推理等级(须落在 reasoningLevels 内) */
20
20
  defaultReasoningLevel?: string
21
+ /** 上下文窗口上限(token)。配置后输入栏用量环按「最近一次请求总 token / 上限」显示占比 */
22
+ contextWindow?: number
21
23
  }
22
24
 
23
25
  /** 模型服务商配置(内嵌 models) */
package/src/style.ts CHANGED
@@ -1,2 +1 @@
1
1
  import './components/ai-chat/style'
2
- import './components/ai-orb/style'
@@ -76,6 +76,15 @@ async function expandProcess(host: HTMLElement) {
76
76
  await nextTick()
77
77
  }
78
78
 
79
+ /** 工具卡片终态默认折叠(destroyOnCollapse 卸载内容 DOM):点头部展开后内容才挂载 */
80
+ async function expandToolCard(host: HTMLElement) {
81
+ const header = host.querySelector<HTMLElement>('.u-ai-chat__tool-call .u-collapse__header')!
82
+ expect(header, '工具卡片头部应存在').toBeTruthy()
83
+ click(header)
84
+ await nextTick()
85
+ await nextTick()
86
+ }
87
+
79
88
  /** 底部导航按钮(UButton disabled 体现为 is-disabled class) */
80
89
  const actionButton = (host: HTMLElement, text: string) => {
81
90
  return [...host.querySelectorAll<HTMLElement>('.u-ai-chat__ask-question-actions button')].find(
@@ -169,10 +178,13 @@ describe('内置提问工具', () => {
169
178
  ]
170
179
  })
171
180
 
172
- // 第二轮文本输出后,提问工具卡片收进「已完成」过程块,先展开
181
+ // 第二轮文本输出后,提问工具卡片收进「已完成」过程块,先展开过程块
173
182
  await expandProcess(host)
174
183
 
175
- // 问答摘要视图:命中选项的回答渲染为 chip,完成后保持展开
184
+ // 工具卡片终态默认折叠且卸载内容 DOM,手动展开后才挂载摘要视图
185
+ await expandToolCard(host)
186
+
187
+ // 问答摘要视图:命中选项的回答渲染为 chip
176
188
  expect(host.querySelectorAll('.u-ai-chat__ask-question-result').length).toBe(3)
177
189
  const summary = host.querySelector('.u-ai-chat__ask-question')!.textContent!
178
190
  expect(summary).toContain('目标用户是谁?')
@@ -200,6 +212,12 @@ describe('内置提问工具', () => {
200
212
 
201
213
  chat.value?.abort()
202
214
 
215
+ // 中止后调用进入终态:卡片默认折叠并卸载内容 DOM,展开后显示状态
216
+ await vi.waitFor(() => {
217
+ expect(host.querySelector('.u-ai-chat__tool-call.is-error')).toBeTruthy()
218
+ })
219
+ await expandToolCard(host)
220
+
203
221
  await vi.waitFor(() => {
204
222
  expect(host.querySelector('.u-ai-chat__ask-question-status')?.textContent).toBe('已取消')
205
223
  })
@@ -126,7 +126,8 @@ const AskQuestionRender = defineComponent({
126
126
 
127
127
  /**
128
128
  * 创建内置提问工具:execute 挂起等待用户在 UI 中作答,提交后结果回灌模型。
129
- * 工具卡片由内联提问表单渲染(多题分页导航,提交后展示问答摘要,不自动折叠)。
129
+ * 工具卡片由内联提问表单渲染(多题分页导航,提交后展示问答摘要);
130
+ * 进行中因有内联 render 自动展开,终态默认折叠,展开后可查看问答摘要。
130
131
  */
131
132
  export function createAskQuestionTool(
132
133
  options: CreateAskQuestionToolOptions = {}
@@ -52,7 +52,8 @@ export interface AiChatProps {
52
52
  rendererProps?: Record<string, unknown>
53
53
  /**
54
54
  * 是否展示 token 用量明细(缓存命中 / 未命中;缺字段不显示)。
55
- * 默认 false:仅在拿到 usage 时显示会话累计「总 token」。接口未返回 usage 时不展示。
55
+ * 默认 false:用量环明细面板只列 总 / 输入 / 输出;true 时追加有数据的缓存项。
56
+ * 接口未返回 usage 时不展示用量环。
56
57
  */
57
58
  tokenUsageDetail?: boolean
58
59
  /**
@@ -1,2 +1 @@
1
1
  export * from './ai-chat'
2
- export * from './ai-orb'
@@ -1 +0,0 @@
1
- export {}
@@ -1 +0,0 @@
1
- import "./style2.css";
@@ -1,26 +0,0 @@
1
- //#region src/types/ai-orb.d.ts
2
- /** 活体球生命状态:驱动常态动画(平静 / 思考 / 输出) */
3
- export type AiOrbStatus = 'idle' | 'thinking' | 'speaking';
4
- /**
5
- * 瞬时表情:播放约 1-2s 后自动回到 status 驱动的常态。
6
- * - happy:成功 / 回答完毕 —— 先睁大眼睛,再弯眼大笑 + 点头
7
- * - shock:惊讶 —— 睁大眼睛 + 小嘴微张 + 轻微后仰
8
- * - frustrated:受挫(如工具调用失败)—— 闭紧眼睛 + 摇头 + 嘴角下撇
9
- */
10
- export type AiOrbReaction = 'happy' | 'shock' | 'frustrated';
11
- export interface AiOrbProps {
12
- /** 球体直径(px),默认 48 */
13
- size?: number;
14
- /** 生命状态,默认 idle */
15
- status?: AiOrbStatus;
16
- }
17
- export interface AiOrbEmits {
18
- /** 点击球体(球体同时会做 Q 弹反馈) */
19
- (e: 'click'): void;
20
- }
21
- export interface AiOrbExposed {
22
- /** 播放一次瞬时表情(对应阶段性事件,如回答完毕 / 工具调用失败) */
23
- react: (reaction: AiOrbReaction) => void;
24
- }
25
- //#endregion
26
- //# sourceMappingURL=ai-orb.d.ts.map
@@ -1 +0,0 @@
1
- export { default as UAiOrb } from './ai-orb.vue'
@@ -1 +0,0 @@
1
- import './style.scss'