@veltra/ai 2.1.8 → 2.1.10
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/fold.d.ts +4 -5
- package/dist/chat/session.d.ts +5 -6
- package/dist/chat/transports/openai.d.ts +3 -4
- package/dist/chat/types.d.ts +15 -16
- package/dist/chat/use-chat.d.ts +2 -3
- package/dist/components/ai-chat/message-item.js +18 -8
- package/dist/components/ai-chat/message-item.js.map +1 -1
- package/dist/components/ai-chat/message-list.js +3 -3
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/style.js +1 -0
- package/dist/components/ai-chat/style2.css +20 -7
- package/dist/components/ai-chat/tool-call.js +18 -11
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -2
- package/dist/providers/types.d.ts +4 -5
- package/dist/style.js +0 -1
- package/dist/tools/ask-question/ask-question.d.ts +4 -5
- package/dist/tools/ask-question/ask-question.js +2 -1
- package/dist/tools/ask-question/ask-question.js.map +1 -1
- package/dist/types/ai-chat.d.ts +4 -5
- package/dist/types/index.d.ts +1 -2
- package/package.json +6 -6
- package/src/components/ai-chat/__test__/ai-chat.test.ts +74 -5
- package/src/components/ai-chat/message-item.vue +18 -16
- package/src/components/ai-chat/message-list.vue +8 -4
- package/src/components/ai-chat/style.scss +22 -8
- package/src/components/ai-chat/style.ts +2 -0
- package/src/components/ai-chat/tool-call.vue +27 -9
- package/src/components/index.ts +0 -1
- package/src/style.ts +0 -1
- package/src/tools/ask-question/__test__/ask-question.test.ts +20 -2
- package/src/tools/ask-question/ask-question.ts +2 -1
- package/src/types/index.ts +0 -1
- package/dist/components/ai-orb/style.d.ts +0 -1
- package/dist/components/ai-orb/style.js +0 -1
- package/dist/types/ai-orb.d.ts +0 -27
- package/src/components/ai-orb/index.ts +0 -1
- package/src/components/ai-orb/style.ts +0 -1
- /package/dist/components/ai-orb/{style2.css → style.css} +0 -0
package/dist/chat/fold.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { AskQuestionItem } from "../tools/ask-question/ask-question.js";
|
|
|
3
3
|
import "../tools/index.js";
|
|
4
4
|
import { ChatSessionEvent } from "./session.js";
|
|
5
5
|
//#region src/chat/fold.d.ts
|
|
6
|
-
interface ChatPendingApproval {
|
|
6
|
+
export interface ChatPendingApproval {
|
|
7
7
|
approvalId: string;
|
|
8
8
|
toolName: string;
|
|
9
9
|
callId?: string;
|
|
@@ -11,7 +11,7 @@ interface ChatPendingApproval {
|
|
|
11
11
|
rpcId: string;
|
|
12
12
|
}
|
|
13
13
|
/** 历史回放与实时 onEvent 共用的折叠状态 */
|
|
14
|
-
interface ChatFoldState {
|
|
14
|
+
export interface ChatFoldState {
|
|
15
15
|
lastSeq: number | null;
|
|
16
16
|
messages: ChatMessage[];
|
|
17
17
|
queue: ChatQueuedMessage[];
|
|
@@ -30,12 +30,11 @@ interface ChatFoldState {
|
|
|
30
30
|
message: string;
|
|
31
31
|
} | null;
|
|
32
32
|
}
|
|
33
|
-
declare function createFoldState(): ChatFoldState;
|
|
33
|
+
export declare function createFoldState(): ChatFoldState;
|
|
34
34
|
/**
|
|
35
35
|
* 纯函数 fold:历史回放与实时 onEvent 走同一套。
|
|
36
36
|
* 后到的更小/重复 seq 不覆盖已应用事件。
|
|
37
37
|
*/
|
|
38
|
-
declare function foldSessionEvent(state: ChatFoldState, event: ChatSessionEvent): ChatFoldState;
|
|
38
|
+
export declare function foldSessionEvent(state: ChatFoldState, event: ChatSessionEvent): ChatFoldState;
|
|
39
39
|
//#endregion
|
|
40
|
-
export { ChatFoldState, ChatPendingApproval, createFoldState, foldSessionEvent };
|
|
41
40
|
//# sourceMappingURL=fold.d.ts.map
|
package/dist/chat/session.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { AskQuestionItem } from "../tools/ask-question/ask-question.js";
|
|
|
3
3
|
import "../tools/index.js";
|
|
4
4
|
//#region src/chat/session.d.ts
|
|
5
5
|
/** 归一化后的服务端会话事件(协议无关) */
|
|
6
|
-
type ChatSessionEvent = {
|
|
6
|
+
export type ChatSessionEvent = {
|
|
7
7
|
type: 'user/message';
|
|
8
8
|
messageId: string;
|
|
9
9
|
seq: number;
|
|
@@ -78,7 +78,7 @@ type ChatSessionEvent = {
|
|
|
78
78
|
message: string;
|
|
79
79
|
};
|
|
80
80
|
/** 服务端会话 transport:对象形态,与函数型 ChatTransport 互斥 */
|
|
81
|
-
interface ChatSessionTransport {
|
|
81
|
+
export interface ChatSessionTransport {
|
|
82
82
|
readonly kind: 'session';
|
|
83
83
|
open(handlers: {
|
|
84
84
|
onEvent(event: ChatSessionEvent): void;
|
|
@@ -97,7 +97,7 @@ interface ChatSessionTransport {
|
|
|
97
97
|
* 协议翻译层:订阅事件流、把动作打到远端、拉历史。
|
|
98
98
|
* 时序校验 / dispose / 断线补拉 / in-flight 去重由 createServerTransport 完成。
|
|
99
99
|
*/
|
|
100
|
-
interface ChatSessionAdapter {
|
|
100
|
+
export interface ChatSessionAdapter {
|
|
101
101
|
subscribe(handlers: {
|
|
102
102
|
onEvent(event: ChatSessionEvent): void;
|
|
103
103
|
onDisconnect?(): void;
|
|
@@ -112,12 +112,11 @@ interface ChatSessionAdapter {
|
|
|
112
112
|
selectModel(provider: string, model: string): Promise<void>;
|
|
113
113
|
}
|
|
114
114
|
/** 仅带 kind: 'session' 的对象为 true;函数 / null / 无 kind 均为 false */
|
|
115
|
-
declare function isServerTransport(t: unknown): t is ChatSessionTransport;
|
|
115
|
+
export declare function isServerTransport(t: unknown): t is ChatSessionTransport;
|
|
116
116
|
/**
|
|
117
117
|
* 用 adapter 包出 ChatSessionTransport。
|
|
118
118
|
* 包内完成 seq 校验、disposer、断线补拉、动作 in-flight 去重。
|
|
119
119
|
*/
|
|
120
|
-
declare function createServerTransport(adapter: ChatSessionAdapter): ChatSessionTransport;
|
|
120
|
+
export declare function createServerTransport(adapter: ChatSessionAdapter): ChatSessionTransport;
|
|
121
121
|
//#endregion
|
|
122
|
-
export { ChatSessionAdapter, ChatSessionEvent, ChatSessionTransport, createServerTransport, isServerTransport };
|
|
123
122
|
//# sourceMappingURL=session.d.ts.map
|
|
@@ -2,7 +2,7 @@ import { ChatTransport } from "../types.js";
|
|
|
2
2
|
import { ChatModelOption, ChatProvider } from "../../providers/types.js";
|
|
3
3
|
//#region src/chat/transports/openai.d.ts
|
|
4
4
|
/** createOpenAITransport 的配置项(多 Provider) */
|
|
5
|
-
interface OpenAITransportOptions {
|
|
5
|
+
export interface OpenAITransportOptions {
|
|
6
6
|
/** 至少一个 Provider;模型 id 须跨 Provider 全局唯一 */
|
|
7
7
|
providers: ChatProvider[];
|
|
8
8
|
/** 全局额外请求头(与各 Provider headers 合并,Provider 优先) */
|
|
@@ -11,7 +11,7 @@ interface OpenAITransportOptions {
|
|
|
11
11
|
body?: Record<string, unknown>;
|
|
12
12
|
}
|
|
13
13
|
/** 带扁平模型列表元数据的 OpenAI 兼容 transport */
|
|
14
|
-
type OpenAITransport = ChatTransport & {
|
|
14
|
+
export type OpenAITransport = ChatTransport & {
|
|
15
15
|
/** 供 UI 使用的扁平模型列表 */
|
|
16
16
|
readonly models: ChatModelOption[];
|
|
17
17
|
/** 默认模型 id(首个 Provider 的首个模型) */
|
|
@@ -21,7 +21,6 @@ type OpenAITransport = ChatTransport & {
|
|
|
21
21
|
* 创建 OpenAI 兼容协议的传输层(chat/completions SSE)。
|
|
22
22
|
* 按 request.model 路由到对应 Provider 的 endpoint;手写 fetch + SSE,无第三方依赖。
|
|
23
23
|
*/
|
|
24
|
-
declare function createOpenAITransport(options: OpenAITransportOptions): OpenAITransport;
|
|
24
|
+
export declare function createOpenAITransport(options: OpenAITransportOptions): OpenAITransport;
|
|
25
25
|
//#endregion
|
|
26
|
-
export { OpenAITransport, OpenAITransportOptions, createOpenAITransport };
|
|
27
26
|
//# sourceMappingURL=openai.d.ts.map
|
package/dist/chat/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Component } from "vue";
|
|
2
2
|
//#region src/chat/types.d.ts
|
|
3
3
|
/** 聊天附件(首版仅支持图片) */
|
|
4
|
-
interface ChatAttachment {
|
|
4
|
+
export interface ChatAttachment {
|
|
5
5
|
/** 文件名 */
|
|
6
6
|
name: string;
|
|
7
7
|
/** MIME 类型 */
|
|
@@ -12,9 +12,9 @@ interface ChatAttachment {
|
|
|
12
12
|
dataUrl: string;
|
|
13
13
|
}
|
|
14
14
|
/** 工具调用状态 */
|
|
15
|
-
type ToolCallStatus = 'pending' | 'awaiting-confirm' | 'running' | 'success' | 'error' | 'rejected';
|
|
15
|
+
export type ToolCallStatus = 'pending' | 'awaiting-confirm' | 'running' | 'success' | 'error' | 'rejected';
|
|
16
16
|
/** 一次工具调用 */
|
|
17
|
-
interface ChatToolCall {
|
|
17
|
+
export interface ChatToolCall {
|
|
18
18
|
/** 调用 id(由模型生成) */
|
|
19
19
|
id: string;
|
|
20
20
|
/** 工具名 */
|
|
@@ -31,7 +31,7 @@ interface ChatToolCall {
|
|
|
31
31
|
view?: unknown;
|
|
32
32
|
}
|
|
33
33
|
/** 聊天消息 */
|
|
34
|
-
interface ChatMessage {
|
|
34
|
+
export interface ChatMessage {
|
|
35
35
|
/** 消息 id */
|
|
36
36
|
id: string;
|
|
37
37
|
/** 角色 */
|
|
@@ -50,7 +50,7 @@ interface ChatMessage {
|
|
|
50
50
|
status?: 'streaming' | 'done' | 'error' | 'aborted';
|
|
51
51
|
}
|
|
52
52
|
/** 队列中的待发送消息(会话进行中提交的消息按序排队) */
|
|
53
|
-
interface ChatQueuedMessage {
|
|
53
|
+
export interface ChatQueuedMessage {
|
|
54
54
|
/** 队列项 id */
|
|
55
55
|
id: string;
|
|
56
56
|
/** 消息内容 */
|
|
@@ -59,7 +59,7 @@ interface ChatQueuedMessage {
|
|
|
59
59
|
attachments?: ChatAttachment[];
|
|
60
60
|
}
|
|
61
61
|
/** 工具执行上下文 */
|
|
62
|
-
interface ChatToolContext {
|
|
62
|
+
export interface ChatToolContext {
|
|
63
63
|
/** 本次工具调用 */
|
|
64
64
|
toolCall: ChatToolCall;
|
|
65
65
|
/** 中断信号(用户点击停止时触发) */
|
|
@@ -69,7 +69,7 @@ interface ChatToolContext {
|
|
|
69
69
|
* 对话工具定义。
|
|
70
70
|
* 在不同场景传入不同的工具数组,即可赋予对话助手不同的能力。
|
|
71
71
|
*/
|
|
72
|
-
interface ChatTool<A = any> {
|
|
72
|
+
export interface ChatTool<A = any> {
|
|
73
73
|
/** 工具名(传给模型,需唯一) */
|
|
74
74
|
name: string;
|
|
75
75
|
/** 工具描述(传给模型) */
|
|
@@ -122,7 +122,7 @@ interface ChatTool<A = any> {
|
|
|
122
122
|
* 服务端驱动模式下 tools 的合法形态:纯渲染元信息(执行在服务端)。
|
|
123
123
|
* 没有 execute / description / parameters / needsConfirm / terminal。
|
|
124
124
|
*/
|
|
125
|
-
interface ChatToolMeta {
|
|
125
|
+
export interface ChatToolMeta {
|
|
126
126
|
/** 与服务端 tool/call.name 匹配 */
|
|
127
127
|
name: string;
|
|
128
128
|
/** 覆盖通用图标 */
|
|
@@ -141,9 +141,9 @@ interface ChatToolMeta {
|
|
|
141
141
|
autoCollapse?: boolean;
|
|
142
142
|
}
|
|
143
143
|
/** 作业条项状态:至少区分进行中与终态 */
|
|
144
|
-
type ChatJobStatus = 'running' | 'done' | 'error';
|
|
144
|
+
export type ChatJobStatus = 'running' | 'done' | 'error';
|
|
145
145
|
/** 服务端下发的作业条项 */
|
|
146
|
-
interface ChatJob {
|
|
146
|
+
export interface ChatJob {
|
|
147
147
|
/** 稳定 id */
|
|
148
148
|
id: string;
|
|
149
149
|
/** 作业类型(用于图标映射) */
|
|
@@ -154,12 +154,12 @@ interface ChatJob {
|
|
|
154
154
|
status: ChatJobStatus;
|
|
155
155
|
}
|
|
156
156
|
/** 工具自定义渲染组件 / 渲染函数的 props */
|
|
157
|
-
interface ChatToolRenderProps {
|
|
157
|
+
export interface ChatToolRenderProps {
|
|
158
158
|
/** 本次工具调用(含 status/arguments/result/error,可自行渲染进度与错误) */
|
|
159
159
|
toolCall: ChatToolCall;
|
|
160
160
|
}
|
|
161
161
|
/** transport 请求参数 */
|
|
162
|
-
interface ChatTransportRequest {
|
|
162
|
+
export interface ChatTransportRequest {
|
|
163
163
|
/** 完整消息历史(内部模型,transport 自行转换为 wire 格式) */
|
|
164
164
|
messages: ChatMessage[];
|
|
165
165
|
/** 系统提示词 */
|
|
@@ -177,7 +177,7 @@ interface ChatTransportRequest {
|
|
|
177
177
|
* 一次模型请求的 token 用量。
|
|
178
178
|
* 仅包含接口实际返回的字段;缓存类数字缺省为 undefined,不补 0。
|
|
179
179
|
*/
|
|
180
|
-
interface ChatTokenUsage {
|
|
180
|
+
export interface ChatTokenUsage {
|
|
181
181
|
/** 输入 token */
|
|
182
182
|
promptTokens: number;
|
|
183
183
|
/** 输出 token */
|
|
@@ -199,7 +199,7 @@ interface ChatTokenUsage {
|
|
|
199
199
|
cacheCreationTokens?: number;
|
|
200
200
|
}
|
|
201
201
|
/** transport 流式事件回调 */
|
|
202
|
-
interface ChatTransportHandlers {
|
|
202
|
+
export interface ChatTransportHandlers {
|
|
203
203
|
/** 文本增量 */
|
|
204
204
|
onTextDelta(delta: string): void;
|
|
205
205
|
/** 思考内容增量 */
|
|
@@ -225,7 +225,6 @@ interface ChatTransportHandlers {
|
|
|
225
225
|
* 对话传输层抽象。组件不关心具体的 LLM 协议,
|
|
226
226
|
* 可传入内置的 createOpenAITransport(),也可自行实现接入任意后端。
|
|
227
227
|
*/
|
|
228
|
-
type ChatTransport = (request: ChatTransportRequest, handlers: ChatTransportHandlers) => Promise<void> | void;
|
|
228
|
+
export type ChatTransport = (request: ChatTransportRequest, handlers: ChatTransportHandlers) => Promise<void> | void;
|
|
229
229
|
//#endregion
|
|
230
|
-
export { ChatAttachment, ChatJob, ChatJobStatus, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolMeta, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus };
|
|
231
230
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/chat/use-chat.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import "../types/index.js";
|
|
|
6
6
|
import { ChatPendingApproval } from "./fold.js";
|
|
7
7
|
import { Ref } from "vue";
|
|
8
8
|
//#region src/chat/use-chat.d.ts
|
|
9
|
-
interface UseChatOptions {
|
|
9
|
+
export interface UseChatOptions {
|
|
10
10
|
props: AiChatProps;
|
|
11
11
|
emit: AiChatEmits;
|
|
12
12
|
}
|
|
@@ -15,7 +15,7 @@ interface UseChatOptions {
|
|
|
15
15
|
* 与 UI 解耦,ai-chat.vue 只负责渲染与交互转发。
|
|
16
16
|
* 函数型 transport 注入内置工具;session transport 不注入、不执行客户端工具。
|
|
17
17
|
*/
|
|
18
|
-
declare function useChat(options: UseChatOptions): {
|
|
18
|
+
export declare function useChat(options: UseChatOptions): {
|
|
19
19
|
messages: Ref<ChatMessage[], ChatMessage[]>;
|
|
20
20
|
model: Ref<string | undefined, string | undefined>;
|
|
21
21
|
reasoningLevel: Ref<string | undefined, string | undefined>;
|
|
@@ -124,5 +124,4 @@ declare function useChat(options: UseChatOptions): {
|
|
|
124
124
|
removeQueued: (id: string) => ChatQueuedMessage | undefined;
|
|
125
125
|
};
|
|
126
126
|
//#endregion
|
|
127
|
-
export { UseChatOptions, useChat };
|
|
128
127
|
//# sourceMappingURL=use-chat.d.ts.map
|
|
@@ -23,10 +23,21 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
23
23
|
const isStreaming = computed(() => props.message.status === "streaming");
|
|
24
24
|
/** 思考中:流式且正文还未开始输出 */
|
|
25
25
|
const isThinking = computed(() => isStreaming.value && !props.message.content);
|
|
26
|
-
|
|
26
|
+
/** 思考默认折叠(含流式进行中),用户点击头部展开 */
|
|
27
|
+
const reasoningExpanded = ref(false);
|
|
28
|
+
/** 折叠时头部右侧滚动展示的最新一行思考:取最后一行非空文本,随流式输出持续刷新 */
|
|
29
|
+
const latestReasoningLine = computed(() => {
|
|
30
|
+
if (!isThinking.value || reasoningExpanded.value) return "";
|
|
31
|
+
const lines = (props.message.reasoning ?? "").split("\n");
|
|
32
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
33
|
+
const line = lines[i]?.trim();
|
|
34
|
+
if (line) return line;
|
|
35
|
+
}
|
|
36
|
+
return "";
|
|
37
|
+
});
|
|
27
38
|
/** 思考区吸附底部:用户在思考区内上滚时取消吸附,回到底部后恢复 */
|
|
28
39
|
const reasoningStick = ref(true);
|
|
29
|
-
/**
|
|
40
|
+
/** 思考内容滚动区:展开且流式输出时吸附底部,始终展示最新思考 */
|
|
30
41
|
const reasoningScrollRef = useTemplateRef("reasoningScrollRef");
|
|
31
42
|
const toggleReasoning = async () => {
|
|
32
43
|
reasoningExpanded.value = !reasoningExpanded.value;
|
|
@@ -41,12 +52,6 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
41
52
|
const handleReasoningScroll = (position) => {
|
|
42
53
|
if (position.sh - position.ch - position.y < 8) reasoningStick.value = true;
|
|
43
54
|
};
|
|
44
|
-
watch(() => props.message.status, (status) => {
|
|
45
|
-
if (status === "streaming") {
|
|
46
|
-
reasoningExpanded.value = true;
|
|
47
|
-
reasoningStick.value = true;
|
|
48
|
-
} else if (status) reasoningExpanded.value = false;
|
|
49
|
-
}, { immediate: true });
|
|
50
55
|
watch(() => props.message.reasoning, async () => {
|
|
51
56
|
if (!isThinking.value || !reasoningExpanded.value || !reasoningStick.value) return;
|
|
52
57
|
await nextTick();
|
|
@@ -116,6 +121,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
116
121
|
_: 1
|
|
117
122
|
}, 8, ["class"]),
|
|
118
123
|
createElementVNode("span", { class: normalizeClass([unref(cls).e("reasoning-title"), isThinking.value && "u-shine"]) }, toDisplayString(isThinking.value ? "思考中…" : "思考过程"), 3),
|
|
124
|
+
createCommentVNode(" 折叠态在头部右侧滚动展示最新一行思考 "),
|
|
125
|
+
latestReasoningLine.value ? (openBlock(), createElementBlock("span", {
|
|
126
|
+
key: 0,
|
|
127
|
+
class: normalizeClass(unref(cls).e("reasoning-line"))
|
|
128
|
+
}, toDisplayString(latestReasoningLine.value), 3)) : createCommentVNode("v-if", true),
|
|
119
129
|
createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("reasoning-chevron")) }, {
|
|
120
130
|
default: withCtx(() => [createVNode(unref(ArrowRight))]),
|
|
121
131
|
_: 1
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-item.js","names":[],"sources":["../../../src/components/ai-chat/message-item.vue"],"sourcesContent":["<template>\n <div\n :class=\"[cls.e('message'), cls.em('message', message.role), bem.is('streaming', isStreaming)]\"\n >\n <!-- 用户消息:右对齐气泡 -->\n <template v-if=\"message.role === 'user'\">\n <div :class=\"cls.e('message-bubble')\">\n <div v-if=\"message.attachments?.length\" :class=\"cls.e('message-attachments')\">\n <img\n v-for=\"att in message.attachments\"\n :key=\"att.dataUrl\"\n :src=\"att.dataUrl\"\n :alt=\"att.name\"\n :class=\"cls.e('message-attachment')\"\n />\n </div>\n <div v-if=\"message.content\" :class=\"cls.e('message-text')\">{{ message.content }}</div>\n </div>\n </template>\n\n <!-- assistant 消息 -->\n <template v-else>\n <div\n v-if=\"message.reasoning\"\n :class=\"[cls.e('reasoning'), bem.is('expanded', reasoningExpanded)]\"\n >\n <div :class=\"cls.e('reasoning-header')\" @click=\"toggleReasoning\">\n <UIcon :class=\"[cls.e('reasoning-icon'), bem.is('loading', isThinking)]\">\n <Loading v-if=\"isThinking\" />\n <InfoCircle v-else />\n </UIcon>\n <span :class=\"[cls.e('reasoning-title'), isThinking && 'u-shine']\">\n {{ isThinking ? '思考中…' : '思考过程' }}\n </span>\n <UIcon :class=\"cls.e('reasoning-chevron')\"><ArrowRight /></UIcon>\n </div>\n <!-- 折叠时完全卸载内容 DOM:思考文本可能很长,常驻挂载浪费内存与渲染成本 -->\n <div v-if=\"reasoningExpanded\" :class=\"cls.e('reasoning-content')\">\n <UScroll\n ref=\"reasoningScrollRef\"\n container-style=\"max-height: 220px\"\n @scroll=\"handleReasoningScroll\"\n @wheel.passive=\"handleReasoningWheel\"\n >\n <div :class=\"cls.e('reasoning-text')\">{{ message.reasoning }}</div>\n </UScroll>\n </div>\n </div>\n\n <MarkdownRender\n v-if=\"message.content || isStreaming\"\n mode=\"chat\"\n :content=\"message.content\"\n :final=\"!isStreaming\"\n :fade=\"false\"\n v-bind=\"rendererProps\"\n />\n\n <ToolCallCard\n v-for=\"call in message.toolCalls ?? []\"\n :key=\"call.id\"\n :tool-call=\"call\"\n @respond=\"(approved) => emit('respond', call.id, approved)\"\n />\n\n <div v-if=\"message.status === 'error'\" :class=\"cls.e('message-status')\">生成出错</div>\n <div v-else-if=\"message.status === 'aborted'\" :class=\"cls.e('message-status')\">\n 已停止生成\n </div>\n\n <div v-if=\"showActions\" :class=\"cls.e('message-actions')\">\n <UButton\n size=\"small\"\n plain\n circle\n :type=\"copied ? 'success' : 'primary'\"\n :icon=\"copied ? CircleCheck : Copy\"\n :disabled=\"!canCopy\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"handleCopy\"\n />\n <UButton\n size=\"small\"\n plain\n circle\n type=\"primary\"\n :icon=\"Refresh\"\n title=\"重新生成\"\n @click=\"emit('regenerate')\"\n />\n </div>\n </template>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { message as toast, UButton, UIcon, UScroll } from '@veltra/desktop'\nimport type { ScrollPosition } from '@veltra/desktop'\nimport { ArrowRight, CircleCheck, Copy, InfoCircle, Loading, Refresh } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport MarkdownRender from 'markstream-vue'\nimport { computed, inject, nextTick, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport { AiChatDIKey } from './di'\nimport ToolCallCard from './tool-call.vue'\n\ndefineOptions({ name: 'UAiChatMessageItem' })\n\nconst props = defineProps<{\n message: ChatMessage\n /** 是否为最后一条可见消息(决定是否显示重新生成) */\n isLast: boolean\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{\n (e: 'respond', toolCallId: string, approved: boolean): void\n (e: 'regenerate'): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst isStreaming = computed(() => props.message.status === 'streaming')\n/** 思考中:流式且正文还未开始输出 */\nconst isThinking = computed(() => isStreaming.value && !props.message.content)\n\nconst reasoningExpanded = ref(true)\n\n/** 思考区吸附底部:用户在思考区内上滚时取消吸附,回到底部后恢复 */\nconst reasoningStick = ref(true)\n\n/** 思考内容滚动区:流式输出时吸附底部,始终展示最新思考 */\nconst reasoningScrollRef = useTemplateRef<InstanceType<typeof UScroll>>('reasoningScrollRef')\n\nconst toggleReasoning = async () => {\n reasoningExpanded.value = !reasoningExpanded.value\n if (!reasoningExpanded.value) return\n // 重新展开时恢复吸附并定位到最新思考\n reasoningStick.value = true\n await nextTick()\n reasoningScrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n}\n\nconst handleReasoningWheel = (e: WheelEvent) => {\n if (e.deltaY < 0) reasoningStick.value = false\n}\n\nconst handleReasoningScroll = (position: Required<ScrollPosition>) => {\n if (position.sh - position.ch - position.y < 8) reasoningStick.value = true\n}\n\n// 流式结束后自动折叠思考过程;immediate 保证终态消息重挂载(过程块展开/收起、载入历史会话)时呈折叠态\nwatch(\n () => props.message.status,\n (status) => {\n if (status === 'streaming') {\n reasoningExpanded.value = true\n reasoningStick.value = true\n } else if (status) {\n reasoningExpanded.value = false\n }\n },\n { immediate: true }\n)\n\nwatch(\n () => props.message.reasoning,\n async () => {\n if (!isThinking.value || !reasoningExpanded.value || !reasoningStick.value) return\n await nextTick()\n // 等待 DOM 更新期间用户可能已上滚,滚动前重新确认吸附状态\n if (!reasoningStick.value) return\n reasoningScrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }\n)\n\nconst showActions = computed(() => {\n if (props.message.role !== 'assistant' || !props.isLast) return false\n return ['done', 'error', 'aborted'].includes(props.message.status ?? '')\n})\n\nconst canCopy = computed(() => !!props.message.content?.trim())\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\n\nconst handleCopy = async () => {\n const text = props.message.content?.trim()\n if (!text) return\n try {\n await navigator.clipboard.writeText(text)\n copied.value = true\n toast.success('已复制到剪贴板')\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => {\n copied.value = false\n }, 2000)\n } catch {\n toast.error('复制失败,请检查浏览器权限')\n }\n}\n\nonBeforeUnmount(() => clearTimeout(copyTimer))\n</script>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"message-item.js","names":[],"sources":["../../../src/components/ai-chat/message-item.vue"],"sourcesContent":["<template>\n <div\n :class=\"[cls.e('message'), cls.em('message', message.role), bem.is('streaming', isStreaming)]\"\n >\n <!-- 用户消息:右对齐气泡 -->\n <template v-if=\"message.role === 'user'\">\n <div :class=\"cls.e('message-bubble')\">\n <div v-if=\"message.attachments?.length\" :class=\"cls.e('message-attachments')\">\n <img\n v-for=\"att in message.attachments\"\n :key=\"att.dataUrl\"\n :src=\"att.dataUrl\"\n :alt=\"att.name\"\n :class=\"cls.e('message-attachment')\"\n />\n </div>\n <div v-if=\"message.content\" :class=\"cls.e('message-text')\">{{ message.content }}</div>\n </div>\n </template>\n\n <!-- assistant 消息 -->\n <template v-else>\n <div\n v-if=\"message.reasoning\"\n :class=\"[cls.e('reasoning'), bem.is('expanded', reasoningExpanded)]\"\n >\n <div :class=\"cls.e('reasoning-header')\" @click=\"toggleReasoning\">\n <UIcon :class=\"[cls.e('reasoning-icon'), bem.is('loading', isThinking)]\">\n <Loading v-if=\"isThinking\" />\n <InfoCircle v-else />\n </UIcon>\n <span :class=\"[cls.e('reasoning-title'), isThinking && 'u-shine']\">\n {{ isThinking ? '思考中…' : '思考过程' }}\n </span>\n <!-- 折叠态在头部右侧滚动展示最新一行思考 -->\n <span v-if=\"latestReasoningLine\" :class=\"cls.e('reasoning-line')\">\n {{ latestReasoningLine }}\n </span>\n <UIcon :class=\"cls.e('reasoning-chevron')\"><ArrowRight /></UIcon>\n </div>\n <!-- 折叠时完全卸载内容 DOM:思考文本可能很长,常驻挂载浪费内存与渲染成本 -->\n <div v-if=\"reasoningExpanded\" :class=\"cls.e('reasoning-content')\">\n <UScroll\n ref=\"reasoningScrollRef\"\n container-style=\"max-height: 220px\"\n @scroll=\"handleReasoningScroll\"\n @wheel.passive=\"handleReasoningWheel\"\n >\n <div :class=\"cls.e('reasoning-text')\">{{ message.reasoning }}</div>\n </UScroll>\n </div>\n </div>\n\n <MarkdownRender\n v-if=\"message.content || isStreaming\"\n mode=\"chat\"\n :content=\"message.content\"\n :final=\"!isStreaming\"\n :fade=\"false\"\n v-bind=\"rendererProps\"\n />\n\n <ToolCallCard\n v-for=\"call in message.toolCalls ?? []\"\n :key=\"call.id\"\n :tool-call=\"call\"\n @respond=\"(approved) => emit('respond', call.id, approved)\"\n />\n\n <div v-if=\"message.status === 'error'\" :class=\"cls.e('message-status')\">生成出错</div>\n <div v-else-if=\"message.status === 'aborted'\" :class=\"cls.e('message-status')\">\n 已停止生成\n </div>\n\n <div v-if=\"showActions\" :class=\"cls.e('message-actions')\">\n <UButton\n size=\"small\"\n plain\n circle\n :type=\"copied ? 'success' : 'primary'\"\n :icon=\"copied ? CircleCheck : Copy\"\n :disabled=\"!canCopy\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"handleCopy\"\n />\n <UButton\n size=\"small\"\n plain\n circle\n type=\"primary\"\n :icon=\"Refresh\"\n title=\"重新生成\"\n @click=\"emit('regenerate')\"\n />\n </div>\n </template>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { message as toast, UButton, UIcon, UScroll } from '@veltra/desktop'\nimport type { ScrollPosition } from '@veltra/desktop'\nimport { ArrowRight, CircleCheck, Copy, InfoCircle, Loading, Refresh } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport MarkdownRender from 'markstream-vue'\nimport { computed, inject, nextTick, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport { AiChatDIKey } from './di'\nimport ToolCallCard from './tool-call.vue'\n\ndefineOptions({ name: 'UAiChatMessageItem' })\n\nconst props = defineProps<{\n message: ChatMessage\n /** 是否为最后一条可见消息(决定是否显示重新生成) */\n isLast: boolean\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{\n (e: 'respond', toolCallId: string, approved: boolean): void\n (e: 'regenerate'): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst isStreaming = computed(() => props.message.status === 'streaming')\n/** 思考中:流式且正文还未开始输出 */\nconst isThinking = computed(() => isStreaming.value && !props.message.content)\n\n/** 思考默认折叠(含流式进行中),用户点击头部展开 */\nconst reasoningExpanded = ref(false)\n\n/** 折叠时头部右侧滚动展示的最新一行思考:取最后一行非空文本,随流式输出持续刷新 */\nconst latestReasoningLine = computed(() => {\n if (!isThinking.value || reasoningExpanded.value) return ''\n const lines = (props.message.reasoning ?? '').split('\\n')\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i]?.trim()\n if (line) return line\n }\n return ''\n})\n\n/** 思考区吸附底部:用户在思考区内上滚时取消吸附,回到底部后恢复 */\nconst reasoningStick = ref(true)\n\n/** 思考内容滚动区:展开且流式输出时吸附底部,始终展示最新思考 */\nconst reasoningScrollRef = useTemplateRef<InstanceType<typeof UScroll>>('reasoningScrollRef')\n\nconst toggleReasoning = async () => {\n reasoningExpanded.value = !reasoningExpanded.value\n if (!reasoningExpanded.value) return\n // 重新展开时恢复吸附并定位到最新思考\n reasoningStick.value = true\n await nextTick()\n reasoningScrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n}\n\nconst handleReasoningWheel = (e: WheelEvent) => {\n if (e.deltaY < 0) reasoningStick.value = false\n}\n\nconst handleReasoningScroll = (position: Required<ScrollPosition>) => {\n if (position.sh - position.ch - position.y < 8) reasoningStick.value = true\n}\n\nwatch(\n () => props.message.reasoning,\n async () => {\n if (!isThinking.value || !reasoningExpanded.value || !reasoningStick.value) return\n await nextTick()\n // 等待 DOM 更新期间用户可能已上滚,滚动前重新确认吸附状态\n if (!reasoningStick.value) return\n reasoningScrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }\n)\n\nconst showActions = computed(() => {\n if (props.message.role !== 'assistant' || !props.isLast) return false\n return ['done', 'error', 'aborted'].includes(props.message.status ?? '')\n})\n\nconst canCopy = computed(() => !!props.message.content?.trim())\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\n\nconst handleCopy = async () => {\n const text = props.message.content?.trim()\n if (!text) return\n try {\n await navigator.clipboard.writeText(text)\n copied.value = true\n toast.success('已复制到剪贴板')\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => {\n copied.value = false\n }, 2000)\n } catch {\n toast.error('复制失败,请检查浏览器权限')\n }\n}\n\nonBeforeUnmount(() => clearTimeout(copyTimer))\n</script>\n"],"mappings":"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"}
|
|
@@ -205,7 +205,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
205
205
|
const stickToBottom = ref(true);
|
|
206
206
|
/** 上一次滚动位置:用于区分用户上翻与自动吸底触发的 scroll 事件 */
|
|
207
207
|
let lastScrollY = 0;
|
|
208
|
-
/**
|
|
208
|
+
/** 上翻阅读时展示回到底部的悬浮按钮 */
|
|
209
209
|
const showToLatest = computed(() => !stickToBottom.value && visibleMessages.value.length > 0);
|
|
210
210
|
const handleScroll = (position) => {
|
|
211
211
|
if (position.sh - position.ch - position.y < 60) stickToBottom.value = true;
|
|
@@ -263,7 +263,6 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
263
263
|
class: normalizeClass(unref(cls).e("list")),
|
|
264
264
|
ref_key: "scrollRef",
|
|
265
265
|
ref: scrollRef,
|
|
266
|
-
always: "",
|
|
267
266
|
onScroll: handleScroll,
|
|
268
267
|
onWheelPassive: handleWheel
|
|
269
268
|
}, {
|
|
@@ -360,11 +359,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
360
359
|
key: 0,
|
|
361
360
|
type: "button",
|
|
362
361
|
class: normalizeClass(unref(cls).e("to-latest")),
|
|
362
|
+
title: "回到最新",
|
|
363
363
|
onClick: scrollToLatest
|
|
364
364
|
}, [createVNode(unref(UIcon), null, {
|
|
365
365
|
default: withCtx(() => [createVNode(unref(ArrowDown))]),
|
|
366
366
|
_: 1
|
|
367
|
-
})
|
|
367
|
+
})], 2)) : createCommentVNode("v-if", true)]),
|
|
368
368
|
_: 1
|
|
369
369
|
}, 8, ["name"])
|
|
370
370
|
], 2);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-list.js","names":[],"sources":["../../../src/components/ai-chat/message-list.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('list-wrap')\">\n <UScroll\n :class=\"cls.e('list')\"\n ref=\"scrollRef\"\n always\n @scroll=\"handleScroll\"\n @wheel.passive=\"handleWheel\"\n >\n <template v-for=\"turn in turns\" :key=\"turn.key\">\n <MessageItem\n v-if=\"turn.userMsg\"\n :message=\"turn.userMsg\"\n :is-last=\"false\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n\n <!-- 最终答案开始输出后,本轮之前的思考/工具调用过程收进「已完成」折叠块 -->\n <TurnProcess\n v-if=\"turn.processCollapsed\"\n :messages=\"turn.processMsgs\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n />\n <template v-else>\n <MessageItem\n v-for=\"msg in turn.processMsgs\"\n :key=\"msg.id\"\n :message=\"msg\"\n :is-last=\"false\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n </template>\n\n <MessageItem\n v-if=\"turn.answerMsg\"\n :message=\"turn.answerMsg\"\n :is-last=\"turn.isLast\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n </template>\n\n <!-- 工作中:活体球从输入框上方跳到最新会话下面;结束停留后再跳回 -->\n <div v-if=\"showWorking\" :class=\"[cls.e('working'), bem.is('leaving', workingLeaving)]\">\n <UAiOrb ref=\"workingOrb\" :size=\"ORB_SIZE\" :status=\"workingOrbStatus\" />\n <span v-if=\"running\" :class=\"[cls.e('working-text'), 'u-shine']\">工作中…</span>\n </div>\n </UScroll>\n\n <!-- 空闲:始终钉在滚动容器外、输入框上方;工作开始时缩小离开、结束后放大回来 -->\n <div\n v-if=\"showIdleWelcome\"\n :class=\"[cls.e('welcome'), bem.is('leaving', idleLeaving), bem.is('enter', idleEnter)]\"\n >\n <slot name=\"welcome\">\n <div :class=\"cls.e('welcome-inner')\">\n <UAiOrb :size=\"ORB_SIZE\" @click=\"cycleWelcome\" />\n <!-- 快捷提问逐条展示在球右侧并自动轮换:点文案发送,点球立即换下一条。\n 固定锚点布局:球位置钉死不动,气泡只向右延展,避免轮换时水平瞬移 -->\n <div v-if=\"welcomeItems.length\" :class=\"cls.e('welcome-item-wrap')\">\n <Transition :name=\"cls.em('welcome', 'swap')\" mode=\"out-in\">\n <button\n v-if=\"currentWelcomeItem\"\n :key=\"welcomeIndex\"\n type=\"button\"\n :class=\"cls.e('welcome-item')\"\n :disabled=\"readonly\"\n @click=\"!readonly && emit('welcome-click', currentWelcomeItem)\"\n >\n {{ currentWelcomeItem }}\n </button>\n </Transition>\n </div>\n </div>\n </slot>\n </div>\n\n <!-- 用户上翻浏览时悬浮入口:一键回到最新消息 -->\n <Transition :name=\"cls.em('to-latest', 'fade')\">\n <button v-if=\"showToLatest\" type=\"button\" :class=\"cls.e('to-latest')\" @click=\"scrollToLatest\">\n <UIcon><ArrowDown /></UIcon>\n <span>最新消息</span>\n </button>\n </Transition>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon, UScroll } from '@veltra/desktop'\nimport type { ScrollPosition } from '@veltra/desktop'\nimport { ArrowDown } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport {\n computed,\n inject,\n nextTick,\n onBeforeUnmount,\n ref,\n shallowRef,\n useTemplateRef,\n watch\n} from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport type { AiOrbExposed, AiOrbReaction, AiOrbStatus } from '../../types/ai-orb'\nimport UAiOrb from '../ai-orb/ai-orb.vue'\nimport { AiChatDIKey } from './di'\nimport MessageItem from './message-item.vue'\nimport TurnProcess from './turn-process.vue'\n\ndefineOptions({ name: 'UAiChatMessageList' })\n\n/** 空闲欢迎 / 工作指示共用活体球直径(px) */\nconst ORB_SIZE = 48\n/** 生成结束 / 失败后工作球停留再跳回输入框上方 */\nconst WORKING_LINGER_MS = 2500\n/** 两处活体球对向缩放的时长(与样式中的 jump 过渡一致) */\nconst JUMP_MS = 380\n\nconst props = defineProps<{\n messages: ChatMessage[]\n /** 空闲时输入框上方的快捷提问;字符串按单项处理 */\n welcome?: string | string[]\n /** 是否生成中(控制底部工作状态指示) */\n running?: boolean\n /** 只读时欢迎语不可点击发送 */\n readonly?: boolean\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{\n (e: 'respond', toolCallId: string, approved: boolean): void\n (e: 'regenerate'): void\n /** 点击欢迎项(快捷提问) */\n (e: 'welcome-click', text: string): void\n}>()\n\ndefineSlots<{\n /** 空闲欢迎区插槽(输入框上方;工作中跳到列表末尾) */\n welcome(): any\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** tool 消息不单独渲染,其内容体现在工具卡片的结果区 */\nconst visibleMessages = computed(() => props.messages.filter((msg) => msg.role !== 'tool'))\n\n/** 一轮对话:以 user 消息为界,含本轮全部 assistant 消息(每次工具循环产生一条) */\ninterface ChatTurn {\n /** 轮次 key(取用户消息或首条 assistant 消息 id) */\n key: string\n userMsg?: ChatMessage\n /** 最终答案之前的过程消息(思考 + 中间文本 + 工具卡片) */\n processMsgs: ChatMessage[]\n /** 本轮最终答案(最后一条 assistant 消息) */\n answerMsg?: ChatMessage\n /** 过程消息是否收进「已完成」折叠块 */\n processCollapsed: boolean\n /** answerMsg 是否为全局最后一条可见消息 */\n isLast: boolean\n}\n\nconst isFinalStatus = (msg?: ChatMessage) =>\n msg?.status === 'done' || msg?.status === 'error' || msg?.status === 'aborted'\n\nconst turns = computed<ChatTurn[]>(() => {\n const grouped: { key: string; userMsg?: ChatMessage; assistants: ChatMessage[] }[] = []\n for (const msg of visibleMessages.value) {\n if (msg.role === 'user') {\n grouped.push({ key: msg.id, userMsg: msg, assistants: [] })\n continue\n }\n const last = grouped[grouped.length - 1]\n // 兼容 assistant 开头的历史会话:归入无用户消息的轮次\n if (last) last.assistants.push(msg)\n else grouped.push({ key: msg.id, assistants: [msg] })\n }\n return grouped.map((turn, index) => {\n const processMsgs = turn.assistants.slice(0, -1)\n const answerMsg = turn.assistants[turn.assistants.length - 1]\n const isLastTurn = index === grouped.length - 1\n // 该轮结束(非最后一轮 / 答案进入终态)或最终答案开始输出时折叠过程。\n // content 只增不减,折叠后不会回弹;regenerate 删除本轮 assistant 重跑,条件自然复位\n const processCollapsed =\n processMsgs.length > 0 &&\n (!isLastTurn || !!answerMsg?.content || (!props.running && isFinalStatus(answerMsg)))\n return {\n key: turn.key,\n userMsg: turn.userMsg,\n processMsgs,\n answerMsg,\n processCollapsed,\n isLast: isLastTurn && !!answerMsg\n }\n })\n})\n\nconst welcomeItems = computed(() => {\n const welcome = props.welcome\n if (!welcome) return []\n return (Array.isArray(welcome) ? welcome : [welcome]).filter(Boolean)\n})\n\n/** 欢迎语逐条轮换:当前展示项的下标 */\nconst welcomeIndex = ref(0)\n\n/** 当前展示的欢迎语 */\nconst currentWelcomeItem = computed(() => {\n const items = welcomeItems.value\n return items.length ? items[welcomeIndex.value % items.length] : undefined\n})\n\nlet welcomeTimer: ReturnType<typeof setInterval> | undefined\n\nconst stopWelcomeRotation = () => {\n clearInterval(welcomeTimer)\n welcomeTimer = undefined\n}\n\nconst startWelcomeRotation = () => {\n stopWelcomeRotation()\n if (welcomeItems.value.length > 1) {\n welcomeTimer = setInterval(() => {\n welcomeIndex.value += 1\n }, 4000)\n }\n}\n\n/** 点击欢迎球:不等自动轮换,立即换下一条,并重置计时 */\nconst cycleWelcome = () => {\n if (welcomeItems.value.length <= 1) return\n welcomeIndex.value += 1\n startWelcomeRotation()\n}\n\n/** 回答完毕 / 失败后工作球短暂停留,播完表情再跳回输入框上方 */\nconst finishing = ref(false)\n/** 列表中的球正在缩小离开 */\nconst workingLeaving = ref(false)\n/** 输入框上方的球正在缩小离开 */\nconst idleLeaving = ref(false)\n/** 输入框上方的球从工作位放大回来(首屏出现不播) */\nconst idleEnter = ref(false)\n\nlet finishTimer: ReturnType<typeof setTimeout> | undefined\nlet jumpTimer: ReturnType<typeof setTimeout> | undefined\n\nconst clearJumpTimers = () => {\n clearTimeout(finishTimer)\n clearTimeout(jumpTimer)\n}\n\n/** 列表末尾的工作球:生成中 / 收尾停留 / 跳回过渡 */\nconst showWorking = computed(() => !!(props.running || finishing.value || workingLeaving.value))\n/** 输入框上方的空闲欢迎:含工作开始时的缩小离开帧 */\nconst showIdleWelcome = computed(() => (!props.running && !finishing.value) || idleLeaving.value)\n\nconst workingOrbRef = useTemplateRef<AiOrbExposed>('workingOrb')\n\n/** 工具调用失败的 id 去重集合:同一次失败只播一次沮丧表情 */\nconst reactedToolErrorIds = new Set<string>()\n\nconst toolErrorIds = computed(() => {\n const ids: string[] = []\n for (const msg of props.messages) {\n if (msg.role !== 'assistant' || !msg.toolCalls) continue\n for (const call of msg.toolCalls) {\n if (call.status === 'error') ids.push(call.id)\n }\n }\n return ids\n})\n\n// 工具调用失败 → 工作球闭眼摇头(frustrated)\nwatch(toolErrorIds, (ids) => {\n for (const id of ids) {\n if (reactedToolErrorIds.has(id)) continue\n reactedToolErrorIds.add(id)\n workingOrbRef.value?.react('frustrated')\n }\n})\n\n/** 工作球状态:收尾停留 → idle;正文输出中 → speaking;其余生成阶段 → thinking */\nconst workingOrbStatus = computed<AiOrbStatus>(() => {\n if (finishing.value) return 'idle'\n const last = visibleMessages.value[visibleMessages.value.length - 1]\n if (last?.role === 'assistant' && last.status === 'streaming' && last.content) {\n return 'speaking'\n }\n return 'thinking'\n})\n\nwatch(\n () => props.running,\n (running, prev) => {\n if (running) {\n // 工作开始:立即从输入框上方跳到列表,无停留\n clearJumpTimers()\n const idleWasVisible = !prev && !finishing.value && !workingLeaving.value\n finishing.value = false\n workingLeaving.value = false\n idleEnter.value = false\n if (idleWasVisible) {\n idleLeaving.value = true\n jumpTimer = setTimeout(() => {\n idleLeaving.value = false\n }, JUMP_MS)\n } else {\n idleLeaving.value = false\n }\n return\n }\n if (!prev) return\n\n // 清除会话后列表已空:跳过结束停留,立刻回到欢迎区\n if (visibleMessages.value.length === 0) {\n finishing.value = false\n workingLeaving.value = false\n idleLeaving.value = false\n idleEnter.value = false\n return\n }\n\n finishing.value = true\n idleLeaving.value = false\n const last = visibleMessages.value[visibleMessages.value.length - 1]\n let reaction: AiOrbReaction | undefined\n if (last?.role === 'assistant') {\n if (last.status === 'done') reaction = 'happy'\n else if (last.status === 'error') reaction = 'frustrated'\n }\n if (reaction) {\n nextTick(() => workingOrbRef.value?.react(reaction))\n }\n // 停留结束 → 列表中缩小、输入框上方放大\n finishTimer = setTimeout(() => {\n finishing.value = false\n workingLeaving.value = true\n idleEnter.value = true\n jumpTimer = setTimeout(() => {\n workingLeaving.value = false\n idleEnter.value = false\n }, JUMP_MS)\n }, WORKING_LINGER_MS)\n }\n)\n\n// 消息被清空(clear):立刻收掉工作球,欢迎区恢复\nwatch(\n () => visibleMessages.value.length,\n (len) => {\n if (len > 0) return\n clearJumpTimers()\n finishing.value = false\n workingLeaving.value = false\n idleLeaving.value = false\n idleEnter.value = false\n }\n)\n\n// 空闲欢迎区展示期间自动轮换;跳到工作位后停止\nwatch(\n () => [welcomeItems.value.length, showIdleWelcome.value] as const,\n () => {\n if (showIdleWelcome.value) startWelcomeRotation()\n else stopWelcomeRotation()\n },\n { immediate: true }\n)\n\nonBeforeUnmount(() => {\n stopWelcomeRotation()\n clearJumpTimers()\n})\n\nconst scrollRef = shallowRef<{ scrollTo: (position: ScrollPosition) => void }>()\n/** 是否吸附底部(用户上翻浏览历史时取消吸附) */\nconst stickToBottom = ref(true)\n/** 上一次滚动位置:用于区分用户上翻与自动吸底触发的 scroll 事件 */\nlet lastScrollY = 0\n\n/** 上翻阅读时展示「最新消息」悬浮按钮 */\nconst showToLatest = computed(() => !stickToBottom.value && visibleMessages.value.length > 0)\n\nconst handleScroll = (position: Required<ScrollPosition>) => {\n const atBottom = position.sh - position.ch - position.y < 60\n if (atBottom) {\n stickToBottom.value = true\n } else if (position.y < lastScrollY) {\n // 只有「向上滚」才算用户离开底部的意图:自动吸底的 scroll 事件可能滞后于\n // 流式内容增长(sh 已变大、y 仍是旧底部),按位置判断会误判成未吸底\n stickToBottom.value = false\n }\n lastScrollY = position.y\n}\n\n/**\n * 用户向上滚动轮 = 明确的离开底部意图,立即取消吸附。\n * 不能等 scroll 事件再按方向判断:流式期间每个 chunk 都会排一次吸底滚动,\n * 滞后的 scrollTo 会把用户拉回底部并触发 atBottom 重新吸附,导致「上滚动不了」\n */\nconst handleWheel = (e: WheelEvent) => {\n if (e.deltaY < 0) stickToBottom.value = false\n}\n\n/** 点击悬浮按钮:恢复吸附并回到底部 */\nconst scrollToLatest = async () => {\n stickToBottom.value = true\n await nextTick()\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n}\n\n// 消息数量或流式内容变化时,若处于吸附状态则滚动到底部\nwatch(\n () => {\n const list = visibleMessages.value\n const last = list[list.length - 1]\n return [\n list.length,\n last?.content.length ?? 0,\n last?.reasoning?.length ?? 0,\n last?.toolCalls?.length ?? 0,\n props.running\n ].join('|')\n },\n async () => {\n // 用户刚发出消息时强制回到底部(无论之前是否在上翻)\n const list = visibleMessages.value\n if (list[list.length - 1]?.role === 'user') stickToBottom.value = true\n if (!stickToBottom.value) return\n await nextTick()\n // 等待 DOM 更新期间用户可能已上滚,滚动前重新确认吸附状态,避免把用户拉回底部\n if (!stickToBottom.value) return\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }\n)\n\nlet trailingScrollTimer: ReturnType<typeof setTimeout> | undefined\n\n// 生成收尾时动作区出现 / markdown 终态渲染 / 思考块折叠会再次改变高度,\n// 同帧滚动可能够不到最终底部,追加一次延迟兜底滚动\nwatch(\n () => props.running,\n async (running) => {\n if (running) return\n clearTimeout(trailingScrollTimer)\n if (!stickToBottom.value) return\n await nextTick()\n if (!stickToBottom.value) return\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n trailingScrollTimer = setTimeout(() => {\n if (stickToBottom.value) scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }, 150)\n }\n)\n\nonBeforeUnmount(() => clearTimeout(trailingScrollTimer))\n</script>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"message-list.js","names":[],"sources":["../../../src/components/ai-chat/message-list.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('list-wrap')\">\n <UScroll\n :class=\"cls.e('list')\"\n ref=\"scrollRef\"\n @scroll=\"handleScroll\"\n @wheel.passive=\"handleWheel\"\n >\n <template v-for=\"turn in turns\" :key=\"turn.key\">\n <MessageItem\n v-if=\"turn.userMsg\"\n :message=\"turn.userMsg\"\n :is-last=\"false\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n\n <!-- 最终答案开始输出后,本轮之前的思考/工具调用过程收进「已完成」折叠块 -->\n <TurnProcess\n v-if=\"turn.processCollapsed\"\n :messages=\"turn.processMsgs\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n />\n <template v-else>\n <MessageItem\n v-for=\"msg in turn.processMsgs\"\n :key=\"msg.id\"\n :message=\"msg\"\n :is-last=\"false\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n </template>\n\n <MessageItem\n v-if=\"turn.answerMsg\"\n :message=\"turn.answerMsg\"\n :is-last=\"turn.isLast\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n </template>\n\n <!-- 工作中:活体球从输入框上方跳到最新会话下面;结束停留后再跳回 -->\n <div v-if=\"showWorking\" :class=\"[cls.e('working'), bem.is('leaving', workingLeaving)]\">\n <UAiOrb ref=\"workingOrb\" :size=\"ORB_SIZE\" :status=\"workingOrbStatus\" />\n <span v-if=\"running\" :class=\"[cls.e('working-text'), 'u-shine']\">工作中…</span>\n </div>\n </UScroll>\n\n <!-- 空闲:始终钉在滚动容器外、输入框上方;工作开始时缩小离开、结束后放大回来 -->\n <div\n v-if=\"showIdleWelcome\"\n :class=\"[cls.e('welcome'), bem.is('leaving', idleLeaving), bem.is('enter', idleEnter)]\"\n >\n <slot name=\"welcome\">\n <div :class=\"cls.e('welcome-inner')\">\n <UAiOrb :size=\"ORB_SIZE\" @click=\"cycleWelcome\" />\n <!-- 快捷提问逐条展示在球右侧并自动轮换:点文案发送,点球立即换下一条。\n 固定锚点布局:球位置钉死不动,气泡只向右延展,避免轮换时水平瞬移 -->\n <div v-if=\"welcomeItems.length\" :class=\"cls.e('welcome-item-wrap')\">\n <Transition :name=\"cls.em('welcome', 'swap')\" mode=\"out-in\">\n <button\n v-if=\"currentWelcomeItem\"\n :key=\"welcomeIndex\"\n type=\"button\"\n :class=\"cls.e('welcome-item')\"\n :disabled=\"readonly\"\n @click=\"!readonly && emit('welcome-click', currentWelcomeItem)\"\n >\n {{ currentWelcomeItem }}\n </button>\n </Transition>\n </div>\n </div>\n </slot>\n </div>\n\n <!-- 用户上翻浏览时悬浮入口:一键回到最新消息 -->\n <Transition :name=\"cls.em('to-latest', 'fade')\">\n <button\n v-if=\"showToLatest\"\n type=\"button\"\n :class=\"cls.e('to-latest')\"\n title=\"回到最新\"\n @click=\"scrollToLatest\"\n >\n <UIcon><ArrowDown /></UIcon>\n </button>\n </Transition>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon, UScroll } from '@veltra/desktop'\nimport type { ScrollPosition } from '@veltra/desktop'\nimport { ArrowDown } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport {\n computed,\n inject,\n nextTick,\n onBeforeUnmount,\n ref,\n shallowRef,\n useTemplateRef,\n watch\n} from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport type { AiOrbExposed, AiOrbReaction, AiOrbStatus } from '../../types/ai-orb'\nimport UAiOrb from '../ai-orb/ai-orb.vue'\nimport { AiChatDIKey } from './di'\nimport MessageItem from './message-item.vue'\nimport TurnProcess from './turn-process.vue'\n\ndefineOptions({ name: 'UAiChatMessageList' })\n\n/** 空闲欢迎 / 工作指示共用活体球直径(px) */\nconst ORB_SIZE = 48\n/** 生成结束 / 失败后工作球停留再跳回输入框上方 */\nconst WORKING_LINGER_MS = 2500\n/** 两处活体球对向缩放的时长(与样式中的 jump 过渡一致) */\nconst JUMP_MS = 380\n\nconst props = defineProps<{\n messages: ChatMessage[]\n /** 空闲时输入框上方的快捷提问;字符串按单项处理 */\n welcome?: string | string[]\n /** 是否生成中(控制底部工作状态指示) */\n running?: boolean\n /** 只读时欢迎语不可点击发送 */\n readonly?: boolean\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{\n (e: 'respond', toolCallId: string, approved: boolean): void\n (e: 'regenerate'): void\n /** 点击欢迎项(快捷提问) */\n (e: 'welcome-click', text: string): void\n}>()\n\ndefineSlots<{\n /** 空闲欢迎区插槽(输入框上方;工作中跳到列表末尾) */\n welcome(): any\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** tool 消息不单独渲染,其内容体现在工具卡片的结果区 */\nconst visibleMessages = computed(() => props.messages.filter((msg) => msg.role !== 'tool'))\n\n/** 一轮对话:以 user 消息为界,含本轮全部 assistant 消息(每次工具循环产生一条) */\ninterface ChatTurn {\n /** 轮次 key(取用户消息或首条 assistant 消息 id) */\n key: string\n userMsg?: ChatMessage\n /** 最终答案之前的过程消息(思考 + 中间文本 + 工具卡片) */\n processMsgs: ChatMessage[]\n /** 本轮最终答案(最后一条 assistant 消息) */\n answerMsg?: ChatMessage\n /** 过程消息是否收进「已完成」折叠块 */\n processCollapsed: boolean\n /** answerMsg 是否为全局最后一条可见消息 */\n isLast: boolean\n}\n\nconst isFinalStatus = (msg?: ChatMessage) =>\n msg?.status === 'done' || msg?.status === 'error' || msg?.status === 'aborted'\n\nconst turns = computed<ChatTurn[]>(() => {\n const grouped: { key: string; userMsg?: ChatMessage; assistants: ChatMessage[] }[] = []\n for (const msg of visibleMessages.value) {\n if (msg.role === 'user') {\n grouped.push({ key: msg.id, userMsg: msg, assistants: [] })\n continue\n }\n const last = grouped[grouped.length - 1]\n // 兼容 assistant 开头的历史会话:归入无用户消息的轮次\n if (last) last.assistants.push(msg)\n else grouped.push({ key: msg.id, assistants: [msg] })\n }\n return grouped.map((turn, index) => {\n const processMsgs = turn.assistants.slice(0, -1)\n const answerMsg = turn.assistants[turn.assistants.length - 1]\n const isLastTurn = index === grouped.length - 1\n // 该轮结束(非最后一轮 / 答案进入终态)或最终答案开始输出时折叠过程。\n // content 只增不减,折叠后不会回弹;regenerate 删除本轮 assistant 重跑,条件自然复位\n const processCollapsed =\n processMsgs.length > 0 &&\n (!isLastTurn || !!answerMsg?.content || (!props.running && isFinalStatus(answerMsg)))\n return {\n key: turn.key,\n userMsg: turn.userMsg,\n processMsgs,\n answerMsg,\n processCollapsed,\n isLast: isLastTurn && !!answerMsg\n }\n })\n})\n\nconst welcomeItems = computed(() => {\n const welcome = props.welcome\n if (!welcome) return []\n return (Array.isArray(welcome) ? welcome : [welcome]).filter(Boolean)\n})\n\n/** 欢迎语逐条轮换:当前展示项的下标 */\nconst welcomeIndex = ref(0)\n\n/** 当前展示的欢迎语 */\nconst currentWelcomeItem = computed(() => {\n const items = welcomeItems.value\n return items.length ? items[welcomeIndex.value % items.length] : undefined\n})\n\nlet welcomeTimer: ReturnType<typeof setInterval> | undefined\n\nconst stopWelcomeRotation = () => {\n clearInterval(welcomeTimer)\n welcomeTimer = undefined\n}\n\nconst startWelcomeRotation = () => {\n stopWelcomeRotation()\n if (welcomeItems.value.length > 1) {\n welcomeTimer = setInterval(() => {\n welcomeIndex.value += 1\n }, 4000)\n }\n}\n\n/** 点击欢迎球:不等自动轮换,立即换下一条,并重置计时 */\nconst cycleWelcome = () => {\n if (welcomeItems.value.length <= 1) return\n welcomeIndex.value += 1\n startWelcomeRotation()\n}\n\n/** 回答完毕 / 失败后工作球短暂停留,播完表情再跳回输入框上方 */\nconst finishing = ref(false)\n/** 列表中的球正在缩小离开 */\nconst workingLeaving = ref(false)\n/** 输入框上方的球正在缩小离开 */\nconst idleLeaving = ref(false)\n/** 输入框上方的球从工作位放大回来(首屏出现不播) */\nconst idleEnter = ref(false)\n\nlet finishTimer: ReturnType<typeof setTimeout> | undefined\nlet jumpTimer: ReturnType<typeof setTimeout> | undefined\n\nconst clearJumpTimers = () => {\n clearTimeout(finishTimer)\n clearTimeout(jumpTimer)\n}\n\n/** 列表末尾的工作球:生成中 / 收尾停留 / 跳回过渡 */\nconst showWorking = computed(() => !!(props.running || finishing.value || workingLeaving.value))\n/** 输入框上方的空闲欢迎:含工作开始时的缩小离开帧 */\nconst showIdleWelcome = computed(() => (!props.running && !finishing.value) || idleLeaving.value)\n\nconst workingOrbRef = useTemplateRef<AiOrbExposed>('workingOrb')\n\n/** 工具调用失败的 id 去重集合:同一次失败只播一次沮丧表情 */\nconst reactedToolErrorIds = new Set<string>()\n\nconst toolErrorIds = computed(() => {\n const ids: string[] = []\n for (const msg of props.messages) {\n if (msg.role !== 'assistant' || !msg.toolCalls) continue\n for (const call of msg.toolCalls) {\n if (call.status === 'error') ids.push(call.id)\n }\n }\n return ids\n})\n\n// 工具调用失败 → 工作球闭眼摇头(frustrated)\nwatch(toolErrorIds, (ids) => {\n for (const id of ids) {\n if (reactedToolErrorIds.has(id)) continue\n reactedToolErrorIds.add(id)\n workingOrbRef.value?.react('frustrated')\n }\n})\n\n/** 工作球状态:收尾停留 → idle;正文输出中 → speaking;其余生成阶段 → thinking */\nconst workingOrbStatus = computed<AiOrbStatus>(() => {\n if (finishing.value) return 'idle'\n const last = visibleMessages.value[visibleMessages.value.length - 1]\n if (last?.role === 'assistant' && last.status === 'streaming' && last.content) {\n return 'speaking'\n }\n return 'thinking'\n})\n\nwatch(\n () => props.running,\n (running, prev) => {\n if (running) {\n // 工作开始:立即从输入框上方跳到列表,无停留\n clearJumpTimers()\n const idleWasVisible = !prev && !finishing.value && !workingLeaving.value\n finishing.value = false\n workingLeaving.value = false\n idleEnter.value = false\n if (idleWasVisible) {\n idleLeaving.value = true\n jumpTimer = setTimeout(() => {\n idleLeaving.value = false\n }, JUMP_MS)\n } else {\n idleLeaving.value = false\n }\n return\n }\n if (!prev) return\n\n // 清除会话后列表已空:跳过结束停留,立刻回到欢迎区\n if (visibleMessages.value.length === 0) {\n finishing.value = false\n workingLeaving.value = false\n idleLeaving.value = false\n idleEnter.value = false\n return\n }\n\n finishing.value = true\n idleLeaving.value = false\n const last = visibleMessages.value[visibleMessages.value.length - 1]\n let reaction: AiOrbReaction | undefined\n if (last?.role === 'assistant') {\n if (last.status === 'done') reaction = 'happy'\n else if (last.status === 'error') reaction = 'frustrated'\n }\n if (reaction) {\n nextTick(() => workingOrbRef.value?.react(reaction))\n }\n // 停留结束 → 列表中缩小、输入框上方放大\n finishTimer = setTimeout(() => {\n finishing.value = false\n workingLeaving.value = true\n idleEnter.value = true\n jumpTimer = setTimeout(() => {\n workingLeaving.value = false\n idleEnter.value = false\n }, JUMP_MS)\n }, WORKING_LINGER_MS)\n }\n)\n\n// 消息被清空(clear):立刻收掉工作球,欢迎区恢复\nwatch(\n () => visibleMessages.value.length,\n (len) => {\n if (len > 0) return\n clearJumpTimers()\n finishing.value = false\n workingLeaving.value = false\n idleLeaving.value = false\n idleEnter.value = false\n }\n)\n\n// 空闲欢迎区展示期间自动轮换;跳到工作位后停止\nwatch(\n () => [welcomeItems.value.length, showIdleWelcome.value] as const,\n () => {\n if (showIdleWelcome.value) startWelcomeRotation()\n else stopWelcomeRotation()\n },\n { immediate: true }\n)\n\nonBeforeUnmount(() => {\n stopWelcomeRotation()\n clearJumpTimers()\n})\n\nconst scrollRef = shallowRef<{ scrollTo: (position: ScrollPosition) => void }>()\n/** 是否吸附底部(用户上翻浏览历史时取消吸附) */\nconst stickToBottom = ref(true)\n/** 上一次滚动位置:用于区分用户上翻与自动吸底触发的 scroll 事件 */\nlet lastScrollY = 0\n\n/** 上翻阅读时展示回到底部的悬浮按钮 */\nconst showToLatest = computed(() => !stickToBottom.value && visibleMessages.value.length > 0)\n\nconst handleScroll = (position: Required<ScrollPosition>) => {\n const atBottom = position.sh - position.ch - position.y < 60\n if (atBottom) {\n stickToBottom.value = true\n } else if (position.y < lastScrollY) {\n // 只有「向上滚」才算用户离开底部的意图:自动吸底的 scroll 事件可能滞后于\n // 流式内容增长(sh 已变大、y 仍是旧底部),按位置判断会误判成未吸底\n stickToBottom.value = false\n }\n lastScrollY = position.y\n}\n\n/**\n * 用户向上滚动轮 = 明确的离开底部意图,立即取消吸附。\n * 不能等 scroll 事件再按方向判断:流式期间每个 chunk 都会排一次吸底滚动,\n * 滞后的 scrollTo 会把用户拉回底部并触发 atBottom 重新吸附,导致「上滚动不了」\n */\nconst handleWheel = (e: WheelEvent) => {\n if (e.deltaY < 0) stickToBottom.value = false\n}\n\n/** 点击悬浮按钮:恢复吸附并回到底部 */\nconst scrollToLatest = async () => {\n stickToBottom.value = true\n await nextTick()\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n}\n\n// 消息数量或流式内容变化时,若处于吸附状态则滚动到底部\nwatch(\n () => {\n const list = visibleMessages.value\n const last = list[list.length - 1]\n return [\n list.length,\n last?.content.length ?? 0,\n last?.reasoning?.length ?? 0,\n last?.toolCalls?.length ?? 0,\n props.running\n ].join('|')\n },\n async () => {\n // 用户刚发出消息时强制回到底部(无论之前是否在上翻)\n const list = visibleMessages.value\n if (list[list.length - 1]?.role === 'user') stickToBottom.value = true\n if (!stickToBottom.value) return\n await nextTick()\n // 等待 DOM 更新期间用户可能已上滚,滚动前重新确认吸附状态,避免把用户拉回底部\n if (!stickToBottom.value) return\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }\n)\n\nlet trailingScrollTimer: ReturnType<typeof setTimeout> | undefined\n\n// 生成收尾时动作区出现 / markdown 终态渲染 / 思考块折叠会再次改变高度,\n// 同帧滚动可能够不到最终底部,追加一次延迟兜底滚动\nwatch(\n () => props.running,\n async (running) => {\n if (running) return\n clearTimeout(trailingScrollTimer)\n if (!stickToBottom.value) return\n await nextTick()\n if (!stickToBottom.value) return\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n trailingScrollTimer = setTimeout(() => {\n if (stickToBottom.value) scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }, 150)\n }\n)\n\nonBeforeUnmount(() => clearTimeout(trailingScrollTimer))\n</script>\n"],"mappings":"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"}
|
|
@@ -173,6 +173,8 @@
|
|
|
173
173
|
max-width: 800px;
|
|
174
174
|
display: flex;
|
|
175
175
|
flex-direction: column;
|
|
176
|
+
padding: var(--u-gap-default) 0;
|
|
177
|
+
box-sizing: border-box;
|
|
176
178
|
}
|
|
177
179
|
.u-ai-chat__list {
|
|
178
180
|
flex: 1;
|
|
@@ -300,21 +302,22 @@
|
|
|
300
302
|
}
|
|
301
303
|
.u-ai-chat__to-latest {
|
|
302
304
|
position: absolute;
|
|
303
|
-
|
|
305
|
+
bottom: var(--u-gap-small);
|
|
304
306
|
left: 50%;
|
|
305
307
|
transform: translateX(-50%);
|
|
306
308
|
z-index: 1;
|
|
309
|
+
width: 28px;
|
|
310
|
+
height: 28px;
|
|
311
|
+
padding: 0;
|
|
307
312
|
display: flex;
|
|
308
313
|
justify-content: center;
|
|
309
314
|
align-items: center;
|
|
310
315
|
flex-wrap: nowrap;
|
|
311
|
-
gap: 4px;
|
|
312
|
-
padding: 4px 12px;
|
|
313
316
|
border: none;
|
|
314
|
-
border-radius:
|
|
317
|
+
border-radius: 50%;
|
|
315
318
|
background-color: var(--u-bg-color-top);
|
|
316
319
|
color: var(--u-text-color-second);
|
|
317
|
-
font-size:
|
|
320
|
+
font-size: 14px;
|
|
318
321
|
box-shadow: 0 2px 8px var(--u-shadow-color-a-20);
|
|
319
322
|
cursor: pointer;
|
|
320
323
|
transition: color 0.15s ease;
|
|
@@ -330,11 +333,11 @@
|
|
|
330
333
|
}
|
|
331
334
|
.u-ai-chat__to-latest--fade-enter-from {
|
|
332
335
|
opacity: 0;
|
|
333
|
-
transform: translateX(-50%) translateY(
|
|
336
|
+
transform: translateX(-50%) translateY(4px);
|
|
334
337
|
}
|
|
335
338
|
.u-ai-chat__to-latest--fade-leave-to {
|
|
336
339
|
opacity: 0;
|
|
337
|
-
transform: translateX(-50%) translateY(
|
|
340
|
+
transform: translateX(-50%) translateY(4px);
|
|
338
341
|
}
|
|
339
342
|
.u-ai-chat__message {
|
|
340
343
|
padding: var(--u-gap-small) var(--u-gap-default);
|
|
@@ -426,7 +429,17 @@
|
|
|
426
429
|
font-size: var(--u-font-size-main-small);
|
|
427
430
|
}
|
|
428
431
|
.u-ai-chat__reasoning-title {
|
|
432
|
+
flex-shrink: 0;
|
|
433
|
+
line-height: 1.5;
|
|
434
|
+
}
|
|
435
|
+
.u-ai-chat__reasoning-line {
|
|
436
|
+
flex: 1;
|
|
437
|
+
min-width: 0;
|
|
438
|
+
overflow: hidden;
|
|
439
|
+
text-overflow: ellipsis;
|
|
440
|
+
white-space: nowrap;
|
|
429
441
|
line-height: 1.5;
|
|
442
|
+
color: var(--u-text-color-assist);
|
|
430
443
|
}
|
|
431
444
|
.u-ai-chat__reasoning-chevron {
|
|
432
445
|
font-size: 12px;
|
|
@@ -40,12 +40,25 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
40
40
|
};
|
|
41
41
|
/** 面板工具:render 展示在右侧侧边面板,卡片 body 仅保留「查看面板」入口 */
|
|
42
42
|
const isPanelTool = computed(() => tool.value?.renderTo === "panel");
|
|
43
|
-
/**
|
|
44
|
-
const
|
|
43
|
+
/** 「UI 即答复」的终结工具(session 元信息无 terminal,恒为 false) */
|
|
44
|
+
const isAnswerTool = computed(() => {
|
|
45
|
+
const t = tool.value;
|
|
46
|
+
return !!(t && "terminal" in t && t.terminal);
|
|
47
|
+
});
|
|
48
|
+
/** 内联自定义 body(render / tool-<name> 插槽):进行中可能是提问表单等交互 UI,需展开可见 */
|
|
49
|
+
const hasInlineBody = computed(() => !isPanelTool.value && !!(tool.value?.render || di?.slots[`tool-${props.toolCall.name}`]));
|
|
50
|
+
/** 完成后是否自动折叠:缺省一律折叠;「UI 即答复」的终结工具缺省保持展开 */
|
|
51
|
+
const autoCollapse = computed(() => tool.value?.autoCollapse ?? !isAnswerTool.value);
|
|
52
|
+
/** 需要用户交互/展示的进行中状态:待确认(看参数再决策)、有内联自定义 body(提问表单等) */
|
|
53
|
+
const needsInteraction = computed(() => {
|
|
54
|
+
if (props.toolCall.status === "awaiting-confirm") return true;
|
|
55
|
+
return (props.toolCall.status === "pending" || props.toolCall.status === "running") && hasInlineBody.value;
|
|
56
|
+
});
|
|
57
|
+
const isSettledStatus = (status) => status === "success" || status === "error" || status === "rejected";
|
|
45
58
|
watch(() => props.toolCall.status, (status) => {
|
|
46
59
|
if (userToggled) return;
|
|
47
|
-
if (status
|
|
48
|
-
else isExpanded.value =
|
|
60
|
+
if (isSettledStatus(status)) isExpanded.value = !autoCollapse.value;
|
|
61
|
+
else isExpanded.value = needsInteraction.value;
|
|
49
62
|
}, { immediate: true });
|
|
50
63
|
const STATUS_CLASS = {
|
|
51
64
|
pending: "is-active",
|
|
@@ -61,13 +74,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
61
74
|
return props.toolCall.status === "pending" || props.toolCall.status === "running";
|
|
62
75
|
});
|
|
63
76
|
/** 终态(成功/失败/拒绝) */
|
|
64
|
-
const isSettled = computed(() =>
|
|
65
|
-
return [
|
|
66
|
-
"success",
|
|
67
|
-
"error",
|
|
68
|
-
"rejected"
|
|
69
|
-
].includes(props.toolCall.status);
|
|
70
|
-
});
|
|
77
|
+
const isSettled = computed(() => isSettledStatus(props.toolCall.status));
|
|
71
78
|
/**
|
|
72
79
|
* 折叠后卸载内容 DOM:仅终态且非面板工具时启用——
|
|
73
80
|
* 进行中/待确认要保留内容状态(确认按钮、表单),面板工具的 body 是「查看面板」入口必须可达
|