@veltra/ai 1.4.0 → 2.1.0
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/index.d.ts +3 -3
- package/dist/chat/transports/index.d.ts +2 -2
- package/dist/chat/transports/openai.d.ts +16 -12
- package/dist/chat/transports/openai.js +56 -15
- package/dist/chat/transports/openai.js.map +1 -1
- package/dist/chat/types.d.ts +41 -2
- package/dist/chat/use-chat.d.ts +26 -1
- package/dist/chat/use-chat.js +132 -13
- package/dist/chat/use-chat.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js +222 -41
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/ask-question.js +2 -2
- package/dist/components/ai-chat/ask-question.js.map +1 -1
- package/dist/components/ai-chat/chat-input.js +81 -44
- package/dist/components/ai-chat/chat-input.js.map +1 -1
- package/dist/components/ai-chat/di.js.map +1 -1
- package/dist/components/ai-chat/message-item.js +101 -23
- package/dist/components/ai-chat/message-item.js.map +1 -1
- package/dist/components/ai-chat/message-list.js +217 -32
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/model-picker.js +140 -0
- package/dist/components/ai-chat/model-picker.js.map +1 -0
- package/dist/components/ai-chat/queue-list.js +70 -0
- package/dist/components/ai-chat/queue-list.js.map +1 -0
- package/dist/components/ai-chat/side-panel.js +96 -0
- package/dist/components/ai-chat/side-panel.js.map +1 -0
- package/dist/components/ai-chat/style.d.ts +6 -0
- package/dist/components/ai-chat/style.js +6 -0
- package/dist/components/ai-chat/style2.css +552 -41
- package/dist/components/ai-chat/tool-call.js +61 -27
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/components/ai-orb/ai-orb.js +94 -0
- package/dist/components/ai-orb/ai-orb.js.map +1 -0
- package/dist/components/ai-orb/orb-renderer.js +411 -0
- package/dist/components/ai-orb/orb-renderer.js.map +1 -0
- package/dist/components/ai-orb/style.d.ts +1 -0
- package/dist/components/ai-orb/style.js +1 -0
- package/dist/components/ai-orb/style2.css +23 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +2 -2
- package/dist/providers/types.d.ts +47 -0
- package/dist/style.js +1 -0
- package/dist/tools/ask-question/ask-question.d.ts +2 -17
- package/dist/tools/ask-question/index.d.ts +2 -2
- package/dist/tools/index.d.ts +3 -2
- package/dist/tools/index.js +10 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types/ai-chat.d.ts +35 -7
- package/dist/types/ai-orb.d.ts +27 -0
- package/dist/types/index.d.ts +2 -1
- package/package.json +8 -8
- package/src/chat/__test__/use-chat.test.ts +295 -9
- package/src/chat/transports/__test__/openai.test.ts +317 -0
- package/src/chat/transports/openai.ts +88 -26
- package/src/chat/types.ts +41 -1
- package/src/chat/use-chat.ts +177 -14
- package/src/components/ai-chat/__test__/ai-chat.test.ts +690 -12
- package/src/components/ai-chat/ai-chat.vue +241 -31
- package/src/components/ai-chat/chat-input.vue +78 -31
- package/src/components/ai-chat/di.ts +2 -0
- package/src/components/ai-chat/message-item.vue +98 -11
- package/src/components/ai-chat/message-list.vue +261 -21
- package/src/components/ai-chat/model-picker.vue +177 -0
- package/src/components/ai-chat/queue-list.vue +63 -0
- package/src/components/ai-chat/side-panel.vue +76 -0
- package/src/components/ai-chat/style.scss +621 -43
- package/src/components/ai-chat/style.ts +6 -0
- package/src/components/ai-chat/tool-call.vue +86 -42
- package/src/components/ai-orb/__test__/ai-orb.test.ts +251 -0
- package/src/components/ai-orb/ai-orb.vue +107 -0
- package/src/components/ai-orb/index.ts +1 -0
- package/src/components/ai-orb/orb-renderer.ts +505 -0
- package/src/components/ai-orb/style.scss +10 -0
- package/src/components/ai-orb/style.ts +1 -0
- package/src/components/index.ts +1 -0
- package/src/index.ts +8 -1
- package/src/providers/index.ts +1 -0
- package/src/providers/types.ts +46 -0
- package/src/style.ts +1 -0
- package/src/tools/ask-question/__test__/ask-question.test.ts +4 -4
- package/src/tools/index.ts +14 -1
- package/src/types/ai-chat.ts +35 -6
- package/src/types/ai-orb.ts +27 -0
- package/src/types/index.ts +1 -0
- package/dist/components/ai-chat/ai-chat.vue.d.ts +0 -36
- package/dist/components/ai-chat/index.d.ts +0 -2
- package/dist/components/index.d.ts +0 -3
|
@@ -1,29 +1,145 @@
|
|
|
1
1
|
import { AiChatDIKey } from "./di.js";
|
|
2
|
-
import _sfc_main$1 from "
|
|
3
|
-
import
|
|
2
|
+
import _sfc_main$1 from "../ai-orb/ai-orb.js";
|
|
3
|
+
import _sfc_main$2 from "./message-item.js";
|
|
4
|
+
import { Fragment, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, nextTick, normalizeClass, onBeforeUnmount, openBlock, ref, renderList, renderSlot, shallowRef, toDisplayString, unref, useTemplateRef, watch, withCtx } from "vue";
|
|
5
|
+
import { ArrowDown } from "@veltra/icons/normal";
|
|
6
|
+
import { UIcon, UScroll } from "@veltra/desktop";
|
|
4
7
|
import { bem } from "@veltra/utils";
|
|
5
|
-
import { UScroll } from "@veltra/desktop";
|
|
6
8
|
//#region src/components/ai-chat/message-list.vue
|
|
9
|
+
const WELCOME_ORB_SIZE = 64;
|
|
10
|
+
/** 工作指示活体球直径(px) */
|
|
11
|
+
const WORKING_ORB_SIZE = 48;
|
|
12
|
+
/** 生成结束 / 失败后工作球停留再隐藏 */
|
|
13
|
+
const WORKING_LINGER_MS = 2500;
|
|
7
14
|
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
8
15
|
name: "UAiChatMessageList",
|
|
9
16
|
__name: "message-list",
|
|
10
17
|
props: {
|
|
11
18
|
messages: {},
|
|
12
19
|
welcome: {},
|
|
20
|
+
running: { type: Boolean },
|
|
13
21
|
rendererProps: {}
|
|
14
22
|
},
|
|
15
|
-
emits: [
|
|
23
|
+
emits: [
|
|
24
|
+
"respond",
|
|
25
|
+
"regenerate",
|
|
26
|
+
"welcome-click"
|
|
27
|
+
],
|
|
16
28
|
setup(__props, { emit: __emit }) {
|
|
29
|
+
/** 欢迎区活体球直径(px) */
|
|
17
30
|
const props = __props;
|
|
18
31
|
const emit = __emit;
|
|
19
32
|
const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
|
|
20
33
|
/** tool 消息不单独渲染,其内容体现在工具卡片的结果区 */
|
|
21
34
|
const visibleMessages = computed(() => props.messages.filter((msg) => msg.role !== "tool"));
|
|
35
|
+
const welcomeItems = computed(() => {
|
|
36
|
+
const welcome = props.welcome;
|
|
37
|
+
if (!welcome) return [];
|
|
38
|
+
return (Array.isArray(welcome) ? welcome : [welcome]).filter(Boolean);
|
|
39
|
+
});
|
|
40
|
+
/** 欢迎语逐条轮换:当前展示项的下标 */
|
|
41
|
+
const welcomeIndex = ref(0);
|
|
42
|
+
/** 当前展示的欢迎语 */
|
|
43
|
+
const currentWelcomeItem = computed(() => {
|
|
44
|
+
const items = welcomeItems.value;
|
|
45
|
+
return items.length ? items[welcomeIndex.value % items.length] : void 0;
|
|
46
|
+
});
|
|
47
|
+
let welcomeTimer;
|
|
48
|
+
const stopWelcomeRotation = () => {
|
|
49
|
+
clearInterval(welcomeTimer);
|
|
50
|
+
welcomeTimer = void 0;
|
|
51
|
+
};
|
|
52
|
+
const startWelcomeRotation = () => {
|
|
53
|
+
stopWelcomeRotation();
|
|
54
|
+
if (welcomeItems.value.length > 1) welcomeTimer = setInterval(() => {
|
|
55
|
+
welcomeIndex.value += 1;
|
|
56
|
+
}, 4e3);
|
|
57
|
+
};
|
|
58
|
+
/** 点击欢迎球:不等自动轮换,立即换下一条,并重置计时 */
|
|
59
|
+
const cycleWelcome = () => {
|
|
60
|
+
if (welcomeItems.value.length <= 1) return;
|
|
61
|
+
welcomeIndex.value += 1;
|
|
62
|
+
startWelcomeRotation();
|
|
63
|
+
};
|
|
64
|
+
watch(() => [welcomeItems.value.length, visibleMessages.value.length], () => {
|
|
65
|
+
if (visibleMessages.value.length > 0) stopWelcomeRotation();
|
|
66
|
+
else startWelcomeRotation();
|
|
67
|
+
}, { immediate: true });
|
|
68
|
+
onBeforeUnmount(stopWelcomeRotation);
|
|
69
|
+
const workingOrbRef = useTemplateRef("workingOrb");
|
|
70
|
+
/** 工具调用失败的 id 去重集合:同一次失败只播一次沮丧表情 */
|
|
71
|
+
const reactedToolErrorIds = /* @__PURE__ */ new Set();
|
|
72
|
+
const toolErrorIds = computed(() => {
|
|
73
|
+
const ids = [];
|
|
74
|
+
for (const msg of props.messages) {
|
|
75
|
+
if (msg.role !== "assistant" || !msg.toolCalls) continue;
|
|
76
|
+
for (const call of msg.toolCalls) if (call.status === "error") ids.push(call.id);
|
|
77
|
+
}
|
|
78
|
+
return ids;
|
|
79
|
+
});
|
|
80
|
+
watch(toolErrorIds, (ids) => {
|
|
81
|
+
for (const id of ids) {
|
|
82
|
+
if (reactedToolErrorIds.has(id)) continue;
|
|
83
|
+
reactedToolErrorIds.add(id);
|
|
84
|
+
workingOrbRef.value?.react("frustrated");
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
/** 回答完毕 / 失败后工作球短暂停留,播完表情再隐藏 */
|
|
88
|
+
const finishing = ref(false);
|
|
89
|
+
let finishTimer;
|
|
90
|
+
const clearFinishing = () => {
|
|
91
|
+
clearTimeout(finishTimer);
|
|
92
|
+
finishing.value = false;
|
|
93
|
+
};
|
|
94
|
+
/** 工作球状态:收尾停留 → idle;正文输出中 → speaking;其余生成阶段 → thinking */
|
|
95
|
+
const workingOrbStatus = computed(() => {
|
|
96
|
+
if (finishing.value) return "idle";
|
|
97
|
+
const last = visibleMessages.value[visibleMessages.value.length - 1];
|
|
98
|
+
if (last?.role === "assistant" && last.status === "streaming" && last.content) return "speaking";
|
|
99
|
+
return "thinking";
|
|
100
|
+
});
|
|
101
|
+
watch(() => props.running, (running, prev) => {
|
|
102
|
+
if (running) {
|
|
103
|
+
clearFinishing();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!prev) return;
|
|
107
|
+
finishing.value = true;
|
|
108
|
+
const last = visibleMessages.value[visibleMessages.value.length - 1];
|
|
109
|
+
let reaction;
|
|
110
|
+
if (last?.role === "assistant") {
|
|
111
|
+
if (last.status === "done") reaction = "happy";
|
|
112
|
+
else if (last.status === "error") reaction = "frustrated";
|
|
113
|
+
}
|
|
114
|
+
if (reaction) nextTick(() => workingOrbRef.value?.react(reaction));
|
|
115
|
+
finishTimer = setTimeout(clearFinishing, WORKING_LINGER_MS);
|
|
116
|
+
});
|
|
117
|
+
onBeforeUnmount(clearFinishing);
|
|
22
118
|
const scrollRef = shallowRef();
|
|
23
119
|
/** 是否吸附底部(用户上翻浏览历史时取消吸附) */
|
|
24
120
|
const stickToBottom = ref(true);
|
|
121
|
+
/** 上一次滚动位置:用于区分用户上翻与自动吸底触发的 scroll 事件 */
|
|
122
|
+
let lastScrollY = 0;
|
|
123
|
+
/** 上翻阅读时展示「最新消息」悬浮按钮 */
|
|
124
|
+
const showToLatest = computed(() => !stickToBottom.value && visibleMessages.value.length > 0);
|
|
25
125
|
const handleScroll = (position) => {
|
|
26
|
-
|
|
126
|
+
if (position.sh - position.ch - position.y < 60) stickToBottom.value = true;
|
|
127
|
+
else if (position.y < lastScrollY) stickToBottom.value = false;
|
|
128
|
+
lastScrollY = position.y;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* 用户向上滚动轮 = 明确的离开底部意图,立即取消吸附。
|
|
132
|
+
* 不能等 scroll 事件再按方向判断:流式期间每个 chunk 都会排一次吸底滚动,
|
|
133
|
+
* 滞后的 scrollTo 会把用户拉回底部并触发 atBottom 重新吸附,导致「上滚动不了」
|
|
134
|
+
*/
|
|
135
|
+
const handleWheel = (e) => {
|
|
136
|
+
if (e.deltaY < 0) stickToBottom.value = false;
|
|
137
|
+
};
|
|
138
|
+
/** 点击悬浮按钮:恢复吸附并回到底部 */
|
|
139
|
+
const scrollToLatest = async () => {
|
|
140
|
+
stickToBottom.value = true;
|
|
141
|
+
await nextTick();
|
|
142
|
+
scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER });
|
|
27
143
|
};
|
|
28
144
|
watch(() => {
|
|
29
145
|
const list = visibleMessages.value;
|
|
@@ -32,40 +148,109 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
32
148
|
list.length,
|
|
33
149
|
last?.content.length ?? 0,
|
|
34
150
|
last?.reasoning?.length ?? 0,
|
|
35
|
-
last?.toolCalls?.length ?? 0
|
|
151
|
+
last?.toolCalls?.length ?? 0,
|
|
152
|
+
props.running
|
|
36
153
|
].join("|");
|
|
37
154
|
}, async () => {
|
|
155
|
+
const list = visibleMessages.value;
|
|
156
|
+
if (list[list.length - 1]?.role === "user") stickToBottom.value = true;
|
|
157
|
+
if (!stickToBottom.value) return;
|
|
158
|
+
await nextTick();
|
|
159
|
+
if (!stickToBottom.value) return;
|
|
160
|
+
scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER });
|
|
161
|
+
});
|
|
162
|
+
let trailingScrollTimer;
|
|
163
|
+
watch(() => props.running, async (running) => {
|
|
164
|
+
if (running) return;
|
|
165
|
+
clearTimeout(trailingScrollTimer);
|
|
38
166
|
if (!stickToBottom.value) return;
|
|
39
167
|
await nextTick();
|
|
168
|
+
if (!stickToBottom.value) return;
|
|
40
169
|
scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER });
|
|
170
|
+
trailingScrollTimer = setTimeout(() => {
|
|
171
|
+
if (stickToBottom.value) scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER });
|
|
172
|
+
}, 150);
|
|
41
173
|
});
|
|
174
|
+
onBeforeUnmount(() => clearTimeout(trailingScrollTimer));
|
|
42
175
|
return (_ctx, _cache) => {
|
|
43
|
-
return openBlock(),
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
176
|
+
return openBlock(), createElementBlock("div", { class: normalizeClass(unref(cls).e("list-wrap")) }, [
|
|
177
|
+
createVNode(unref(UScroll), {
|
|
178
|
+
class: normalizeClass(unref(cls).e("list")),
|
|
179
|
+
ref_key: "scrollRef",
|
|
180
|
+
ref: scrollRef,
|
|
181
|
+
always: "",
|
|
182
|
+
onScroll: handleScroll,
|
|
183
|
+
onWheelPassive: handleWheel
|
|
184
|
+
}, {
|
|
185
|
+
default: withCtx(() => [
|
|
186
|
+
!visibleMessages.value.length ? (openBlock(), createElementBlock("div", {
|
|
187
|
+
key: 0,
|
|
188
|
+
class: normalizeClass(unref(cls).e("welcome"))
|
|
189
|
+
}, [renderSlot(_ctx.$slots, "welcome", {}, () => [createElementVNode("div", { class: normalizeClass(unref(cls).e("welcome-inner")) }, [
|
|
190
|
+
createVNode(_sfc_main$1, {
|
|
191
|
+
size: WELCOME_ORB_SIZE,
|
|
192
|
+
onClick: cycleWelcome
|
|
193
|
+
}),
|
|
194
|
+
createCommentVNode(" 快捷提问逐条展示在球右侧并自动轮换:点文案发送,点球立即换下一条。\n 固定锚点布局:球位置钉死不动,气泡只向右延展,避免轮换时水平瞬移 "),
|
|
195
|
+
welcomeItems.value.length ? (openBlock(), createElementBlock("div", {
|
|
196
|
+
key: 0,
|
|
197
|
+
class: normalizeClass(unref(cls).e("welcome-item-wrap"))
|
|
198
|
+
}, [createVNode(Transition, {
|
|
199
|
+
name: unref(cls).em("welcome", "swap"),
|
|
200
|
+
mode: "out-in"
|
|
201
|
+
}, {
|
|
202
|
+
default: withCtx(() => [currentWelcomeItem.value ? (openBlock(), createElementBlock("button", {
|
|
203
|
+
key: welcomeIndex.value,
|
|
204
|
+
type: "button",
|
|
205
|
+
class: normalizeClass(unref(cls).e("welcome-item")),
|
|
206
|
+
onClick: _cache[0] || (_cache[0] = ($event) => emit("welcome-click", currentWelcomeItem.value))
|
|
207
|
+
}, toDisplayString(currentWelcomeItem.value), 3)) : createCommentVNode("v-if", true)]),
|
|
208
|
+
_: 1
|
|
209
|
+
}, 8, ["name"])], 2)) : createCommentVNode("v-if", true)
|
|
210
|
+
], 2)])], 2)) : createCommentVNode("v-if", true),
|
|
211
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(visibleMessages.value, (msg, index) => {
|
|
212
|
+
return openBlock(), createBlock(_sfc_main$2, {
|
|
213
|
+
key: msg.id,
|
|
214
|
+
message: msg,
|
|
215
|
+
"is-last": index === visibleMessages.value.length - 1,
|
|
216
|
+
"renderer-props": __props.rendererProps,
|
|
217
|
+
onRespond: _cache[1] || (_cache[1] = (id, approved) => emit("respond", id, approved)),
|
|
218
|
+
onRegenerate: _cache[2] || (_cache[2] = ($event) => emit("regenerate"))
|
|
219
|
+
}, null, 8, [
|
|
220
|
+
"message",
|
|
221
|
+
"is-last",
|
|
222
|
+
"renderer-props"
|
|
223
|
+
]);
|
|
224
|
+
}), 128)),
|
|
225
|
+
createCommentVNode(" 工作状态统一指示:生成中在列表末尾展示活体球 + 文案;结束 / 失败停留片刻再隐藏 "),
|
|
226
|
+
__props.running || finishing.value ? (openBlock(), createElementBlock("div", {
|
|
227
|
+
key: 1,
|
|
228
|
+
class: normalizeClass(unref(cls).e("working"))
|
|
229
|
+
}, [createVNode(_sfc_main$1, {
|
|
230
|
+
ref: "workingOrb",
|
|
231
|
+
size: WORKING_ORB_SIZE,
|
|
232
|
+
status: workingOrbStatus.value
|
|
233
|
+
}, null, 8, ["status"]), __props.running ? (openBlock(), createElementBlock("span", {
|
|
234
|
+
key: 0,
|
|
235
|
+
class: normalizeClass([unref(cls).e("working-text"), "u-shine"])
|
|
236
|
+
}, "工作中…", 2)) : createCommentVNode("v-if", true)], 2)) : createCommentVNode("v-if", true)
|
|
237
|
+
]),
|
|
238
|
+
_: 3
|
|
239
|
+
}, 8, ["class"]),
|
|
240
|
+
createCommentVNode(" 用户上翻浏览时悬浮入口:一键回到最新消息 "),
|
|
241
|
+
createVNode(Transition, { name: unref(cls).em("to-latest", "fade") }, {
|
|
242
|
+
default: withCtx(() => [showToLatest.value ? (openBlock(), createElementBlock("button", {
|
|
243
|
+
key: 0,
|
|
244
|
+
type: "button",
|
|
245
|
+
class: normalizeClass(unref(cls).e("to-latest")),
|
|
246
|
+
onClick: scrollToLatest
|
|
247
|
+
}, [createVNode(unref(UIcon), null, {
|
|
248
|
+
default: withCtx(() => [createVNode(unref(ArrowDown))]),
|
|
249
|
+
_: 1
|
|
250
|
+
}), _cache[3] || (_cache[3] = createElementVNode("span", null, "最新消息", -1))], 2)) : createCommentVNode("v-if", true)]),
|
|
251
|
+
_: 1
|
|
252
|
+
}, 8, ["name"])
|
|
253
|
+
], 2);
|
|
69
254
|
};
|
|
70
255
|
}
|
|
71
256
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-list.js","names":[],"sources":["../../../src/components/ai-chat/message-list.vue"],"sourcesContent":["<template>\n <UScroll :class=\"cls.e('list')\" ref=\"scrollRef\" always @scroll=\"handleScroll\">\n <div v-if=\"!visibleMessages.length\" :class=\"cls.e('welcome')\">\n <slot name=\"welcome\">{{ welcome }}</slot>\n </div>\n\n <MessageItem\n v-for=\"(msg, index) in visibleMessages\"\n :key=\"msg.id\"\n :message=\"msg\"\n :is-last=\"index === visibleMessages.length - 1\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n </UScroll>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UScroll } from '@veltra/desktop'\nimport type { ScrollPosition } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, ref, shallowRef, watch } from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport { AiChatDIKey } from './di'\nimport MessageItem from './message-item.vue'\n\ndefineOptions({ name: 'UAiChatMessageList' })\n\nconst props = defineProps<{\n messages: ChatMessage[]\n /** 空状态欢迎语 */\n welcome?: string\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\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\nconst scrollRef = shallowRef<{ scrollTo: (position: ScrollPosition) => void }>()\n/** 是否吸附底部(用户上翻浏览历史时取消吸附) */\nconst stickToBottom = ref(true)\n\nconst handleScroll = (position: Required<ScrollPosition>) => {\n stickToBottom.value = position.sh - position.ch - position.y < 60\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 ].join('|')\n },\n async () => {\n if (!stickToBottom.value) return\n await nextTick()\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }\n)\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;EA8BA,MAAM,QAAQ;EAQd,MAAM,OAAO;EAWb,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;;EAGpC,MAAM,kBAAkB,eAAe,MAAM,SAAS,QAAQ,QAAQ,IAAI,SAAS,MAAM,CAAC;EAE1F,MAAM,YAAY,WAA6D;;EAE/E,MAAM,gBAAgB,IAAI,IAAI;EAE9B,MAAM,gBAAgB,aAAuC;GAC3D,cAAc,QAAQ,SAAS,KAAK,SAAS,KAAK,SAAS,IAAI;EACjE;EAGA,YACQ;GACJ,MAAM,OAAO,gBAAgB;GAC7B,MAAM,OAAO,KAAK,KAAK,SAAS;GAChC,OAAO;IACL,KAAK;IACL,MAAM,QAAQ,UAAU;IACxB,MAAM,WAAW,UAAU;IAC3B,MAAM,WAAW,UAAU;GAC7B,CAAC,CAAC,KAAK,GAAG;EACZ,GACA,YAAY;GACV,IAAI,CAAC,cAAc,OAAO;GAC1B,MAAM,SAAS;GACf,UAAU,OAAO,SAAS,EAAE,GAAG,OAAO,iBAAiB,CAAC;EAC1D,CACF;;uBA9EE,YAcU,MAAA,OAAA,GAAA;IAdA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,MAAA,CAAA;aAAc;IAAJ,KAAI;IAAY,QAAA;IAAQ,UAAQ;;2BAGxD,CAAA,CAFM,gBAAA,MAAgB,UAAA,UAAA,GAA5B,mBAEM,OAAA;;KAF+B,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,SAAA,CAAA;QAC/C,WAAyC,KAAA,QAAA,WAAA,CAAA,SAAA,CAAA,gBAAA,gBAAjB,QAAA,OAAO,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,IAAA,UAAA,IAAA,GAGjC,mBAQE,UAAA,MAAA,WAPuB,gBAAA,QAAf,KAAK,UAAK;yBADpB,YAQE,aAAA;MANC,KAAK,IAAI;MACT,SAAS;MACT,WAAS,UAAU,gBAAA,MAAgB,SAAM;MACzC,kBAAgB,QAAA;MAChB,WAAO,OAAA,OAAA,OAAA,MAAG,IAAI,aAAa,KAAI,WAAY,IAAI,QAAQ;MACvD,cAAU,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,YAAA"}
|
|
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 <div v-if=\"!visibleMessages.length\" :class=\"cls.e('welcome')\">\n <slot name=\"welcome\">\n <div :class=\"cls.e('welcome-inner')\">\n <UAiOrb :size=\"WELCOME_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 @click=\"emit('welcome-click', currentWelcomeItem)\"\n >\n {{ currentWelcomeItem }}\n </button>\n </Transition>\n </div>\n </div>\n </slot>\n </div>\n\n <MessageItem\n v-for=\"(msg, index) in visibleMessages\"\n :key=\"msg.id\"\n :message=\"msg\"\n :is-last=\"index === visibleMessages.length - 1\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n\n <!-- 工作状态统一指示:生成中在列表末尾展示活体球 + 文案;结束 / 失败停留片刻再隐藏 -->\n <div v-if=\"running || finishing\" :class=\"cls.e('working')\">\n <UAiOrb ref=\"workingOrb\" :size=\"WORKING_ORB_SIZE\" :status=\"workingOrbStatus\" />\n <span v-if=\"running\" :class=\"[cls.e('working-text'), 'u-shine']\">工作中…</span>\n </div>\n </UScroll>\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'\n\ndefineOptions({ name: 'UAiChatMessageList' })\n\n/** 欢迎区活体球直径(px) */\nconst WELCOME_ORB_SIZE = 64\n/** 工作指示活体球直径(px) */\nconst WORKING_ORB_SIZE = 48\n/** 生成结束 / 失败后工作球停留再隐藏 */\nconst WORKING_LINGER_MS = 2500\n\nconst props = defineProps<{\n messages: ChatMessage[]\n /** 空状态欢迎项;字符串按单项处理 */\n welcome?: string | string[]\n /** 是否生成中(控制底部工作状态指示) */\n running?: 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\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// 欢迎区展示期间自动轮换;出现消息后停止\nwatch(\n () => [welcomeItems.value.length, visibleMessages.value.length] as const,\n () => {\n if (visibleMessages.value.length > 0) stopWelcomeRotation()\n else startWelcomeRotation()\n },\n { immediate: true }\n)\n\nonBeforeUnmount(stopWelcomeRotation)\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/** 回答完毕 / 失败后工作球短暂停留,播完表情再隐藏 */\nconst finishing = ref(false)\nlet finishTimer: ReturnType<typeof setTimeout> | undefined\n\nconst clearFinishing = () => {\n clearTimeout(finishTimer)\n finishing.value = false\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 clearFinishing()\n return\n }\n if (!prev) return\n\n finishing.value = true\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 finishTimer = setTimeout(clearFinishing, WORKING_LINGER_MS)\n }\n)\n\nonBeforeUnmount(clearFinishing)\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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"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { AiChatDIKey } from "./di.js";
|
|
2
|
+
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeModels, nextTick, normalizeClass, openBlock, renderList, shallowRef, toDisplayString, unref, useModel, useTemplateRef, withCtx, withModifiers } from "vue";
|
|
3
|
+
import { ArrowDown, Check } from "@veltra/icons/normal";
|
|
4
|
+
import { UDropdown, UIcon } from "@veltra/desktop";
|
|
5
|
+
import { bem } from "@veltra/utils";
|
|
6
|
+
//#region src/components/ai-chat/model-picker.vue
|
|
7
|
+
const _hoisted_1 = ["onClick"];
|
|
8
|
+
const _hoisted_2 = ["onClick"];
|
|
9
|
+
const _hoisted_3 = ["onClick"];
|
|
10
|
+
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
11
|
+
name: "UAiChatModelPicker",
|
|
12
|
+
__name: "model-picker",
|
|
13
|
+
props: /*@__PURE__*/ mergeModels({ models: {} }, {
|
|
14
|
+
"model": {},
|
|
15
|
+
"modelModifiers": {},
|
|
16
|
+
"reasoningLevel": {},
|
|
17
|
+
"reasoningLevelModifiers": {}
|
|
18
|
+
}),
|
|
19
|
+
emits: ["update:model", "update:reasoningLevel"],
|
|
20
|
+
setup(__props) {
|
|
21
|
+
const props = __props;
|
|
22
|
+
const model = useModel(__props, "model");
|
|
23
|
+
const reasoningLevel = useModel(__props, "reasoningLevel");
|
|
24
|
+
const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
|
|
25
|
+
const dropdownRef = useTemplateRef("dropdownRef");
|
|
26
|
+
/** 当前内联展开思考强度的模型 id(手风琴,同时只展开一个) */
|
|
27
|
+
const expandedReasoningId = shallowRef();
|
|
28
|
+
const currentModel = computed(() => {
|
|
29
|
+
return props.models.find((m) => m.id === model.value);
|
|
30
|
+
});
|
|
31
|
+
const currentReasoningLabel = computed(() => {
|
|
32
|
+
return currentModel.value?.reasoningLevels?.find((level) => level.value === reasoningLevel.value)?.label;
|
|
33
|
+
});
|
|
34
|
+
/** 取模型默认推理等级:显式配置优先,缺省落到首个可选等级 */
|
|
35
|
+
const resolveDefaultReasoningLevel = (item) => {
|
|
36
|
+
const levels = item.reasoningLevels ?? [];
|
|
37
|
+
if (!levels.length) return void 0;
|
|
38
|
+
return levels.find((level) => level.value === item.defaultReasoningLevel)?.value ?? levels[0]?.value;
|
|
39
|
+
};
|
|
40
|
+
/** 胶囊文案:已选中模型展示当前推理等级,其余展示该模型的默认等级 */
|
|
41
|
+
const getReasoningLabel = (item) => {
|
|
42
|
+
if (item.id === model.value && currentReasoningLabel.value) return currentReasoningLabel.value;
|
|
43
|
+
return item.reasoningLevels?.find((level) => level.value === resolveDefaultReasoningLevel(item))?.label;
|
|
44
|
+
};
|
|
45
|
+
const isReasoningActive = (item, value) => {
|
|
46
|
+
return item.id === model.value && reasoningLevel.value === value;
|
|
47
|
+
};
|
|
48
|
+
/** 关闭时收起内联思考强度,下次打开仍从模型列表开始 */
|
|
49
|
+
const handleVisibleChange = (visible) => {
|
|
50
|
+
if (!visible) expandedReasoningId.value = void 0;
|
|
51
|
+
};
|
|
52
|
+
/** 点击模型主体:直接选中模型,并同步到该模型默认推理等级 */
|
|
53
|
+
const handleSelectModel = (item) => {
|
|
54
|
+
model.value = item.id;
|
|
55
|
+
reasoningLevel.value = resolveDefaultReasoningLevel(item);
|
|
56
|
+
dropdownRef.value?.close();
|
|
57
|
+
};
|
|
58
|
+
/** 点击推理等级胶囊:内联展开/收起该模型的思考强度 */
|
|
59
|
+
const toggleReasoning = (item) => {
|
|
60
|
+
expandedReasoningId.value = expandedReasoningId.value === item.id ? void 0 : item.id;
|
|
61
|
+
nextTick(() => dropdownRef.value?.updateDropdown());
|
|
62
|
+
};
|
|
63
|
+
/** 选中思考强度:同时切到该等级所属模型,避免模型与推理等级错配 */
|
|
64
|
+
const handleSelectReasoning = (item, value) => {
|
|
65
|
+
model.value = item.id;
|
|
66
|
+
reasoningLevel.value = value;
|
|
67
|
+
dropdownRef.value?.close();
|
|
68
|
+
};
|
|
69
|
+
return (_ctx, _cache) => {
|
|
70
|
+
return openBlock(), createBlock(unref(UDropdown), {
|
|
71
|
+
ref_key: "dropdownRef",
|
|
72
|
+
ref: dropdownRef,
|
|
73
|
+
trigger: "click",
|
|
74
|
+
width: "auto",
|
|
75
|
+
"min-width": "220px",
|
|
76
|
+
"content-class": unref(cls).e("model-panel"),
|
|
77
|
+
"onUpdate:visible": handleVisibleChange
|
|
78
|
+
}, {
|
|
79
|
+
trigger: withCtx(() => [createElementVNode("button", {
|
|
80
|
+
type: "button",
|
|
81
|
+
class: normalizeClass(unref(cls).e("model-trigger"))
|
|
82
|
+
}, [
|
|
83
|
+
createElementVNode("span", { class: normalizeClass(unref(cls).e("model-trigger-label")) }, toDisplayString(currentModel.value?.label ?? model.value ?? "模型"), 3),
|
|
84
|
+
currentReasoningLabel.value ? (openBlock(), createElementBlock("span", {
|
|
85
|
+
key: 0,
|
|
86
|
+
class: normalizeClass(unref(cls).e("model-trigger-reasoning"))
|
|
87
|
+
}, toDisplayString(currentReasoningLabel.value), 3)) : createCommentVNode("v-if", true),
|
|
88
|
+
createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("model-trigger-arrow")) }, {
|
|
89
|
+
default: withCtx(() => [createVNode(unref(ArrowDown))]),
|
|
90
|
+
_: 1
|
|
91
|
+
}, 8, ["class"])
|
|
92
|
+
], 2)]),
|
|
93
|
+
content: withCtx(() => [createElementVNode("div", { class: normalizeClass(unref(cls).e("model-panel-list")) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.models, (item) => {
|
|
94
|
+
return openBlock(), createElementBlock("div", {
|
|
95
|
+
key: item.id,
|
|
96
|
+
class: normalizeClass([unref(cls).e("model-option"), unref(bem).is("active", item.id === model.value)])
|
|
97
|
+
}, [createElementVNode("div", {
|
|
98
|
+
class: normalizeClass(unref(cls).e("model-option-main")),
|
|
99
|
+
onClick: ($event) => handleSelectModel(item)
|
|
100
|
+
}, [createElementVNode("div", { class: normalizeClass(unref(cls).e("model-option-head")) }, [createElementVNode("span", { class: normalizeClass(unref(cls).e("model-option-name")) }, toDisplayString(item.label ?? item.id), 3), createElementVNode("span", { class: normalizeClass(unref(cls).e("model-option-actions")) }, [
|
|
101
|
+
item.reasoningLevels?.length ? (openBlock(), createElementBlock("button", {
|
|
102
|
+
key: 0,
|
|
103
|
+
type: "button",
|
|
104
|
+
class: normalizeClass([unref(cls).e("model-option-reasoning"), unref(bem).is("expanded", expandedReasoningId.value === item.id)]),
|
|
105
|
+
onClick: withModifiers(($event) => toggleReasoning(item), ["stop"])
|
|
106
|
+
}, [createElementVNode("span", null, toDisplayString(getReasoningLabel(item)), 1), createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("model-option-reasoning-arrow")) }, {
|
|
107
|
+
default: withCtx(() => [createVNode(unref(ArrowDown))]),
|
|
108
|
+
_: 1
|
|
109
|
+
}, 8, ["class"])], 10, _hoisted_2)) : createCommentVNode("v-if", true),
|
|
110
|
+
createCommentVNode(" 勾选槽位固定占位:未选中也保留位置,保证推理等级按钮跨行对齐 "),
|
|
111
|
+
createVNode(unref(UIcon), { class: normalizeClass([unref(cls).e("model-option-check"), unref(bem).is("hidden", item.id !== model.value)]) }, {
|
|
112
|
+
default: withCtx(() => [createVNode(unref(Check))]),
|
|
113
|
+
_: 1
|
|
114
|
+
}, 8, ["class"])
|
|
115
|
+
], 2)], 2), item.description ? (openBlock(), createElementBlock("div", {
|
|
116
|
+
key: 0,
|
|
117
|
+
class: normalizeClass(unref(cls).e("model-option-desc"))
|
|
118
|
+
}, toDisplayString(item.description), 3)) : createCommentVNode("v-if", true)], 10, _hoisted_1), item.reasoningLevels?.length ? (openBlock(), createElementBlock("div", {
|
|
119
|
+
key: 0,
|
|
120
|
+
class: normalizeClass([unref(cls).e("reasoning-levels"), unref(bem).is("expanded", expandedReasoningId.value === item.id)])
|
|
121
|
+
}, [createElementVNode("div", { class: normalizeClass(unref(cls).e("reasoning-levels-inner")) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(item.reasoningLevels, (level) => {
|
|
122
|
+
return openBlock(), createElementBlock("div", {
|
|
123
|
+
key: level.value,
|
|
124
|
+
class: normalizeClass([unref(cls).e("reasoning-option"), unref(bem).is("active", isReasoningActive(item, level.value))]),
|
|
125
|
+
onClick: ($event) => handleSelectReasoning(item, level.value)
|
|
126
|
+
}, [createElementVNode("span", null, toDisplayString(level.label), 1), createVNode(unref(UIcon), { class: normalizeClass([unref(cls).e("model-option-check"), unref(bem).is("hidden", !isReasoningActive(item, level.value))]) }, {
|
|
127
|
+
default: withCtx(() => [createVNode(unref(Check))]),
|
|
128
|
+
_: 1
|
|
129
|
+
}, 8, ["class"])], 10, _hoisted_3);
|
|
130
|
+
}), 128))], 2)], 2)) : createCommentVNode("v-if", true)], 2);
|
|
131
|
+
}), 128))], 2)]),
|
|
132
|
+
_: 1
|
|
133
|
+
}, 8, ["content-class"]);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
//#endregion
|
|
138
|
+
export { _sfc_main as default };
|
|
139
|
+
|
|
140
|
+
//# sourceMappingURL=model-picker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-picker.js","names":[],"sources":["../../../src/components/ai-chat/model-picker.vue"],"sourcesContent":["<template>\n <UDropdown\n ref=\"dropdownRef\"\n trigger=\"click\"\n width=\"auto\"\n min-width=\"220px\"\n :content-class=\"cls.e('model-panel')\"\n @update:visible=\"handleVisibleChange\"\n >\n <template #trigger>\n <button type=\"button\" :class=\"cls.e('model-trigger')\">\n <span :class=\"cls.e('model-trigger-label')\">\n {{ currentModel?.label ?? model ?? '模型' }}\n </span>\n <span v-if=\"currentReasoningLabel\" :class=\"cls.e('model-trigger-reasoning')\">\n {{ currentReasoningLabel }}\n </span>\n <UIcon :class=\"cls.e('model-trigger-arrow')\">\n <ArrowDown />\n </UIcon>\n </button>\n </template>\n\n <template #content>\n <div :class=\"cls.e('model-panel-list')\">\n <div\n v-for=\"item in models\"\n :key=\"item.id\"\n :class=\"[cls.e('model-option'), bem.is('active', item.id === model)]\"\n >\n <div :class=\"cls.e('model-option-main')\" @click=\"handleSelectModel(item)\">\n <div :class=\"cls.e('model-option-head')\">\n <span :class=\"cls.e('model-option-name')\">{{ item.label ?? item.id }}</span>\n <span :class=\"cls.e('model-option-actions')\">\n <button\n v-if=\"item.reasoningLevels?.length\"\n type=\"button\"\n :class=\"[\n cls.e('model-option-reasoning'),\n bem.is('expanded', expandedReasoningId === item.id)\n ]\"\n @click.stop=\"toggleReasoning(item)\"\n >\n <span>{{ getReasoningLabel(item) }}</span>\n <UIcon :class=\"cls.e('model-option-reasoning-arrow')\">\n <ArrowDown />\n </UIcon>\n </button>\n <!-- 勾选槽位固定占位:未选中也保留位置,保证推理等级按钮跨行对齐 -->\n <UIcon :class=\"[cls.e('model-option-check'), bem.is('hidden', item.id !== model)]\">\n <Check />\n </UIcon>\n </span>\n </div>\n <div v-if=\"item.description\" :class=\"cls.e('model-option-desc')\">\n {{ item.description }}\n </div>\n </div>\n\n <div\n v-if=\"item.reasoningLevels?.length\"\n :class=\"[\n cls.e('reasoning-levels'),\n bem.is('expanded', expandedReasoningId === item.id)\n ]\"\n >\n <div :class=\"cls.e('reasoning-levels-inner')\">\n <div\n v-for=\"level in item.reasoningLevels\"\n :key=\"level.value\"\n :class=\"[\n cls.e('reasoning-option'),\n bem.is('active', isReasoningActive(item, level.value))\n ]\"\n @click=\"handleSelectReasoning(item, level.value)\"\n >\n <span>{{ level.label }}</span>\n <UIcon\n :class=\"[\n cls.e('model-option-check'),\n bem.is('hidden', !isReasoningActive(item, level.value))\n ]\"\n >\n <Check />\n </UIcon>\n </div>\n </div>\n </div>\n </div>\n </div>\n </template>\n </UDropdown>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UDropdown, UIcon } from '@veltra/desktop'\nimport { ArrowDown, Check } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, shallowRef, useTemplateRef } from 'vue'\n\nimport type { ChatModelOption } from '../../providers'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatModelPicker' })\n\nconst props = defineProps<{\n /** 可选模型列表 */\n models: ChatModelOption[]\n}>()\n\nconst model = defineModel<string | undefined>('model')\nconst reasoningLevel = defineModel<string | undefined>('reasoningLevel')\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst dropdownRef = useTemplateRef('dropdownRef')\n\n/** 当前内联展开思考强度的模型 id(手风琴,同时只展开一个) */\nconst expandedReasoningId = shallowRef<string>()\n\nconst currentModel = computed(() => {\n return props.models.find((m) => m.id === model.value)\n})\n\nconst currentReasoningLabel = computed(() => {\n return currentModel.value?.reasoningLevels?.find((level) => level.value === reasoningLevel.value)\n ?.label\n})\n\n/** 取模型默认推理等级:显式配置优先,缺省落到首个可选等级 */\nconst resolveDefaultReasoningLevel = (item: ChatModelOption) => {\n const levels = item.reasoningLevels ?? []\n if (!levels.length) return undefined\n return (\n levels.find((level) => level.value === item.defaultReasoningLevel)?.value ?? levels[0]?.value\n )\n}\n\n/** 胶囊文案:已选中模型展示当前推理等级,其余展示该模型的默认等级 */\nconst getReasoningLabel = (item: ChatModelOption) => {\n if (item.id === model.value && currentReasoningLabel.value) {\n return currentReasoningLabel.value\n }\n return item.reasoningLevels?.find((level) => level.value === resolveDefaultReasoningLevel(item))\n ?.label\n}\n\nconst isReasoningActive = (item: ChatModelOption, value: string) => {\n return item.id === model.value && reasoningLevel.value === value\n}\n\n/** 关闭时收起内联思考强度,下次打开仍从模型列表开始 */\nconst handleVisibleChange = (visible: boolean) => {\n if (!visible) expandedReasoningId.value = undefined\n}\n\n/** 点击模型主体:直接选中模型,并同步到该模型默认推理等级 */\nconst handleSelectModel = (item: ChatModelOption) => {\n model.value = item.id\n reasoningLevel.value = resolveDefaultReasoningLevel(item)\n dropdownRef.value?.close()\n}\n\n/** 点击推理等级胶囊:内联展开/收起该模型的思考强度 */\nconst toggleReasoning = (item: ChatModelOption) => {\n expandedReasoningId.value = expandedReasoningId.value === item.id ? undefined : item.id\n nextTick(() => dropdownRef.value?.updateDropdown())\n}\n\n/** 选中思考强度:同时切到该等级所属模型,避免模型与推理等级错配 */\nconst handleSelectReasoning = (item: ChatModelOption, value: string) => {\n model.value = item.id\n reasoningLevel.value = value\n dropdownRef.value?.close()\n}\n</script>\n"],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AiChatDIKey } from "./di.js";
|
|
2
|
+
import { Fragment, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, renderList, toDisplayString, unref, withCtx } from "vue";
|
|
3
|
+
import { CaretRight, Close, EditPen } from "@veltra/icons/normal";
|
|
4
|
+
import { UIcon } from "@veltra/desktop";
|
|
5
|
+
import { bem } from "@veltra/utils";
|
|
6
|
+
//#region src/components/ai-chat/queue-list.vue
|
|
7
|
+
const _hoisted_1 = ["title"];
|
|
8
|
+
const _hoisted_2 = ["onClick"];
|
|
9
|
+
const _hoisted_3 = ["onClick"];
|
|
10
|
+
const _hoisted_4 = ["onClick"];
|
|
11
|
+
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
12
|
+
name: "UAiChatQueueList",
|
|
13
|
+
__name: "queue-list",
|
|
14
|
+
props: { queue: {} },
|
|
15
|
+
emits: [
|
|
16
|
+
"start-now",
|
|
17
|
+
"edit",
|
|
18
|
+
"remove"
|
|
19
|
+
],
|
|
20
|
+
setup(__props, { emit: __emit }) {
|
|
21
|
+
const emit = __emit;
|
|
22
|
+
const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
|
|
23
|
+
return (_ctx, _cache) => {
|
|
24
|
+
return __props.queue.length ? (openBlock(), createElementBlock("div", {
|
|
25
|
+
key: 0,
|
|
26
|
+
class: normalizeClass(unref(cls).e("queue"))
|
|
27
|
+
}, [createElementVNode("div", { class: normalizeClass(unref(cls).e("queue-head")) }, "待发送队列 · " + toDisplayString(__props.queue.length), 3), (openBlock(true), createElementBlock(Fragment, null, renderList(__props.queue, (item) => {
|
|
28
|
+
return openBlock(), createElementBlock("div", {
|
|
29
|
+
key: item.id,
|
|
30
|
+
class: normalizeClass(unref(cls).e("queue-item"))
|
|
31
|
+
}, [createElementVNode("span", {
|
|
32
|
+
class: normalizeClass(unref(cls).e("queue-text")),
|
|
33
|
+
title: item.content
|
|
34
|
+
}, toDisplayString(item.content), 11, _hoisted_1), createElementVNode("span", { class: normalizeClass(unref(cls).e("queue-actions")) }, [
|
|
35
|
+
createElementVNode("button", {
|
|
36
|
+
type: "button",
|
|
37
|
+
class: normalizeClass([unref(cls).e("queue-action"), unref(cls).em("queue-action", "primary")]),
|
|
38
|
+
title: "中断当前会话,立即执行该条",
|
|
39
|
+
onClick: ($event) => emit("start-now", item.id)
|
|
40
|
+
}, [createVNode(unref(UIcon), null, {
|
|
41
|
+
default: withCtx(() => [createVNode(unref(CaretRight))]),
|
|
42
|
+
_: 1
|
|
43
|
+
}), _cache[0] || (_cache[0] = createTextVNode("立即开始 ", -1))], 10, _hoisted_2),
|
|
44
|
+
createElementVNode("button", {
|
|
45
|
+
type: "button",
|
|
46
|
+
class: normalizeClass(unref(cls).e("queue-action")),
|
|
47
|
+
title: "取回输入框编辑",
|
|
48
|
+
onClick: ($event) => emit("edit", item.id)
|
|
49
|
+
}, [createVNode(unref(UIcon), null, {
|
|
50
|
+
default: withCtx(() => [createVNode(unref(EditPen))]),
|
|
51
|
+
_: 1
|
|
52
|
+
})], 10, _hoisted_3),
|
|
53
|
+
createElementVNode("button", {
|
|
54
|
+
type: "button",
|
|
55
|
+
class: normalizeClass(unref(cls).e("queue-action")),
|
|
56
|
+
title: "移出队列",
|
|
57
|
+
onClick: ($event) => emit("remove", item.id)
|
|
58
|
+
}, [createVNode(unref(UIcon), null, {
|
|
59
|
+
default: withCtx(() => [createVNode(unref(Close))]),
|
|
60
|
+
_: 1
|
|
61
|
+
})], 10, _hoisted_4)
|
|
62
|
+
], 2)], 2);
|
|
63
|
+
}), 128))], 2)) : createCommentVNode("v-if", true);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
//#endregion
|
|
68
|
+
export { _sfc_main as default };
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=queue-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue-list.js","names":[],"sources":["../../../src/components/ai-chat/queue-list.vue"],"sourcesContent":["<template>\n <div v-if=\"queue.length\" :class=\"cls.e('queue')\">\n <div :class=\"cls.e('queue-head')\">待发送队列 · {{ queue.length }}</div>\n <div v-for=\"item in queue\" :key=\"item.id\" :class=\"cls.e('queue-item')\">\n <span :class=\"cls.e('queue-text')\" :title=\"item.content\">{{ item.content }}</span>\n <span :class=\"cls.e('queue-actions')\">\n <button\n type=\"button\"\n :class=\"[cls.e('queue-action'), cls.em('queue-action', 'primary')]\"\n title=\"中断当前会话,立即执行该条\"\n @click=\"emit('start-now', item.id)\"\n >\n <UIcon><CaretRight /></UIcon>立即开始\n </button>\n <button\n type=\"button\"\n :class=\"cls.e('queue-action')\"\n title=\"取回输入框编辑\"\n @click=\"emit('edit', item.id)\"\n >\n <UIcon><EditPen /></UIcon>\n </button>\n <button\n type=\"button\"\n :class=\"cls.e('queue-action')\"\n title=\"移出队列\"\n @click=\"emit('remove', item.id)\"\n >\n <UIcon><Close /></UIcon>\n </button>\n </span>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon } from '@veltra/desktop'\nimport { CaretRight, Close, EditPen } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { inject } from 'vue'\n\nimport type { ChatQueuedMessage } from '../../chat/types'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatQueueList' })\n\ndefineProps<{\n /** 待发送队列(按执行顺序排列) */\n queue: ChatQueuedMessage[]\n}>()\n\nconst emit = defineEmits<{\n /** 立即开始:中断当前会话并插队执行 */\n (e: 'start-now', id: string): void\n /** 编辑:取回输入框重新编辑 */\n (e: 'edit', id: string): void\n /** 移出队列 */\n (e: 'remove', id: string): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;EAmDA,MAAM,OAAO;EAUb,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;;GA5DvB,OAAA,QAAA,MAAM,UAAA,UAAA,GAAjB,mBA+BM,OAAA;;IA/BoB,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,OAAA,CAAA;GACpC,GAAA,CAAA,mBAAkE,OAAA,EAA5D,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA,EAAA,GAAgB,aAAQ,gBAAG,QAAA,MAAM,MAAM,GAAA,CAAA,IAAA,UAAA,IAAA,GACzD,mBA4BM,UAAA,MAAA,WA5Bc,QAAA,QAAR,SAAI;IAAhB,OAAA,UAAA,GAAA,mBA4BM,OAAA;KA5BsB,KAAK,KAAK;KAAK,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA;IACrD,GAAA,CAAA,mBAAkF,QAAA;KAA3E,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA;KAAiB,OAAO,KAAK;IAAY,GAAA,gBAAA,KAAK,OAAO,GAAA,IAAA,UAAA,GACxE,mBAyBO,QAAA,EAzBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,eAAA,CAAA,EAAA,GAAA;KACjB,mBAOS,UAAA;MANP,MAAK;MACJ,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,GAAkB,MAAA,GAAA,CAAG,CAAC,GAAE,gBAAA,SAAA,CAAA,CAAA;MACtC,OAAM;MACL,UAAK,WAAE,KAAI,aAAc,KAAK,EAAE;KAEjC,GAAA,CAAA,YAA6B,MAAA,KAAA,GAAA,MAAA;MAAR,SAAA,cAAA,CAAd,YAAc,MAAA,UAAA,CAAA,CAAA,CAAA;;KAAQ,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,gBAAA,SAC/B,EAAA,EAAA,GAAA,IAAA,UAAA;KACA,mBAOS,UAAA;MANP,MAAK;MACJ,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;MACb,OAAM;MACL,UAAK,WAAE,KAAI,QAAS,KAAK,EAAE;KAE5B,GAAA,CAAA,YAA0B,MAAA,KAAA,GAAA,MAAA;MAAR,SAAA,cAAA,CAAX,YAAW,MAAA,OAAA,CAAA,CAAA,CAAA;;;KAEpB,mBAOS,UAAA;MANP,MAAK;MACJ,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;MACb,OAAM;MACL,UAAK,WAAE,KAAI,UAAW,KAAK,EAAE;KAE9B,GAAA,CAAA,YAAwB,MAAA,KAAA,GAAA,MAAA;MAAR,SAAA,cAAA,CAAT,YAAS,MAAA,KAAA,CAAA,CAAA,CAAA"}
|