@veltra/ai 2.1.1 → 2.1.3
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/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 +2 -7
- 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 +111 -31
- 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/components/ai-chat/__test__/ai-chat.test.ts +136 -35
- package/src/components/ai-chat/ai-chat.vue +2 -2
- package/src/components/ai-chat/message-item.vue +3 -3
- package/src/components/ai-chat/message-list.vue +186 -66
- package/src/components/ai-chat/style.scss +149 -41
- 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
|
@@ -7,46 +7,79 @@
|
|
|
7
7
|
@scroll="handleScroll"
|
|
8
8
|
@wheel.passive="handleWheel"
|
|
9
9
|
>
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
10
|
+
<template v-for="turn in turns" :key="turn.key">
|
|
11
|
+
<MessageItem
|
|
12
|
+
v-if="turn.userMsg"
|
|
13
|
+
:message="turn.userMsg"
|
|
14
|
+
:is-last="false"
|
|
15
|
+
:renderer-props="rendererProps"
|
|
16
|
+
@respond="(id, approved) => emit('respond', id, approved)"
|
|
17
|
+
@regenerate="emit('regenerate')"
|
|
18
|
+
/>
|
|
19
|
+
|
|
20
|
+
<!-- 最终答案开始输出后,本轮之前的思考/工具调用过程收进「已完成」折叠块 -->
|
|
21
|
+
<TurnProcess
|
|
22
|
+
v-if="turn.processCollapsed"
|
|
23
|
+
:messages="turn.processMsgs"
|
|
24
|
+
:renderer-props="rendererProps"
|
|
25
|
+
@respond="(id, approved) => emit('respond', id, approved)"
|
|
26
|
+
/>
|
|
27
|
+
<template v-else>
|
|
28
|
+
<MessageItem
|
|
29
|
+
v-for="msg in turn.processMsgs"
|
|
30
|
+
:key="msg.id"
|
|
31
|
+
:message="msg"
|
|
32
|
+
:is-last="false"
|
|
33
|
+
:renderer-props="rendererProps"
|
|
34
|
+
@respond="(id, approved) => emit('respond', id, approved)"
|
|
35
|
+
@regenerate="emit('regenerate')"
|
|
36
|
+
/>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<MessageItem
|
|
40
|
+
v-if="turn.answerMsg"
|
|
41
|
+
:message="turn.answerMsg"
|
|
42
|
+
:is-last="turn.isLast"
|
|
43
|
+
:renderer-props="rendererProps"
|
|
44
|
+
@respond="(id, approved) => emit('respond', id, approved)"
|
|
45
|
+
@regenerate="emit('regenerate')"
|
|
46
|
+
/>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<!-- 工作中:活体球从输入框上方跳到最新会话下面;结束停留后再跳回 -->
|
|
50
|
+
<div v-if="showWorking" :class="[cls.e('working'), bem.is('leaving', workingLeaving)]">
|
|
51
|
+
<UAiOrb ref="workingOrb" :size="ORB_SIZE" :status="workingOrbStatus" />
|
|
46
52
|
<span v-if="running" :class="[cls.e('working-text'), 'u-shine']">工作中…</span>
|
|
47
53
|
</div>
|
|
48
54
|
</UScroll>
|
|
49
55
|
|
|
56
|
+
<!-- 空闲:始终钉在滚动容器外、输入框上方;工作开始时缩小离开、结束后放大回来 -->
|
|
57
|
+
<div
|
|
58
|
+
v-if="showIdleWelcome"
|
|
59
|
+
:class="[cls.e('welcome'), bem.is('leaving', idleLeaving), bem.is('enter', idleEnter)]"
|
|
60
|
+
>
|
|
61
|
+
<slot name="welcome">
|
|
62
|
+
<div :class="cls.e('welcome-inner')">
|
|
63
|
+
<UAiOrb :size="ORB_SIZE" @click="cycleWelcome" />
|
|
64
|
+
<!-- 快捷提问逐条展示在球右侧并自动轮换:点文案发送,点球立即换下一条。
|
|
65
|
+
固定锚点布局:球位置钉死不动,气泡只向右延展,避免轮换时水平瞬移 -->
|
|
66
|
+
<div v-if="welcomeItems.length" :class="cls.e('welcome-item-wrap')">
|
|
67
|
+
<Transition :name="cls.em('welcome', 'swap')" mode="out-in">
|
|
68
|
+
<button
|
|
69
|
+
v-if="currentWelcomeItem"
|
|
70
|
+
:key="welcomeIndex"
|
|
71
|
+
type="button"
|
|
72
|
+
:class="cls.e('welcome-item')"
|
|
73
|
+
@click="emit('welcome-click', currentWelcomeItem)"
|
|
74
|
+
>
|
|
75
|
+
{{ currentWelcomeItem }}
|
|
76
|
+
</button>
|
|
77
|
+
</Transition>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</slot>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
50
83
|
<!-- 用户上翻浏览时悬浮入口:一键回到最新消息 -->
|
|
51
84
|
<Transition :name="cls.em('to-latest', 'fade')">
|
|
52
85
|
<button v-if="showToLatest" type="button" :class="cls.e('to-latest')" @click="scrollToLatest">
|
|
@@ -78,19 +111,20 @@ import type { AiOrbExposed, AiOrbReaction, AiOrbStatus } from '../../types/ai-or
|
|
|
78
111
|
import UAiOrb from '../ai-orb/ai-orb.vue'
|
|
79
112
|
import { AiChatDIKey } from './di'
|
|
80
113
|
import MessageItem from './message-item.vue'
|
|
114
|
+
import TurnProcess from './turn-process.vue'
|
|
81
115
|
|
|
82
116
|
defineOptions({ name: 'UAiChatMessageList' })
|
|
83
117
|
|
|
84
|
-
/**
|
|
85
|
-
const
|
|
86
|
-
/**
|
|
87
|
-
const WORKING_ORB_SIZE = 48
|
|
88
|
-
/** 生成结束 / 失败后工作球停留再隐藏 */
|
|
118
|
+
/** 空闲欢迎 / 工作指示共用活体球直径(px) */
|
|
119
|
+
const ORB_SIZE = 48
|
|
120
|
+
/** 生成结束 / 失败后工作球停留再跳回输入框上方 */
|
|
89
121
|
const WORKING_LINGER_MS = 2500
|
|
122
|
+
/** 两处活体球对向缩放的时长(与样式中的 jump 过渡一致) */
|
|
123
|
+
const JUMP_MS = 380
|
|
90
124
|
|
|
91
125
|
const props = defineProps<{
|
|
92
126
|
messages: ChatMessage[]
|
|
93
|
-
/**
|
|
127
|
+
/** 空闲时输入框上方的快捷提问;字符串按单项处理 */
|
|
94
128
|
welcome?: string | string[]
|
|
95
129
|
/** 是否生成中(控制底部工作状态指示) */
|
|
96
130
|
running?: boolean
|
|
@@ -106,7 +140,7 @@ const emit = defineEmits<{
|
|
|
106
140
|
}>()
|
|
107
141
|
|
|
108
142
|
defineSlots<{
|
|
109
|
-
/**
|
|
143
|
+
/** 空闲欢迎区插槽(输入框上方;工作中跳到列表末尾) */
|
|
110
144
|
welcome(): any
|
|
111
145
|
}>()
|
|
112
146
|
|
|
@@ -116,6 +150,56 @@ const cls = di?.cls ?? bem('ai-chat')
|
|
|
116
150
|
/** tool 消息不单独渲染,其内容体现在工具卡片的结果区 */
|
|
117
151
|
const visibleMessages = computed(() => props.messages.filter((msg) => msg.role !== 'tool'))
|
|
118
152
|
|
|
153
|
+
/** 一轮对话:以 user 消息为界,含本轮全部 assistant 消息(每次工具循环产生一条) */
|
|
154
|
+
interface ChatTurn {
|
|
155
|
+
/** 轮次 key(取用户消息或首条 assistant 消息 id) */
|
|
156
|
+
key: string
|
|
157
|
+
userMsg?: ChatMessage
|
|
158
|
+
/** 最终答案之前的过程消息(思考 + 中间文本 + 工具卡片) */
|
|
159
|
+
processMsgs: ChatMessage[]
|
|
160
|
+
/** 本轮最终答案(最后一条 assistant 消息) */
|
|
161
|
+
answerMsg?: ChatMessage
|
|
162
|
+
/** 过程消息是否收进「已完成」折叠块 */
|
|
163
|
+
processCollapsed: boolean
|
|
164
|
+
/** answerMsg 是否为全局最后一条可见消息 */
|
|
165
|
+
isLast: boolean
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const isFinalStatus = (msg?: ChatMessage) =>
|
|
169
|
+
msg?.status === 'done' || msg?.status === 'error' || msg?.status === 'aborted'
|
|
170
|
+
|
|
171
|
+
const turns = computed<ChatTurn[]>(() => {
|
|
172
|
+
const grouped: { key: string; userMsg?: ChatMessage; assistants: ChatMessage[] }[] = []
|
|
173
|
+
for (const msg of visibleMessages.value) {
|
|
174
|
+
if (msg.role === 'user') {
|
|
175
|
+
grouped.push({ key: msg.id, userMsg: msg, assistants: [] })
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
const last = grouped[grouped.length - 1]
|
|
179
|
+
// 兼容 assistant 开头的历史会话:归入无用户消息的轮次
|
|
180
|
+
if (last) last.assistants.push(msg)
|
|
181
|
+
else grouped.push({ key: msg.id, assistants: [msg] })
|
|
182
|
+
}
|
|
183
|
+
return grouped.map((turn, index) => {
|
|
184
|
+
const processMsgs = turn.assistants.slice(0, -1)
|
|
185
|
+
const answerMsg = turn.assistants[turn.assistants.length - 1]
|
|
186
|
+
const isLastTurn = index === grouped.length - 1
|
|
187
|
+
// 该轮结束(非最后一轮 / 答案进入终态)或最终答案开始输出时折叠过程。
|
|
188
|
+
// content 只增不减,折叠后不会回弹;regenerate 删除本轮 assistant 重跑,条件自然复位
|
|
189
|
+
const processCollapsed =
|
|
190
|
+
processMsgs.length > 0 &&
|
|
191
|
+
(!isLastTurn || !!answerMsg?.content || (!props.running && isFinalStatus(answerMsg)))
|
|
192
|
+
return {
|
|
193
|
+
key: turn.key,
|
|
194
|
+
userMsg: turn.userMsg,
|
|
195
|
+
processMsgs,
|
|
196
|
+
answerMsg,
|
|
197
|
+
processCollapsed,
|
|
198
|
+
isLast: isLastTurn && !!answerMsg
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
119
203
|
const welcomeItems = computed(() => {
|
|
120
204
|
const welcome = props.welcome
|
|
121
205
|
if (!welcome) return []
|
|
@@ -154,17 +238,27 @@ const cycleWelcome = () => {
|
|
|
154
238
|
startWelcomeRotation()
|
|
155
239
|
}
|
|
156
240
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
)
|
|
241
|
+
/** 回答完毕 / 失败后工作球短暂停留,播完表情再跳回输入框上方 */
|
|
242
|
+
const finishing = ref(false)
|
|
243
|
+
/** 列表中的球正在缩小离开 */
|
|
244
|
+
const workingLeaving = ref(false)
|
|
245
|
+
/** 输入框上方的球正在缩小离开 */
|
|
246
|
+
const idleLeaving = ref(false)
|
|
247
|
+
/** 输入框上方的球从工作位放大回来(首屏出现不播) */
|
|
248
|
+
const idleEnter = ref(false)
|
|
166
249
|
|
|
167
|
-
|
|
250
|
+
let finishTimer: ReturnType<typeof setTimeout> | undefined
|
|
251
|
+
let jumpTimer: ReturnType<typeof setTimeout> | undefined
|
|
252
|
+
|
|
253
|
+
const clearJumpTimers = () => {
|
|
254
|
+
clearTimeout(finishTimer)
|
|
255
|
+
clearTimeout(jumpTimer)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 列表末尾的工作球:生成中 / 收尾停留 / 跳回过渡 */
|
|
259
|
+
const showWorking = computed(() => !!(props.running || finishing.value || workingLeaving.value))
|
|
260
|
+
/** 输入框上方的空闲欢迎:含工作开始时的缩小离开帧 */
|
|
261
|
+
const showIdleWelcome = computed(() => (!props.running && !finishing.value) || idleLeaving.value)
|
|
168
262
|
|
|
169
263
|
const workingOrbRef = useTemplateRef<AiOrbExposed>('workingOrb')
|
|
170
264
|
|
|
@@ -191,15 +285,6 @@ watch(toolErrorIds, (ids) => {
|
|
|
191
285
|
}
|
|
192
286
|
})
|
|
193
287
|
|
|
194
|
-
/** 回答完毕 / 失败后工作球短暂停留,播完表情再隐藏 */
|
|
195
|
-
const finishing = ref(false)
|
|
196
|
-
let finishTimer: ReturnType<typeof setTimeout> | undefined
|
|
197
|
-
|
|
198
|
-
const clearFinishing = () => {
|
|
199
|
-
clearTimeout(finishTimer)
|
|
200
|
-
finishing.value = false
|
|
201
|
-
}
|
|
202
|
-
|
|
203
288
|
/** 工作球状态:收尾停留 → idle;正文输出中 → speaking;其余生成阶段 → thinking */
|
|
204
289
|
const workingOrbStatus = computed<AiOrbStatus>(() => {
|
|
205
290
|
if (finishing.value) return 'idle'
|
|
@@ -214,13 +299,26 @@ watch(
|
|
|
214
299
|
() => props.running,
|
|
215
300
|
(running, prev) => {
|
|
216
301
|
if (running) {
|
|
217
|
-
//
|
|
218
|
-
|
|
302
|
+
// 工作开始:立即从输入框上方跳到列表,无停留
|
|
303
|
+
clearJumpTimers()
|
|
304
|
+
const idleWasVisible = !prev && !finishing.value && !workingLeaving.value
|
|
305
|
+
finishing.value = false
|
|
306
|
+
workingLeaving.value = false
|
|
307
|
+
idleEnter.value = false
|
|
308
|
+
if (idleWasVisible) {
|
|
309
|
+
idleLeaving.value = true
|
|
310
|
+
jumpTimer = setTimeout(() => {
|
|
311
|
+
idleLeaving.value = false
|
|
312
|
+
}, JUMP_MS)
|
|
313
|
+
} else {
|
|
314
|
+
idleLeaving.value = false
|
|
315
|
+
}
|
|
219
316
|
return
|
|
220
317
|
}
|
|
221
318
|
if (!prev) return
|
|
222
319
|
|
|
223
320
|
finishing.value = true
|
|
321
|
+
idleLeaving.value = false
|
|
224
322
|
const last = visibleMessages.value[visibleMessages.value.length - 1]
|
|
225
323
|
let reaction: AiOrbReaction | undefined
|
|
226
324
|
if (last?.role === 'assistant') {
|
|
@@ -230,11 +328,33 @@ watch(
|
|
|
230
328
|
if (reaction) {
|
|
231
329
|
nextTick(() => workingOrbRef.value?.react(reaction))
|
|
232
330
|
}
|
|
233
|
-
|
|
331
|
+
// 停留结束 → 列表中缩小、输入框上方放大
|
|
332
|
+
finishTimer = setTimeout(() => {
|
|
333
|
+
finishing.value = false
|
|
334
|
+
workingLeaving.value = true
|
|
335
|
+
idleEnter.value = true
|
|
336
|
+
jumpTimer = setTimeout(() => {
|
|
337
|
+
workingLeaving.value = false
|
|
338
|
+
idleEnter.value = false
|
|
339
|
+
}, JUMP_MS)
|
|
340
|
+
}, WORKING_LINGER_MS)
|
|
234
341
|
}
|
|
235
342
|
)
|
|
236
343
|
|
|
237
|
-
|
|
344
|
+
// 空闲欢迎区展示期间自动轮换;跳到工作位后停止
|
|
345
|
+
watch(
|
|
346
|
+
() => [welcomeItems.value.length, showIdleWelcome.value] as const,
|
|
347
|
+
() => {
|
|
348
|
+
if (showIdleWelcome.value) startWelcomeRotation()
|
|
349
|
+
else stopWelcomeRotation()
|
|
350
|
+
},
|
|
351
|
+
{ immediate: true }
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
onBeforeUnmount(() => {
|
|
355
|
+
stopWelcomeRotation()
|
|
356
|
+
clearJumpTimers()
|
|
357
|
+
})
|
|
238
358
|
|
|
239
359
|
const scrollRef = shallowRef<{ scrollTo: (position: ScrollPosition) => void }>()
|
|
240
360
|
/** 是否吸附底部(用户上翻浏览历史时取消吸附) */
|
|
@@ -22,7 +22,7 @@ $root-name: ai-chat;
|
|
|
22
22
|
padding: 0 fn.use-var(gap, default);
|
|
23
23
|
|
|
24
24
|
// 空会话:列表区与末尾占位各分一半剩余空间,输入区垂直居中,
|
|
25
|
-
//
|
|
25
|
+
// 欢迎区钉在滚动容器外、贴于输入框上方;有消息后自动回落到底部
|
|
26
26
|
@include m.is(empty) {
|
|
27
27
|
&::after {
|
|
28
28
|
content: '';
|
|
@@ -78,6 +78,19 @@ $root-name: ai-chat;
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// 活体球在输入框上方 ↔ 列表末尾对向缩放(时长与 JUMP_MS 一致)
|
|
82
|
+
@keyframes u-ai-chat-orb-jump-in {
|
|
83
|
+
from {
|
|
84
|
+
opacity: 0;
|
|
85
|
+
transform: scale(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
to {
|
|
89
|
+
opacity: 1;
|
|
90
|
+
transform: scale(1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
// 面板头部:无下分割线,靠留白与卡片边界区分内容区
|
|
82
95
|
@include m.e(panel-header) {
|
|
83
96
|
flex: none;
|
|
@@ -178,22 +191,43 @@ $root-name: ai-chat;
|
|
|
178
191
|
@include m.e(list) {
|
|
179
192
|
flex: 1;
|
|
180
193
|
min-height: 0;
|
|
181
|
-
|
|
182
|
-
// 内容不足一屏时撑满滚动视口并按列排布,空会话欢迎区才能占满整列做底部对齐
|
|
183
|
-
.u-scroll__content {
|
|
184
|
-
min-height: 100%;
|
|
185
|
-
display: flex;
|
|
186
|
-
flex-direction: column;
|
|
187
|
-
}
|
|
188
194
|
}
|
|
189
195
|
|
|
190
|
-
//
|
|
196
|
+
// 空闲欢迎:钉在滚动容器外、输入框上方;高度锁 48px(球 + 快捷提问)
|
|
191
197
|
@include m.e(welcome) {
|
|
192
|
-
flex:
|
|
193
|
-
|
|
198
|
+
flex-shrink: 0;
|
|
199
|
+
width: 100%;
|
|
200
|
+
height: 48px;
|
|
201
|
+
@include m.flex(flex, flex-start, center);
|
|
194
202
|
color: fn.use-var(text-color, assist);
|
|
195
|
-
padding: fn.use-var(gap, default)
|
|
196
|
-
|
|
203
|
+
padding: 0 fn.use-var(gap, default);
|
|
204
|
+
|
|
205
|
+
.u-ai-orb {
|
|
206
|
+
transform-origin: center;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@include m.is(enter) {
|
|
210
|
+
.u-ai-orb {
|
|
211
|
+
animation: u-ai-chat-orb-jump-in 0.38s ease both;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@include m.is(leaving) {
|
|
216
|
+
pointer-events: none;
|
|
217
|
+
|
|
218
|
+
.u-ai-orb {
|
|
219
|
+
opacity: 0;
|
|
220
|
+
transform: scale(0);
|
|
221
|
+
transition:
|
|
222
|
+
opacity 0.28s ease,
|
|
223
|
+
transform 0.38s ease;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.u-#{$root-name}__welcome-item-wrap {
|
|
227
|
+
opacity: 0;
|
|
228
|
+
transition: opacity 0.2s ease;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
197
231
|
}
|
|
198
232
|
|
|
199
233
|
@include m.e(welcome-inner) {
|
|
@@ -207,14 +241,14 @@ $root-name: ai-chat;
|
|
|
207
241
|
@include m.e(welcome-item-wrap) {
|
|
208
242
|
flex: 1;
|
|
209
243
|
min-width: 0;
|
|
244
|
+
height: 48px;
|
|
210
245
|
display: flex;
|
|
211
246
|
align-items: center;
|
|
212
|
-
// 预留两行高度:长文案换行时行高不抖动
|
|
213
|
-
min-height: 54px;
|
|
214
247
|
}
|
|
215
248
|
|
|
216
249
|
@include m.e(welcome-item) {
|
|
217
250
|
max-width: 100%;
|
|
251
|
+
@include m.ellipsis;
|
|
218
252
|
padding: fn.use-var(gap, small) fn.use-var(gap, default);
|
|
219
253
|
border: none;
|
|
220
254
|
border-radius: fn.use-var(radius, large);
|
|
@@ -261,12 +295,44 @@ $root-name: ai-chat;
|
|
|
261
295
|
@include m.flex(flex, flex-start, center);
|
|
262
296
|
gap: fn.use-var(gap, small);
|
|
263
297
|
padding: 0 fn.use-var(gap, default) fn.use-var(gap, small);
|
|
298
|
+
width: fit-content;
|
|
299
|
+
|
|
300
|
+
// 工作开始:立即从 0 放大(无延时),与输入框上方缩小形成对向跳
|
|
301
|
+
.u-ai-orb {
|
|
302
|
+
transform-origin: center;
|
|
303
|
+
animation: u-ai-chat-orb-jump-in 0.38s ease both;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@include m.is(leaving) {
|
|
307
|
+
pointer-events: none;
|
|
308
|
+
|
|
309
|
+
.u-ai-orb {
|
|
310
|
+
animation: none;
|
|
311
|
+
opacity: 0;
|
|
312
|
+
transform: scale(0);
|
|
313
|
+
transition:
|
|
314
|
+
opacity 0.28s ease,
|
|
315
|
+
transform 0.38s ease;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
264
318
|
}
|
|
265
319
|
|
|
266
320
|
@include m.e(working-text) {
|
|
267
321
|
font-size: fn.use-var(font-size-main, small);
|
|
268
322
|
}
|
|
269
323
|
|
|
324
|
+
@media (prefers-reduced-motion: reduce) {
|
|
325
|
+
.u-#{$root-name}__welcome.is-enter .u-ai-orb,
|
|
326
|
+
.u-#{$root-name}__working .u-ai-orb {
|
|
327
|
+
animation: none;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.u-#{$root-name}__welcome.is-leaving .u-ai-orb,
|
|
331
|
+
.u-#{$root-name}__working.is-leaving .u-ai-orb {
|
|
332
|
+
transition: none;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
270
336
|
// 「最新消息」悬浮按钮:上翻浏览时出现,点击回到底部
|
|
271
337
|
@include m.e(to-latest) {
|
|
272
338
|
position: absolute;
|
|
@@ -350,10 +416,11 @@ $root-name: ai-chat;
|
|
|
350
416
|
display: block;
|
|
351
417
|
}
|
|
352
418
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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 {
|
|
357
424
|
--ms-text-body: 13px;
|
|
358
425
|
--ms-leading-body: 1.6;
|
|
359
426
|
--ms-text-h1: 1.55em;
|
|
@@ -378,11 +445,18 @@ $root-name: ai-chat;
|
|
|
378
445
|
--ms-flow-hr-y: 0.9em;
|
|
379
446
|
--ms-flow-admonition-y: 0.7em;
|
|
380
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
|
+
}
|
|
381
455
|
}
|
|
382
456
|
|
|
383
457
|
@include m.e(message-status) {
|
|
384
458
|
margin-top: fn.use-var(gap, small);
|
|
385
|
-
color: fn.use-var(text-color,
|
|
459
|
+
color: fn.use-var(text-color, second);
|
|
386
460
|
font-size: fn.use-var(font-size-main, small);
|
|
387
461
|
}
|
|
388
462
|
|
|
@@ -395,7 +469,7 @@ $root-name: ai-chat;
|
|
|
395
469
|
// 思考过程
|
|
396
470
|
@include m.e(reasoning) {
|
|
397
471
|
margin-bottom: 4px;
|
|
398
|
-
color: fn.use-var(text-color,
|
|
472
|
+
color: fn.use-var(text-color, second);
|
|
399
473
|
|
|
400
474
|
@include m.is(expanded) {
|
|
401
475
|
@include m.bem($root-name, 'reasoning-chevron') {
|
|
@@ -435,38 +509,76 @@ $root-name: ai-chat;
|
|
|
435
509
|
line-height: 1.55;
|
|
436
510
|
}
|
|
437
511
|
|
|
438
|
-
//
|
|
512
|
+
// 轮次过程折叠块(最终答案输出后,之前的思考/工具调用过程收进「已完成」)
|
|
513
|
+
@include m.e(process) {
|
|
514
|
+
padding: 4px fn.use-var(gap, default);
|
|
515
|
+
color: fn.use-var(text-color, second);
|
|
516
|
+
|
|
517
|
+
@include m.is(expanded) {
|
|
518
|
+
@include m.bem($root-name, 'process-chevron') {
|
|
519
|
+
transform: rotate(90deg);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
@include m.e(process-header) {
|
|
525
|
+
@include m.flex(flex, flex-start, center);
|
|
526
|
+
gap: 4px;
|
|
527
|
+
cursor: pointer;
|
|
528
|
+
user-select: none;
|
|
529
|
+
font-size: fn.use-var(font-size-main, small);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
@include m.e(process-icon) {
|
|
533
|
+
color: fn.use-var(color, success);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
@include m.e(process-title) {
|
|
537
|
+
line-height: 1.5;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
@include m.e(process-chevron) {
|
|
541
|
+
font-size: 12px;
|
|
542
|
+
transition: transform 0.2s;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
@include m.e(process-content) {
|
|
546
|
+
margin-top: 4px;
|
|
547
|
+
padding-left: fn.use-var(gap, small);
|
|
548
|
+
border-left: fn.use-var(border-muted);
|
|
549
|
+
|
|
550
|
+
// 过程消息在边线内对齐:抵消列表项横向 padding
|
|
551
|
+
.u-#{$root-name}__message {
|
|
552
|
+
padding-left: 0;
|
|
553
|
+
padding-right: 0;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// 工具调用卡片(复用 UCollapseItem:背景/边框走组件原生配色,#header 只自定义标题区)
|
|
439
558
|
@include m.e(tool-call) {
|
|
440
559
|
margin: 4px 0;
|
|
441
|
-
background-color: fn.use-var(bg-color, top);
|
|
442
560
|
font-size: fn.use-var(font-size-main, small);
|
|
443
561
|
|
|
444
|
-
//
|
|
445
|
-
|
|
562
|
+
// 产物里 desktop collapse 样式在本文件之后加载;
|
|
563
|
+
// `.u-collapse__item + .u-collapse__item` 与单层兄弟选择器同特异性,必须带 .u-ai-chat 前缀。
|
|
564
|
+
.u-#{$root-name} & + #{fn.bem($root-name, tool-call)} {
|
|
446
565
|
margin-top: 4px;
|
|
447
566
|
}
|
|
448
567
|
|
|
449
568
|
// 覆盖 UCollapseItem 默认头部/内容区节奏,保持紧凑单行头
|
|
450
|
-
.u-collapse__header {
|
|
569
|
+
.u-#{$root-name} & .u-collapse__header {
|
|
451
570
|
gap: 6px;
|
|
452
571
|
padding: 5px fn.use-var(gap, small);
|
|
453
572
|
font-weight: 400;
|
|
454
573
|
letter-spacing: normal;
|
|
455
574
|
}
|
|
456
575
|
|
|
457
|
-
.u-collapse__content {
|
|
458
|
-
border-top: fn.use-var(border-muted);
|
|
576
|
+
.u-#{$root-name} & .u-collapse__content {
|
|
459
577
|
padding: fn.use-var(gap, small);
|
|
460
578
|
line-height: 1.5;
|
|
461
579
|
}
|
|
462
580
|
}
|
|
463
581
|
|
|
464
|
-
@include m.e(tool-call-chevron) {
|
|
465
|
-
flex-shrink: 0;
|
|
466
|
-
font-size: 12px;
|
|
467
|
-
color: fn.use-var(text-color, assist);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
582
|
@include m.e(tool-call-status) {
|
|
471
583
|
flex-shrink: 0;
|
|
472
584
|
font-size: 13px;
|
|
@@ -754,21 +866,17 @@ $root-name: ai-chat;
|
|
|
754
866
|
}
|
|
755
867
|
}
|
|
756
868
|
|
|
757
|
-
//
|
|
869
|
+
// 输入区:无边框卡片,靠浅阴影区分层级;聚焦不换蓝描边,避免变成另一种框
|
|
758
870
|
@include m.e(input-area) {
|
|
759
871
|
flex-shrink: 0;
|
|
760
872
|
width: 100%;
|
|
761
873
|
max-width: 800px;
|
|
762
874
|
box-sizing: border-box;
|
|
763
|
-
margin:
|
|
875
|
+
margin: 0 0 fn.use-var(gap, default);
|
|
764
876
|
border-radius: fn.use-var(radius, large);
|
|
765
877
|
background-color: fn.use-var(bg-color, top);
|
|
878
|
+
box-shadow: fn.use-var(shadow, sm);
|
|
766
879
|
padding: fn.use-var(gap, small);
|
|
767
|
-
transition: box-shadow 0.15s ease;
|
|
768
|
-
|
|
769
|
-
&:focus-within {
|
|
770
|
-
box-shadow: 0 0 0 1px fn.use-var(color, primary);
|
|
771
|
-
}
|
|
772
880
|
}
|
|
773
881
|
|
|
774
882
|
@include m.e(input) {
|
|
@@ -24,8 +24,6 @@
|
|
|
24
24
|
<UButton size="small" type="primary" @click="emit('respond', true)">允许</UButton>
|
|
25
25
|
<UButton size="small" text @click="emit('respond', false)">拒绝</UButton>
|
|
26
26
|
</span>
|
|
27
|
-
|
|
28
|
-
<UIcon :class="cls.e('tool-call-chevron')"><ArrowDown /></UIcon>
|
|
29
27
|
</template>
|
|
30
28
|
|
|
31
29
|
<!-- 面板工具:render 展示在右侧侧边面板,卡片 body 仅提供查看入口 -->
|
|
@@ -56,14 +54,7 @@
|
|
|
56
54
|
|
|
57
55
|
<script lang="ts" setup>
|
|
58
56
|
import { UButton, UCollapseItem, UIcon } from '@veltra/desktop'
|
|
59
|
-
import {
|
|
60
|
-
ArrowDown,
|
|
61
|
-
CircleCheckFilled,
|
|
62
|
-
CircleClose,
|
|
63
|
-
Close,
|
|
64
|
-
Loading,
|
|
65
|
-
WarningFilled
|
|
66
|
-
} from '@veltra/icons/normal'
|
|
57
|
+
import { CircleCheckFilled, CircleClose, Close, Loading, WarningFilled } from '@veltra/icons/normal'
|
|
67
58
|
import { bem } from '@veltra/utils'
|
|
68
59
|
import { computed, inject, ref, watch, type Component } from 'vue'
|
|
69
60
|
|