@veltra/ai 2.1.0 → 2.1.2
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/transports/openai.js +18 -17
- package/dist/chat/transports/openai.js.map +1 -1
- package/dist/chat/use-chat.js +8 -2
- package/dist/chat/use-chat.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js +1 -1
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/message-item.js +1 -1
- package/dist/components/ai-chat/message-item.js.map +1 -1
- package/dist/components/ai-chat/message-list.js +164 -65
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/style2.css +101 -21
- package/dist/components/ai-chat/tool-call.js +2 -6
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/components/ai-chat/turn-process.js +56 -0
- package/dist/components/ai-chat/turn-process.js.map +1 -0
- package/dist/components/ai-orb/orb-renderer.js +88 -8
- package/dist/components/ai-orb/orb-renderer.js.map +1 -1
- package/dist/types/ai-chat.d.ts +1 -1
- package/package.json +15 -6
- package/src/chat/__test__/use-chat.test.ts +42 -4
- package/src/chat/use-chat.ts +8 -2
- package/src/components/ai-chat/__test__/ai-chat.test.ts +176 -36
- package/src/components/ai-chat/ai-chat.vue +2 -2
- package/src/components/ai-chat/message-item.vue +3 -2
- package/src/components/ai-chat/message-list.vue +186 -66
- package/src/components/ai-chat/style.scss +130 -27
- package/src/components/ai-chat/tool-call.vue +1 -10
- package/src/components/ai-chat/turn-process.vue +47 -0
- package/src/components/ai-orb/__test__/ai-orb.test.ts +28 -3
- package/src/components/ai-orb/orb-renderer.ts +113 -5
- package/src/tools/ask-question/__test__/ask-question.test.ts +13 -3
- package/src/types/ai-chat.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest'
|
|
2
2
|
import { createApp, defineComponent, h, nextTick, ref, type PropType } from 'vue'
|
|
3
3
|
|
|
4
|
-
import type { ChatTool, ChatToolCall, ChatTransport } from '../../../chat/types'
|
|
4
|
+
import type { ChatMessage, ChatTool, ChatToolCall, ChatTransport } from '../../../chat/types'
|
|
5
5
|
import type { ChatModelOption } from '../../../providers'
|
|
6
6
|
import type { AiChatExposed } from '../../../types'
|
|
7
7
|
import UAiChat from '../ai-chat.vue'
|
|
@@ -60,13 +60,23 @@ function mountAiChat(options: {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/** 最终答案输出后过程消息收进「已完成」折叠块:点击展开并等待过程 DOM 挂载 */
|
|
64
|
+
async function expandProcess(host: HTMLElement) {
|
|
65
|
+
await vi.waitFor(() => {
|
|
66
|
+
expect(host.querySelector('.u-ai-chat__process-header')).toBeTruthy()
|
|
67
|
+
})
|
|
68
|
+
host.querySelector<HTMLElement>('.u-ai-chat__process-header')!.click()
|
|
69
|
+
await nextTick()
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
describe('UAiChat', () => {
|
|
64
73
|
it('空状态显示欢迎语', () => {
|
|
65
74
|
const { host, unmount } = mountAiChat({ transport: () => {}, welcome: '有什么可以帮你?' })
|
|
66
75
|
|
|
67
76
|
expect(host.querySelector('.u-ai-chat__welcome')?.textContent).toContain('有什么可以帮你?')
|
|
68
77
|
const orb = host.querySelector('.u-ai-chat__welcome canvas.u-ai-orb') as HTMLElement | null
|
|
69
|
-
expect(orb?.style.width).toBe('
|
|
78
|
+
expect(orb?.style.width).toBe('48px')
|
|
79
|
+
expect(host.querySelector('.u-ai-chat__list .u-ai-chat__welcome')).toBeFalsy()
|
|
70
80
|
unmount()
|
|
71
81
|
})
|
|
72
82
|
|
|
@@ -136,18 +146,17 @@ describe('UAiChat', () => {
|
|
|
136
146
|
|
|
137
147
|
chat.value?.send('1+1 等于几')
|
|
138
148
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
})
|
|
149
|
+
// 最终答案输出后,工具卡片收进「已完成」过程块,先展开
|
|
150
|
+
await expandProcess(host)
|
|
142
151
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
expect(host.querySelector('.u-ai-chat__tool-call.is-success')).toBeTruthy()
|
|
146
|
-
})
|
|
152
|
+
expect(host.querySelector('.u-ai-chat__tool-call-name')?.textContent).toBe('calculate')
|
|
153
|
+
expect(host.querySelector('.u-ai-chat__tool-call.is-success')).toBeTruthy()
|
|
147
154
|
|
|
155
|
+
// 执行完成后自动折叠,点击头部展开
|
|
148
156
|
const header = host.querySelector<HTMLElement>('.u-ai-chat__tool-call .u-collapse__header')!
|
|
149
157
|
header.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
150
158
|
await nextTick()
|
|
159
|
+
await nextTick()
|
|
151
160
|
|
|
152
161
|
const body = host.querySelector('.u-ai-chat__tool-call .u-collapse__content-wrapper')
|
|
153
162
|
expect(body?.textContent).toContain('"expression": "1+1"')
|
|
@@ -210,10 +219,11 @@ describe('UAiChat', () => {
|
|
|
210
219
|
|
|
211
220
|
chat.value?.send('搜一下 vue')
|
|
212
221
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
222
|
+
// 最终答案输出后工具卡片收进「已完成」过程块,展开后断言
|
|
223
|
+
await expandProcess(host)
|
|
224
|
+
|
|
225
|
+
expect(host.querySelector('.u-ai-chat__tool-call-name')?.textContent).toBe('搜索')
|
|
226
|
+
expect(host.querySelector('.custom-tool-icon')).toBeTruthy()
|
|
217
227
|
unmount()
|
|
218
228
|
})
|
|
219
229
|
|
|
@@ -254,10 +264,8 @@ describe('UAiChat', () => {
|
|
|
254
264
|
|
|
255
265
|
chat.value?.send('北京天气')
|
|
256
266
|
|
|
257
|
-
//
|
|
258
|
-
await
|
|
259
|
-
expect(host.querySelector('.weather-view')).toBeTruthy()
|
|
260
|
-
})
|
|
267
|
+
// 最终答案输出后工具卡片收进「已完成」过程块,展开后 render 保持展开态
|
|
268
|
+
await expandProcess(host)
|
|
261
269
|
|
|
262
270
|
// 完成后保持展开,默认参数/结果区被替换
|
|
263
271
|
await vi.waitFor(() => {
|
|
@@ -291,6 +299,8 @@ describe('UAiChat', () => {
|
|
|
291
299
|
|
|
292
300
|
chat.value?.send('1+1')
|
|
293
301
|
|
|
302
|
+
await expandProcess(host)
|
|
303
|
+
|
|
294
304
|
await vi.waitFor(() => {
|
|
295
305
|
expect(host.querySelector('.u-ai-chat__tool-call.is-success.is-active')).toBeTruthy()
|
|
296
306
|
})
|
|
@@ -306,10 +316,9 @@ describe('UAiChat', () => {
|
|
|
306
316
|
|
|
307
317
|
chat.value?.send('北京天气')
|
|
308
318
|
|
|
309
|
-
await
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
await nextTick()
|
|
319
|
+
await expandProcess(host)
|
|
320
|
+
|
|
321
|
+
expect(host.querySelector('.u-ai-chat__tool-call.is-success')).toBeTruthy()
|
|
313
322
|
expect(host.querySelector('.u-ai-chat__tool-call.is-active')).toBeFalsy()
|
|
314
323
|
const body = host.querySelector('.u-ai-chat__tool-call .u-collapse__content-wrapper')
|
|
315
324
|
expect(body?.getAttribute('aria-hidden')).toBe('true')
|
|
@@ -333,10 +342,8 @@ describe('UAiChat', () => {
|
|
|
333
342
|
|
|
334
343
|
chat.value?.send('1+1 等于几')
|
|
335
344
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
})
|
|
339
|
-
await nextTick()
|
|
345
|
+
// 过程块展开后,工具卡片重挂载为完成态(自动折叠)
|
|
346
|
+
await expandProcess(host)
|
|
340
347
|
|
|
341
348
|
// 完成后自动折叠:内容 DOM 已卸载(减少内存与渲染成本),包装容器保留
|
|
342
349
|
expect(host.querySelector('.u-ai-chat__tool-call.is-active')).toBeFalsy()
|
|
@@ -380,6 +387,63 @@ describe('UAiChat', () => {
|
|
|
380
387
|
unmount()
|
|
381
388
|
})
|
|
382
389
|
|
|
390
|
+
it('最终答案输出后过程消息收进「已完成」折叠块,可展开钻取', async () => {
|
|
391
|
+
let round = 0
|
|
392
|
+
const transport: ChatTransport = (_req, handlers) => {
|
|
393
|
+
round++
|
|
394
|
+
if (round === 1) {
|
|
395
|
+
handlers.onReasoningDelta?.('先分析一下')
|
|
396
|
+
handlers.onToolCall?.({ id: 'c1', name: 'calculate', arguments: '{"expression":"1+1"}' })
|
|
397
|
+
} else {
|
|
398
|
+
handlers.onTextDelta('答案是 2')
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const tools: ChatTool[] = [
|
|
402
|
+
{ name: 'calculate', description: '计算', parameters: {}, execute: () => ({ value: 2 }) }
|
|
403
|
+
]
|
|
404
|
+
const { host, chat, unmount } = mountAiChat({ transport, tools })
|
|
405
|
+
|
|
406
|
+
chat.value?.send('1+1 等于几')
|
|
407
|
+
|
|
408
|
+
// 最终答案输出后:出现「已完成」折叠头,过程 DOM(思考块/工具卡片)卸载,答案仍在块外
|
|
409
|
+
await vi.waitFor(() => {
|
|
410
|
+
expect(host.querySelector('.u-ai-chat__process-title')?.textContent).toBe('已完成')
|
|
411
|
+
})
|
|
412
|
+
expect(host.querySelector('.u-ai-chat__process-content')).toBeFalsy()
|
|
413
|
+
expect(host.querySelector('.u-ai-chat__tool-call')).toBeFalsy()
|
|
414
|
+
expect(host.querySelector('.u-ai-chat__reasoning')).toBeFalsy()
|
|
415
|
+
expect(host.querySelector('.md-stub')?.textContent).toContain('答案是 2')
|
|
416
|
+
|
|
417
|
+
// 点击展开:过程消息可见,思考块默认折叠(可逐层钻取)
|
|
418
|
+
host.querySelector<HTMLElement>('.u-ai-chat__process-header')!.click()
|
|
419
|
+
await nextTick()
|
|
420
|
+
expect(host.querySelector('.u-ai-chat__reasoning-title')?.textContent).toContain('思考过程')
|
|
421
|
+
expect(host.querySelector('.u-ai-chat__reasoning-content')).toBeFalsy()
|
|
422
|
+
expect(host.querySelector('.u-ai-chat__tool-call-name')?.textContent).toBe('calculate')
|
|
423
|
+
|
|
424
|
+
// 再次收起:过程 DOM 重新卸载
|
|
425
|
+
host.querySelector<HTMLElement>('.u-ai-chat__process-header')!.click()
|
|
426
|
+
await nextTick()
|
|
427
|
+
expect(host.querySelector('.u-ai-chat__process-content')).toBeFalsy()
|
|
428
|
+
expect(host.querySelector('.u-ai-chat__tool-call')).toBeFalsy()
|
|
429
|
+
unmount()
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('单轮无工具对话不出现「已完成」过程块', async () => {
|
|
433
|
+
const transport: ChatTransport = (_req, handlers) => {
|
|
434
|
+
handlers.onReasoningDelta?.('想一下')
|
|
435
|
+
handlers.onTextDelta('你好')
|
|
436
|
+
}
|
|
437
|
+
const { host, chat, unmount } = mountAiChat({ transport })
|
|
438
|
+
|
|
439
|
+
chat.value?.send('hi')
|
|
440
|
+
await vi.waitFor(() => {
|
|
441
|
+
expect(host.querySelector('.md-stub')?.textContent).toContain('你好')
|
|
442
|
+
})
|
|
443
|
+
expect(host.querySelector('.u-ai-chat__process')).toBeFalsy()
|
|
444
|
+
unmount()
|
|
445
|
+
})
|
|
446
|
+
|
|
383
447
|
it('流式期间用户上滚取消吸底,展示「最新消息」入口且不被后续输出重新吸附', async () => {
|
|
384
448
|
const transport: ChatTransport = async (_req, handlers) => {
|
|
385
449
|
handlers.onTextDelta('第一段')
|
|
@@ -418,9 +482,9 @@ describe('UAiChat', () => {
|
|
|
418
482
|
|
|
419
483
|
chat.value?.send('北京天气')
|
|
420
484
|
|
|
421
|
-
await
|
|
422
|
-
|
|
423
|
-
|
|
485
|
+
await expandProcess(host)
|
|
486
|
+
|
|
487
|
+
expect(host.querySelector('.u-ai-chat__tool-call.is-success')).toBeTruthy()
|
|
424
488
|
expect(host.querySelector('.weather-view')).toBeTruthy()
|
|
425
489
|
expect(host.querySelector('.slot-view')).toBeFalsy()
|
|
426
490
|
unmount()
|
|
@@ -473,10 +537,11 @@ describe('UAiChat', () => {
|
|
|
473
537
|
})
|
|
474
538
|
expect(host.querySelector('.u-ai-chat__panel-title')?.textContent).toBe('后台页面')
|
|
475
539
|
|
|
540
|
+
// 最终答案输出后工具卡片收进「已完成」过程块,展开后断言
|
|
541
|
+
await expandProcess(host)
|
|
542
|
+
|
|
476
543
|
// 卡片 body 不渲染 render 组件,仅提供「查看面板」入口
|
|
477
|
-
|
|
478
|
-
expect(host.querySelector('.u-ai-chat__tool-call.is-success')).toBeTruthy()
|
|
479
|
-
})
|
|
544
|
+
expect(host.querySelector('.u-ai-chat__tool-call.is-success')).toBeTruthy()
|
|
480
545
|
expect(host.querySelector('.u-ai-chat__tool-call .page-view')).toBeFalsy()
|
|
481
546
|
expect(host.querySelector('.u-ai-chat__tool-call-panel-entry')).toBeTruthy()
|
|
482
547
|
unmount()
|
|
@@ -495,6 +560,9 @@ describe('UAiChat', () => {
|
|
|
495
560
|
await nextTick()
|
|
496
561
|
expect(host.querySelector('.u-ai-chat__panel')).toBeFalsy()
|
|
497
562
|
|
|
563
|
+
// 卡片已收进「已完成」过程块,展开后点击入口
|
|
564
|
+
await expandProcess(host)
|
|
565
|
+
|
|
498
566
|
const entryBtn = [
|
|
499
567
|
...host.querySelectorAll<HTMLElement>('.u-ai-chat__tool-call-panel-entry button')
|
|
500
568
|
].find((btn) => btn.textContent?.includes('查看面板'))!
|
|
@@ -519,7 +587,9 @@ describe('UAiChat', () => {
|
|
|
519
587
|
expect(host.querySelector('.u-ai-chat__panel .page-view')?.textContent).toContain('list')
|
|
520
588
|
})
|
|
521
589
|
|
|
522
|
-
//
|
|
590
|
+
// 卡片已收进「已完成」过程块,展开后点击第一张卡片的「查看面板」切回 form
|
|
591
|
+
await expandProcess(host)
|
|
592
|
+
|
|
523
593
|
const firstCard = host.querySelectorAll<HTMLElement>('.u-ai-chat__tool-call')[0]!
|
|
524
594
|
const entryBtn = [
|
|
525
595
|
...firstCard.querySelectorAll<HTMLElement>('.u-ai-chat__tool-call-panel-entry button')
|
|
@@ -604,7 +674,9 @@ describe('UAiChat', () => {
|
|
|
604
674
|
const layout = host.querySelector<HTMLElement>('.u-ai-chat')!
|
|
605
675
|
expect(layout.style.gridTemplateColumns).toBe('1fr 640px')
|
|
606
676
|
|
|
607
|
-
//
|
|
677
|
+
// 卡片已收进「已完成」过程块,展开后点击 openPage 卡片的「查看面板」→ 切换并应用 480px
|
|
678
|
+
await expandProcess(host)
|
|
679
|
+
|
|
608
680
|
const firstCard = host.querySelectorAll<HTMLElement>('.u-ai-chat__tool-call')[0]!
|
|
609
681
|
const entryBtn = [
|
|
610
682
|
...firstCard.querySelectorAll<HTMLElement>('.u-ai-chat__tool-call-panel-entry button')
|
|
@@ -624,6 +696,45 @@ describe('UAiChat', () => {
|
|
|
624
696
|
unmount()
|
|
625
697
|
})
|
|
626
698
|
|
|
699
|
+
it('受控 v-model:messages 绑定下流式回复正常渲染', async () => {
|
|
700
|
+
const messages = ref<ChatMessage[]>([])
|
|
701
|
+
const chatRef = ref<AiChatExposed>()
|
|
702
|
+
const transport: ChatTransport = async (_req, handlers) => {
|
|
703
|
+
// 留出父级 patch props 的时间窗,还原真实的回显时序
|
|
704
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
705
|
+
handlers.onTextDelta('受控回答')
|
|
706
|
+
}
|
|
707
|
+
const host = document.createElement('div')
|
|
708
|
+
document.body.appendChild(host)
|
|
709
|
+
|
|
710
|
+
const app = createApp({
|
|
711
|
+
setup() {
|
|
712
|
+
return () =>
|
|
713
|
+
h(UAiChat, {
|
|
714
|
+
ref: chatRef,
|
|
715
|
+
transport,
|
|
716
|
+
messages: messages.value,
|
|
717
|
+
'onUpdate:messages': (value: ChatMessage[]) => {
|
|
718
|
+
messages.value = value
|
|
719
|
+
}
|
|
720
|
+
})
|
|
721
|
+
}
|
|
722
|
+
})
|
|
723
|
+
app.mount(host)
|
|
724
|
+
|
|
725
|
+
chatRef.value?.send('你好')
|
|
726
|
+
await vi.waitFor(() => {
|
|
727
|
+
expect(host.querySelector('.md-stub')?.textContent).toContain('受控回答')
|
|
728
|
+
})
|
|
729
|
+
// 父级持有的快照最终也包含 assistant 回复
|
|
730
|
+
expect(messages.value.some((m) => m.role === 'assistant' && m.content === '受控回答')).toBe(
|
|
731
|
+
true
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
app.unmount()
|
|
735
|
+
host.remove()
|
|
736
|
+
})
|
|
737
|
+
|
|
627
738
|
it('生成中空输入显示停止,有内容则显示入队发送', async () => {
|
|
628
739
|
const transport: ChatTransport = (req) => {
|
|
629
740
|
return new Promise<void>((resolve) => {
|
|
@@ -863,11 +974,12 @@ describe('UAiChat', () => {
|
|
|
863
974
|
expect(host.querySelector('.u-ai-chat__welcome-item')?.textContent).toBe('第二条建议')
|
|
864
975
|
})
|
|
865
976
|
|
|
866
|
-
// 点击文案 →
|
|
977
|
+
// 点击文案 → 以当前展示的文案发送;空闲欢迎立即缩小离开,列表末尾出现工作球
|
|
867
978
|
host.querySelector<HTMLElement>('.u-ai-chat__welcome-item')!.click()
|
|
868
979
|
await nextTick()
|
|
869
980
|
|
|
870
|
-
expect(host.querySelector('.u-ai-chat__welcome')).
|
|
981
|
+
expect(host.querySelector('.u-ai-chat__welcome')?.classList.contains('is-leaving')).toBe(true)
|
|
982
|
+
expect(host.querySelector('.u-ai-chat__list .u-ai-chat__working')).toBeTruthy()
|
|
871
983
|
const userBubble = host.querySelector('.u-ai-chat__message--user .u-ai-chat__message-bubble')
|
|
872
984
|
expect(userBubble?.textContent).toContain('第二条建议')
|
|
873
985
|
unmount()
|
|
@@ -906,11 +1018,14 @@ describe('UAiChat', () => {
|
|
|
906
1018
|
const { host, chat, unmount } = mountAiChat({ transport })
|
|
907
1019
|
|
|
908
1020
|
expect(host.querySelector('.u-ai-chat__working')).toBeFalsy()
|
|
1021
|
+
expect(host.querySelector('.u-ai-chat__welcome')).toBeTruthy()
|
|
1022
|
+
expect(host.querySelector('.u-ai-chat__list .u-ai-chat__welcome')).toBeFalsy()
|
|
909
1023
|
|
|
910
1024
|
chat.value?.send('hi')
|
|
911
1025
|
await nextTick()
|
|
912
1026
|
|
|
913
|
-
|
|
1027
|
+
expect(host.querySelector('.u-ai-chat__welcome')?.classList.contains('is-leaving')).toBe(true)
|
|
1028
|
+
const working = host.querySelector('.u-ai-chat__list .u-ai-chat__working')
|
|
914
1029
|
expect(working).toBeTruthy()
|
|
915
1030
|
expect(working?.textContent).toContain('工作中')
|
|
916
1031
|
const orb = working?.querySelector('canvas.u-ai-orb') as HTMLElement | null
|
|
@@ -930,7 +1045,16 @@ describe('UAiChat', () => {
|
|
|
930
1045
|
expect(host.querySelector('.u-ai-chat__working')).toBeTruthy()
|
|
931
1046
|
await vi.advanceTimersByTimeAsync(2)
|
|
932
1047
|
await nextTick()
|
|
1048
|
+
// 停留结束 → 进入缩放退出过渡(is-leaving),元素仍在
|
|
1049
|
+
const leaving = host.querySelector('.u-ai-chat__working')
|
|
1050
|
+
expect(leaving).toBeTruthy()
|
|
1051
|
+
expect(leaving?.classList.contains('is-leaving')).toBe(true)
|
|
1052
|
+
// 过渡播完才卸载
|
|
1053
|
+
await vi.advanceTimersByTimeAsync(380)
|
|
1054
|
+
await nextTick()
|
|
933
1055
|
expect(host.querySelector('.u-ai-chat__working')).toBeFalsy()
|
|
1056
|
+
expect(host.querySelector('.u-ai-chat__welcome')).toBeTruthy()
|
|
1057
|
+
expect(host.querySelector('.u-ai-chat__list .u-ai-chat__welcome')).toBeFalsy()
|
|
934
1058
|
unmount()
|
|
935
1059
|
} finally {
|
|
936
1060
|
vi.useRealTimers()
|
|
@@ -959,7 +1083,15 @@ describe('UAiChat', () => {
|
|
|
959
1083
|
expect(host.querySelector('.u-ai-chat__working')).toBeTruthy()
|
|
960
1084
|
await vi.advanceTimersByTimeAsync(2)
|
|
961
1085
|
await nextTick()
|
|
1086
|
+
// 停留结束 → 进入缩放退出过渡(is-leaving),元素仍在
|
|
1087
|
+
const leaving = host.querySelector('.u-ai-chat__working')
|
|
1088
|
+
expect(leaving).toBeTruthy()
|
|
1089
|
+
expect(leaving?.classList.contains('is-leaving')).toBe(true)
|
|
1090
|
+
// 过渡播完才卸载
|
|
1091
|
+
await vi.advanceTimersByTimeAsync(380)
|
|
1092
|
+
await nextTick()
|
|
962
1093
|
expect(host.querySelector('.u-ai-chat__working')).toBeFalsy()
|
|
1094
|
+
expect(host.querySelector('.u-ai-chat__welcome')).toBeTruthy()
|
|
963
1095
|
unmount()
|
|
964
1096
|
} finally {
|
|
965
1097
|
vi.useRealTimers()
|
|
@@ -985,7 +1117,15 @@ describe('UAiChat', () => {
|
|
|
985
1117
|
|
|
986
1118
|
await vi.advanceTimersByTimeAsync(2500)
|
|
987
1119
|
await nextTick()
|
|
1120
|
+
// 停留结束 → 进入缩放退出过渡(is-leaving),元素仍在
|
|
1121
|
+
const leaving = host.querySelector('.u-ai-chat__working')
|
|
1122
|
+
expect(leaving).toBeTruthy()
|
|
1123
|
+
expect(leaving?.classList.contains('is-leaving')).toBe(true)
|
|
1124
|
+
// 过渡播完才卸载
|
|
1125
|
+
await vi.advanceTimersByTimeAsync(380)
|
|
1126
|
+
await nextTick()
|
|
988
1127
|
expect(host.querySelector('.u-ai-chat__working')).toBeFalsy()
|
|
1128
|
+
expect(host.querySelector('.u-ai-chat__welcome')).toBeTruthy()
|
|
989
1129
|
unmount()
|
|
990
1130
|
} finally {
|
|
991
1131
|
vi.useRealTimers()
|
|
@@ -78,7 +78,7 @@ const props = defineProps<AiChatProps>()
|
|
|
78
78
|
const emit = defineEmits<AiChatEmits>()
|
|
79
79
|
|
|
80
80
|
defineSlots<{
|
|
81
|
-
/**
|
|
81
|
+
/** 空闲欢迎区插槽(输入框上方;工作中跳到列表末尾) */
|
|
82
82
|
welcome(): any
|
|
83
83
|
/** 按工具名自定义工具结果展示,如 tool-getWeather */
|
|
84
84
|
[name: `tool-${string}`]: (scope: { toolCall: ChatToolCall }) => any
|
|
@@ -112,7 +112,7 @@ const {
|
|
|
112
112
|
removeQueued
|
|
113
113
|
} = useChat({ props, emit })
|
|
114
114
|
|
|
115
|
-
/**
|
|
115
|
+
/** 空会话(无可见消息):输入区垂直居中;欢迎区钉在滚动容器外、贴于输入框上方 */
|
|
116
116
|
const isEmpty = computed(() => !messages.value.some((msg) => msg.role !== 'tool'))
|
|
117
117
|
|
|
118
118
|
/** 所有 renderTo: 'panel' 的工具调用(按消息顺序,面板内容的来源) */
|
|
@@ -153,7 +153,7 @@ const handleReasoningScroll = (position: Required<ScrollPosition>) => {
|
|
|
153
153
|
if (position.sh - position.ch - position.y < 8) reasoningStick.value = true
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
//
|
|
156
|
+
// 流式结束后自动折叠思考过程;immediate 保证终态消息重挂载(过程块展开/收起、载入历史会话)时呈折叠态
|
|
157
157
|
watch(
|
|
158
158
|
() => props.message.status,
|
|
159
159
|
(status) => {
|
|
@@ -163,7 +163,8 @@ watch(
|
|
|
163
163
|
} else if (status) {
|
|
164
164
|
reasoningExpanded.value = false
|
|
165
165
|
}
|
|
166
|
-
}
|
|
166
|
+
},
|
|
167
|
+
{ immediate: true }
|
|
167
168
|
)
|
|
168
169
|
|
|
169
170
|
watch(
|