@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,33 +1,46 @@
|
|
|
1
1
|
import { AiChatDIKey } from "./di.js";
|
|
2
|
-
import
|
|
2
|
+
import _sfc_main$1 from "./model-picker.js";
|
|
3
|
+
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeModels, nextTick, normalizeClass, openBlock, ref, renderList, shallowRef, unref, useModel, vModelText, withCtx, withDirectives, withKeys, withModifiers } from "vue";
|
|
4
|
+
import { Attach, Close, Up } from "@veltra/icons/normal";
|
|
5
|
+
import { UButton, UFilePicker, UIcon } from "@veltra/desktop";
|
|
3
6
|
import { bem } from "@veltra/utils";
|
|
4
|
-
import { UButton, UIcon } from "@veltra/desktop";
|
|
5
|
-
import { Attach, Close, Send } from "@veltra/icons/normal";
|
|
6
7
|
//#region src/components/ai-chat/chat-input.vue
|
|
7
8
|
const _hoisted_1 = ["src", "alt"];
|
|
8
9
|
const _hoisted_2 = ["placeholder", "onKeydown"];
|
|
9
|
-
const _hoisted_3 = ["accept"];
|
|
10
10
|
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
11
11
|
name: "UAiChatInput",
|
|
12
12
|
__name: "chat-input",
|
|
13
|
-
props: {
|
|
13
|
+
props: /*@__PURE__*/ mergeModels({
|
|
14
14
|
running: { type: Boolean },
|
|
15
|
+
models: {},
|
|
15
16
|
placeholder: {},
|
|
16
17
|
accept: {},
|
|
17
18
|
maxAttachmentSize: {}
|
|
18
|
-
},
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
}, {
|
|
20
|
+
"model": {},
|
|
21
|
+
"modelModifiers": {},
|
|
22
|
+
"reasoningLevel": {},
|
|
23
|
+
"reasoningLevelModifiers": {}
|
|
24
|
+
}),
|
|
25
|
+
emits: /*@__PURE__*/ mergeModels(["send", "abort"], ["update:model", "update:reasoningLevel"]),
|
|
26
|
+
setup(__props, { expose: __expose, emit: __emit }) {
|
|
21
27
|
const props = __props;
|
|
28
|
+
const model = useModel(__props, "model");
|
|
29
|
+
const reasoningLevel = useModel(__props, "reasoningLevel");
|
|
22
30
|
const emit = __emit;
|
|
23
31
|
const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
|
|
24
32
|
const text = ref("");
|
|
25
33
|
const attachments = ref([]);
|
|
26
34
|
const textareaRef = shallowRef();
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
const hasContent = computed(() => !!text.value.trim() || attachments.value.length > 0);
|
|
36
|
+
/** 生成中且输入为空时显示停止;有内容则显示发送(入队) */
|
|
37
|
+
const showStop = computed(() => props.running && !hasContent.value);
|
|
38
|
+
/** 生成中提示用户消息将进入队列 */
|
|
39
|
+
const placeholderText = computed(() => {
|
|
40
|
+
if (props.running) return "会话进行中,发送的消息将进入待发送队列";
|
|
41
|
+
return props.placeholder ?? "输入消息,Enter 发送,Shift + Enter 换行";
|
|
30
42
|
});
|
|
43
|
+
/** 多行自适应高度,上限 160px */
|
|
31
44
|
const autoResize = () => {
|
|
32
45
|
const el = textareaRef.value;
|
|
33
46
|
if (!el) return;
|
|
@@ -35,17 +48,29 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
35
48
|
el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
|
|
36
49
|
};
|
|
37
50
|
const handleSend = () => {
|
|
38
|
-
if (!
|
|
51
|
+
if (!hasContent.value) return;
|
|
39
52
|
emit("send", text.value.trim(), attachments.value);
|
|
40
53
|
text.value = "";
|
|
41
54
|
attachments.value = [];
|
|
42
55
|
autoResize();
|
|
43
56
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
/** 取回内容到输入框(队列编辑场景),并聚焦 */
|
|
58
|
+
const setContent = (content) => {
|
|
59
|
+
text.value = content;
|
|
60
|
+
nextTick(() => {
|
|
61
|
+
autoResize();
|
|
62
|
+
textareaRef.value?.focus();
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
/** 读取当前输入框内容 */
|
|
66
|
+
const getContent = () => text.value;
|
|
67
|
+
__expose({
|
|
68
|
+
setContent,
|
|
69
|
+
getContent
|
|
70
|
+
});
|
|
71
|
+
/** 将拾取的文件转为 dataUrl 附件(超限忽略) */
|
|
72
|
+
const handlePick = (files) => {
|
|
73
|
+
const maxSize = props.maxAttachmentSize ?? 10485760;
|
|
49
74
|
for (const file of files) {
|
|
50
75
|
if (file.size > maxSize) {
|
|
51
76
|
console.warn(`[UAiChat] 附件 ${file.name} 超过大小限制,已忽略`);
|
|
@@ -88,50 +113,62 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
88
113
|
ref: textareaRef,
|
|
89
114
|
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => text.value = $event),
|
|
90
115
|
class: normalizeClass(unref(cls).e("input")),
|
|
91
|
-
placeholder:
|
|
116
|
+
placeholder: placeholderText.value,
|
|
92
117
|
rows: "1",
|
|
93
118
|
onKeydown: withKeys(withModifiers(handleSend, ["exact", "prevent"]), ["enter"]),
|
|
94
119
|
onInput: autoResize
|
|
95
120
|
}, null, 42, _hoisted_2), [[vModelText, text.value]]),
|
|
96
121
|
createElementVNode("div", { class: normalizeClass(unref(cls).e("input-toolbar")) }, [
|
|
97
|
-
createVNode(unref(
|
|
98
|
-
class: normalizeClass(unref(cls).e("input-attach")),
|
|
99
|
-
title: "添加图片",
|
|
100
|
-
onClick: _cache[1] || (_cache[1] = ($event) => fileRef.value?.click())
|
|
101
|
-
}, {
|
|
102
|
-
default: withCtx(() => [createVNode(unref(Attach))]),
|
|
103
|
-
_: 1
|
|
104
|
-
}, 8, ["class"]),
|
|
105
|
-
createElementVNode("input", {
|
|
106
|
-
ref_key: "fileRef",
|
|
107
|
-
ref: fileRef,
|
|
108
|
-
type: "file",
|
|
109
|
-
hidden: "",
|
|
122
|
+
createElementVNode("div", { class: normalizeClass(unref(cls).e("input-toolbar-left")) }, [createVNode(unref(UFilePicker), {
|
|
110
123
|
multiple: "",
|
|
111
124
|
accept: __props.accept ?? "image/*",
|
|
112
|
-
|
|
113
|
-
},
|
|
114
|
-
|
|
125
|
+
onPick: handlePick
|
|
126
|
+
}, {
|
|
127
|
+
default: withCtx(() => [createVNode(unref(UIcon), {
|
|
128
|
+
class: normalizeClass(unref(cls).e("input-attach")),
|
|
129
|
+
title: "添加图片"
|
|
130
|
+
}, {
|
|
131
|
+
default: withCtx(() => [createVNode(unref(Attach))]),
|
|
132
|
+
_: 1
|
|
133
|
+
}, 8, ["class"])]),
|
|
134
|
+
_: 1
|
|
135
|
+
}, 8, ["accept"])], 2),
|
|
136
|
+
createCommentVNode(" 右簇:模型/推理选择 → 发送或停止(互斥) "),
|
|
137
|
+
createElementVNode("div", { class: normalizeClass(unref(cls).e("input-toolbar-right")) }, [__props.models?.length ? (openBlock(), createBlock(_sfc_main$1, {
|
|
115
138
|
key: 0,
|
|
139
|
+
model: model.value,
|
|
140
|
+
"onUpdate:model": _cache[1] || (_cache[1] = ($event) => model.value = $event),
|
|
141
|
+
"reasoning-level": reasoningLevel.value,
|
|
142
|
+
"onUpdate:reasoningLevel": _cache[2] || (_cache[2] = ($event) => reasoningLevel.value = $event),
|
|
143
|
+
models: __props.models
|
|
144
|
+
}, null, 8, [
|
|
145
|
+
"model",
|
|
146
|
+
"reasoning-level",
|
|
147
|
+
"models"
|
|
148
|
+
])) : createCommentVNode("v-if", true), showStop.value ? (openBlock(), createBlock(unref(UButton), {
|
|
149
|
+
key: "stop",
|
|
150
|
+
size: "small",
|
|
116
151
|
type: "danger",
|
|
117
152
|
circle: "",
|
|
118
|
-
|
|
153
|
+
title: "停止生成",
|
|
154
|
+
onClick: _cache[3] || (_cache[3] = ($event) => emit("abort"))
|
|
119
155
|
}, {
|
|
120
156
|
default: withCtx(() => [createElementVNode("span", { class: normalizeClass(unref(cls).e("input-stop")) }, null, 2)]),
|
|
121
157
|
_: 1
|
|
122
158
|
})) : (openBlock(), createBlock(unref(UButton), {
|
|
123
|
-
key:
|
|
159
|
+
key: "send",
|
|
160
|
+
size: "small",
|
|
124
161
|
type: "primary",
|
|
125
162
|
circle: "",
|
|
126
|
-
disabled: !
|
|
163
|
+
disabled: !hasContent.value,
|
|
164
|
+
icon: unref(Up),
|
|
165
|
+
title: __props.running ? "加入待发送队列" : void 0,
|
|
127
166
|
onClick: handleSend
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
_: 1
|
|
134
|
-
}, 8, ["disabled"]))
|
|
167
|
+
}, null, 8, [
|
|
168
|
+
"disabled",
|
|
169
|
+
"icon",
|
|
170
|
+
"title"
|
|
171
|
+
]))], 2)
|
|
135
172
|
], 2)
|
|
136
173
|
], 2);
|
|
137
174
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-input.js","names":[],"sources":["../../../src/components/ai-chat/chat-input.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('input-area')\">\n <div v-if=\"attachments.length\" :class=\"cls.e('input-attachments')\">\n <div\n v-for=\"(att, index) in attachments\"\n :key=\"att.dataUrl\"\n :class=\"cls.e('input-attachment')\"\n >\n <img :src=\"att.dataUrl\" :alt=\"att.name\" />\n <UIcon :class=\"cls.e('input-attachment-remove')\" @click=\"attachments.splice(index, 1)\">\n <Close />\n </UIcon>\n </div>\n </div>\n\n <textarea\n ref=\"textareaRef\"\n v-model=\"text\"\n :class=\"cls.e('input')\"\n :placeholder=\"
|
|
1
|
+
{"version":3,"file":"chat-input.js","names":[],"sources":["../../../src/components/ai-chat/chat-input.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('input-area')\">\n <div v-if=\"attachments.length\" :class=\"cls.e('input-attachments')\">\n <div\n v-for=\"(att, index) in attachments\"\n :key=\"att.dataUrl\"\n :class=\"cls.e('input-attachment')\"\n >\n <img :src=\"att.dataUrl\" :alt=\"att.name\" />\n <UIcon :class=\"cls.e('input-attachment-remove')\" @click=\"attachments.splice(index, 1)\">\n <Close />\n </UIcon>\n </div>\n </div>\n\n <textarea\n ref=\"textareaRef\"\n v-model=\"text\"\n :class=\"cls.e('input')\"\n :placeholder=\"placeholderText\"\n rows=\"1\"\n @keydown.enter.exact.prevent=\"handleSend\"\n @input=\"autoResize\"\n />\n\n <div :class=\"cls.e('input-toolbar')\">\n <div :class=\"cls.e('input-toolbar-left')\">\n <UFilePicker multiple :accept=\"accept ?? 'image/*'\" @pick=\"handlePick\">\n <UIcon :class=\"cls.e('input-attach')\" title=\"添加图片\">\n <Attach />\n </UIcon>\n </UFilePicker>\n </div>\n\n <!-- 右簇:模型/推理选择 → 发送或停止(互斥) -->\n <div :class=\"cls.e('input-toolbar-right')\">\n <ModelPicker\n v-if=\"models?.length\"\n v-model:model=\"model\"\n v-model:reasoning-level=\"reasoningLevel\"\n :models=\"models\"\n />\n\n <UButton\n v-if=\"showStop\"\n key=\"stop\"\n size=\"small\"\n type=\"danger\"\n circle\n title=\"停止生成\"\n @click=\"emit('abort')\"\n >\n <span :class=\"cls.e('input-stop')\" />\n </UButton>\n <UButton\n v-else\n key=\"send\"\n size=\"small\"\n type=\"primary\"\n circle\n :disabled=\"!hasContent\"\n :icon=\"Up\"\n :title=\"running ? '加入待发送队列' : undefined\"\n @click=\"handleSend\"\n />\n </div>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UFilePicker, UIcon } from '@veltra/desktop'\nimport { Attach, Close, Up } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, ref, shallowRef } from 'vue'\n\nimport type { ChatAttachment } from '../../chat/types'\nimport type { ChatModelOption } from '../../providers'\nimport { AiChatDIKey } from './di'\nimport ModelPicker from './model-picker.vue'\n\ndefineOptions({ name: 'UAiChatInput' })\n\nconst props = defineProps<{\n /** 是否生成中(空输入显示停止,有内容则发送入队) */\n running: boolean\n /** 可选模型列表;有值则显示选择器 */\n models?: ChatModelOption[]\n /** 输入框占位文本 */\n placeholder?: string\n /** 附件 accept 类型 */\n accept?: string\n /** 单个附件最大字节数 */\n maxAttachmentSize?: number\n}>()\n\nconst model = defineModel<string>('model')\nconst reasoningLevel = defineModel<string>('reasoningLevel')\n\nconst emit = defineEmits<{\n (e: 'send', content: string, attachments: ChatAttachment[]): void\n (e: 'abort'): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst text = ref('')\nconst attachments = ref<ChatAttachment[]>([])\nconst textareaRef = shallowRef<HTMLTextAreaElement>()\n\nconst hasContent = computed(() => !!text.value.trim() || attachments.value.length > 0)\n\n/** 生成中且输入为空时显示停止;有内容则显示发送(入队) */\nconst showStop = computed(() => props.running && !hasContent.value)\n\n/** 生成中提示用户消息将进入队列 */\nconst placeholderText = computed(() => {\n if (props.running) return '会话进行中,发送的消息将进入待发送队列'\n return props.placeholder ?? '输入消息,Enter 发送,Shift + Enter 换行'\n})\n\n/** 多行自适应高度,上限 160px */\nconst autoResize = () => {\n const el = textareaRef.value\n if (!el) return\n el.style.height = 'auto'\n el.style.height = `${Math.min(el.scrollHeight, 160)}px`\n}\n\nconst handleSend = () => {\n if (!hasContent.value) return\n emit('send', text.value.trim(), attachments.value)\n text.value = ''\n attachments.value = []\n autoResize()\n}\n\n/** 取回内容到输入框(队列编辑场景),并聚焦 */\nconst setContent = (content: string) => {\n text.value = content\n void nextTick(() => {\n autoResize()\n textareaRef.value?.focus()\n })\n}\n\n/** 读取当前输入框内容 */\nconst getContent = () => text.value\n\ndefineExpose({ setContent, getContent })\n\n/** 将拾取的文件转为 dataUrl 附件(超限忽略) */\nconst handlePick = (files: File[]) => {\n const maxSize = props.maxAttachmentSize ?? 10 * 1024 * 1024\n\n for (const file of files) {\n if (file.size > maxSize) {\n console.warn(`[UAiChat] 附件 ${file.name} 超过大小限制,已忽略`)\n continue\n }\n const reader = new FileReader()\n reader.onload = () => {\n attachments.value.push({\n name: file.name,\n mimeType: file.type,\n size: file.size,\n dataUrl: String(reader.result)\n })\n }\n reader.readAsDataURL(file)\n }\n}\n</script>\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"di.js","names":[],"sources":["../../../src/components/ai-chat/di.ts"],"sourcesContent":["import type { BEM } from '@veltra/utils'\nimport type { ComputedRef, InjectionKey, Slots } from 'vue'\n\nimport type { ChatTool } from '../../chat/types'\n\nexport interface AiChatContext {\n /** ai-chat 组件的 BEM 类生成器 */\n cls: BEM<'ai-chat'>\n /** ai-chat 使用方提供的插槽(用于 tool-<name> 动态插槽) */\n slots: Slots\n /** 按 name 索引的工具定义(解析 icon/label/render/autoCollapse) */\n tools: ComputedRef<Record<string, ChatTool | undefined>>\n}\n\nexport const AiChatDIKey: InjectionKey<AiChatContext> = Symbol('AiChatDIKey')\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"di.js","names":[],"sources":["../../../src/components/ai-chat/di.ts"],"sourcesContent":["import type { BEM } from '@veltra/utils'\nimport type { ComputedRef, InjectionKey, Slots } from 'vue'\n\nimport type { ChatTool } from '../../chat/types'\n\nexport interface AiChatContext {\n /** ai-chat 组件的 BEM 类生成器 */\n cls: BEM<'ai-chat'>\n /** ai-chat 使用方提供的插槽(用于 tool-<name> 动态插槽) */\n slots: Slots\n /** 按 name 索引的工具定义(解析 icon/label/render/autoCollapse) */\n tools: ComputedRef<Record<string, ChatTool | undefined>>\n /** 打开右侧面板并聚焦到指定工具调用(renderTo: 'panel' 工具的「查看面板」入口) */\n openPanel: (toolCallId: string) => void\n}\n\nexport const AiChatDIKey: InjectionKey<AiChatContext> = Symbol('AiChatDIKey')\n"],"mappings":";AAgBA,MAAa,cAA2C,OAAO,aAAa"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { AiChatDIKey } from "./di.js";
|
|
2
2
|
import _sfc_main$1 from "./tool-call.js";
|
|
3
|
-
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeProps, normalizeClass, openBlock, ref, renderList, toDisplayString, unref,
|
|
3
|
+
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeProps, nextTick, normalizeClass, onBeforeUnmount, openBlock, ref, renderList, toDisplayString, unref, useTemplateRef, watch, withCtx } from "vue";
|
|
4
|
+
import { ArrowRight, CircleCheck, Copy, InfoCircle, Loading, Refresh } from "@veltra/icons/normal";
|
|
5
|
+
import { UButton, UIcon, UScroll, message } from "@veltra/desktop";
|
|
4
6
|
import { bem } from "@veltra/utils";
|
|
5
|
-
import { UIcon } from "@veltra/desktop";
|
|
6
|
-
import { ArrowDown, InfoCircle, Loading, Refresh } from "@veltra/icons/normal";
|
|
7
7
|
import MarkdownRender from "markstream-vue";
|
|
8
8
|
//#region src/components/ai-chat/message-item.vue
|
|
9
9
|
const _hoisted_1 = ["src", "alt"];
|
|
@@ -24,9 +24,34 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
24
24
|
/** 思考中:流式且正文还未开始输出 */
|
|
25
25
|
const isThinking = computed(() => isStreaming.value && !props.message.content);
|
|
26
26
|
const reasoningExpanded = ref(true);
|
|
27
|
+
/** 思考区吸附底部:用户在思考区内上滚时取消吸附,回到底部后恢复 */
|
|
28
|
+
const reasoningStick = ref(true);
|
|
29
|
+
/** 思考内容滚动区:流式输出时吸附底部,始终展示最新思考 */
|
|
30
|
+
const reasoningScrollRef = useTemplateRef("reasoningScrollRef");
|
|
31
|
+
const toggleReasoning = async () => {
|
|
32
|
+
reasoningExpanded.value = !reasoningExpanded.value;
|
|
33
|
+
if (!reasoningExpanded.value) return;
|
|
34
|
+
reasoningStick.value = true;
|
|
35
|
+
await nextTick();
|
|
36
|
+
reasoningScrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER });
|
|
37
|
+
};
|
|
38
|
+
const handleReasoningWheel = (e) => {
|
|
39
|
+
if (e.deltaY < 0) reasoningStick.value = false;
|
|
40
|
+
};
|
|
41
|
+
const handleReasoningScroll = (position) => {
|
|
42
|
+
if (position.sh - position.ch - position.y < 8) reasoningStick.value = true;
|
|
43
|
+
};
|
|
27
44
|
watch(() => props.message.status, (status) => {
|
|
28
|
-
if (status === "streaming")
|
|
29
|
-
|
|
45
|
+
if (status === "streaming") {
|
|
46
|
+
reasoningExpanded.value = true;
|
|
47
|
+
reasoningStick.value = true;
|
|
48
|
+
} else if (status) reasoningExpanded.value = false;
|
|
49
|
+
});
|
|
50
|
+
watch(() => props.message.reasoning, async () => {
|
|
51
|
+
if (!isThinking.value || !reasoningExpanded.value || !reasoningStick.value) return;
|
|
52
|
+
await nextTick();
|
|
53
|
+
if (!reasoningStick.value) return;
|
|
54
|
+
reasoningScrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER });
|
|
30
55
|
});
|
|
31
56
|
const showActions = computed(() => {
|
|
32
57
|
if (props.message.role !== "assistant" || !props.isLast) return false;
|
|
@@ -36,6 +61,25 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
36
61
|
"aborted"
|
|
37
62
|
].includes(props.message.status ?? "");
|
|
38
63
|
});
|
|
64
|
+
const canCopy = computed(() => !!props.message.content?.trim());
|
|
65
|
+
const copied = ref(false);
|
|
66
|
+
let copyTimer;
|
|
67
|
+
const handleCopy = async () => {
|
|
68
|
+
const text = props.message.content?.trim();
|
|
69
|
+
if (!text) return;
|
|
70
|
+
try {
|
|
71
|
+
await navigator.clipboard.writeText(text);
|
|
72
|
+
copied.value = true;
|
|
73
|
+
message.success("已复制到剪贴板");
|
|
74
|
+
clearTimeout(copyTimer);
|
|
75
|
+
copyTimer = setTimeout(() => {
|
|
76
|
+
copied.value = false;
|
|
77
|
+
}, 2e3);
|
|
78
|
+
} catch {
|
|
79
|
+
message.error("复制失败,请检查浏览器权限");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
onBeforeUnmount(() => clearTimeout(copyTimer));
|
|
39
83
|
return (_ctx, _cache) => {
|
|
40
84
|
return openBlock(), createElementBlock("div", { class: normalizeClass([
|
|
41
85
|
unref(cls).e("message"),
|
|
@@ -62,20 +106,36 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
62
106
|
__props.message.reasoning ? (openBlock(), createElementBlock("div", {
|
|
63
107
|
key: 0,
|
|
64
108
|
class: normalizeClass([unref(cls).e("reasoning"), unref(bem).is("expanded", reasoningExpanded.value)])
|
|
65
|
-
}, [createElementVNode("div", {
|
|
66
|
-
class: normalizeClass(unref(cls).e("reasoning-header")),
|
|
67
|
-
onClick: _cache[0] || (_cache[0] = ($event) => reasoningExpanded.value = !reasoningExpanded.value)
|
|
68
109
|
}, [
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
110
|
+
createElementVNode("div", {
|
|
111
|
+
class: normalizeClass(unref(cls).e("reasoning-header")),
|
|
112
|
+
onClick: toggleReasoning
|
|
113
|
+
}, [
|
|
114
|
+
createVNode(unref(UIcon), { class: normalizeClass([unref(cls).e("reasoning-icon"), unref(bem).is("loading", isThinking.value)]) }, {
|
|
115
|
+
default: withCtx(() => [isThinking.value ? (openBlock(), createBlock(unref(Loading), { key: 0 })) : (openBlock(), createBlock(unref(InfoCircle), { key: 1 }))]),
|
|
116
|
+
_: 1
|
|
117
|
+
}, 8, ["class"]),
|
|
118
|
+
createElementVNode("span", { class: normalizeClass([unref(cls).e("reasoning-title"), isThinking.value && "u-shine"]) }, toDisplayString(isThinking.value ? "思考中…" : "思考过程"), 3),
|
|
119
|
+
createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("reasoning-chevron")) }, {
|
|
120
|
+
default: withCtx(() => [createVNode(unref(ArrowRight))]),
|
|
121
|
+
_: 1
|
|
122
|
+
}, 8, ["class"])
|
|
123
|
+
], 2),
|
|
124
|
+
createCommentVNode(" 折叠时完全卸载内容 DOM:思考文本可能很长,常驻挂载浪费内存与渲染成本 "),
|
|
125
|
+
reasoningExpanded.value ? (openBlock(), createElementBlock("div", {
|
|
126
|
+
key: 0,
|
|
127
|
+
class: normalizeClass(unref(cls).e("reasoning-content"))
|
|
128
|
+
}, [createVNode(unref(UScroll), {
|
|
129
|
+
ref_key: "reasoningScrollRef",
|
|
130
|
+
ref: reasoningScrollRef,
|
|
131
|
+
"container-style": "max-height: 220px",
|
|
132
|
+
onScroll: handleReasoningScroll,
|
|
133
|
+
onWheelPassive: handleReasoningWheel
|
|
134
|
+
}, {
|
|
135
|
+
default: withCtx(() => [createElementVNode("div", { class: normalizeClass(unref(cls).e("reasoning-text")) }, toDisplayString(__props.message.reasoning), 3)]),
|
|
76
136
|
_: 1
|
|
77
|
-
},
|
|
78
|
-
], 2)
|
|
137
|
+
}, 512)], 2)) : createCommentVNode("v-if", true)
|
|
138
|
+
], 2)) : createCommentVNode("v-if", true),
|
|
79
139
|
__props.message.content || isStreaming.value ? (openBlock(), createBlock(unref(MarkdownRender), mergeProps({
|
|
80
140
|
key: 1,
|
|
81
141
|
mode: "chat",
|
|
@@ -104,12 +164,30 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
104
164
|
}, " 已停止生成 ", 2)) : createCommentVNode("v-if", true),
|
|
105
165
|
showActions.value ? (openBlock(), createElementBlock("div", {
|
|
106
166
|
key: 4,
|
|
107
|
-
class: normalizeClass(unref(cls).e("message-actions"))
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
167
|
+
class: normalizeClass(unref(cls).e("message-actions"))
|
|
168
|
+
}, [createVNode(unref(UButton), {
|
|
169
|
+
size: "small",
|
|
170
|
+
plain: "",
|
|
171
|
+
circle: "",
|
|
172
|
+
type: copied.value ? "success" : "primary",
|
|
173
|
+
icon: copied.value ? unref(CircleCheck) : unref(Copy),
|
|
174
|
+
disabled: !canCopy.value,
|
|
175
|
+
title: copied.value ? "已复制" : "复制",
|
|
176
|
+
onClick: handleCopy
|
|
177
|
+
}, null, 8, [
|
|
178
|
+
"type",
|
|
179
|
+
"icon",
|
|
180
|
+
"disabled",
|
|
181
|
+
"title"
|
|
182
|
+
]), createVNode(unref(UButton), {
|
|
183
|
+
size: "small",
|
|
184
|
+
plain: "",
|
|
185
|
+
circle: "",
|
|
186
|
+
type: "primary",
|
|
187
|
+
icon: unref(Refresh),
|
|
188
|
+
title: "重新生成",
|
|
189
|
+
onClick: _cache[0] || (_cache[0] = ($event) => emit("regenerate"))
|
|
190
|
+
}, null, 8, ["icon"])], 2)) : createCommentVNode("v-if", true)
|
|
113
191
|
], 64))], 2);
|
|
114
192
|
};
|
|
115
193
|
}
|
|
@@ -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=\"reasoningExpanded = !reasoningExpanded\">\n <UIcon :class=\"[cls.e('reasoning-icon'), isThinking && cls.is('loading')]\">\n <Loading v-if=\"isThinking\" />\n <InfoCircle v-else />\n </UIcon>\n <span>{{ isThinking ? '思考中…' : '思考过程' }}</span>\n <UIcon :class=\"cls.e('reasoning-chevron')\"><ArrowDown /></UIcon>\n </div>\n <div v-show=\"reasoningExpanded\" :class=\"cls.e('reasoning-content')\">\n {{ message.reasoning }}\n </div>\n </div>\n\n <MarkdownRender\n v-if=\"message.content || isStreaming\"\n mode=\"chat\"\n :class=\"cls.e('message-content')\"\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')\" @click=\"emit('regenerate')\">\n <UIcon title=\"重新生成\"><Refresh /></UIcon>\n </div>\n </template>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon } from '@veltra/desktop'\nimport { ArrowDown, InfoCircle, Loading, Refresh } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport MarkdownRender from 'markstream-vue'\nimport { computed, inject, ref, 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// 流式结束后自动折叠思考过程\nwatch(\n () => props.message.status,\n (status) => {\n if (status === 'streaming') {\n reasoningExpanded.value = true\n } else if (status) {\n reasoningExpanded.value = false\n }\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</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 <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 :class=\"cls.e('message-content')\"\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// 流式结束后自动折叠思考过程\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)\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":";;;;;;;;;;;;;;;;;;;EA8GA,MAAM,QAAQ;EAQd,MAAM,OAAO;EAMb,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;EAEpC,MAAM,cAAc,eAAe,MAAM,QAAQ,WAAW,WAAW;;EAEvE,MAAM,aAAa,eAAe,YAAY,SAAS,CAAC,MAAM,QAAQ,OAAO;EAE7E,MAAM,oBAAoB,IAAI,IAAI;;EAGlC,MAAM,iBAAiB,IAAI,IAAI;;EAG/B,MAAM,qBAAqB,eAA6C,oBAAoB;EAE5F,MAAM,kBAAkB,YAAY;GAClC,kBAAkB,QAAQ,CAAC,kBAAkB;GAC7C,IAAI,CAAC,kBAAkB,OAAO;GAE9B,eAAe,QAAQ;GACvB,MAAM,SAAS;GACf,mBAAmB,OAAO,SAAS,EAAE,GAAG,OAAO,iBAAiB,CAAC;EACnE;EAEA,MAAM,wBAAwB,MAAkB;GAC9C,IAAI,EAAE,SAAS,GAAG,eAAe,QAAQ;EAC3C;EAEA,MAAM,yBAAyB,aAAuC;GACpE,IAAI,SAAS,KAAK,SAAS,KAAK,SAAS,IAAI,GAAG,eAAe,QAAQ;EACzE;EAGA,YACQ,MAAM,QAAQ,SACnB,WAAW;GACV,IAAI,WAAW,aAAa;IAC1B,kBAAkB,QAAQ;IAC1B,eAAe,QAAQ;GACzB,OAAO,IAAI,QACT,kBAAkB,QAAQ;EAE9B,CACF;EAEA,YACQ,MAAM,QAAQ,WACpB,YAAY;GACV,IAAI,CAAC,WAAW,SAAS,CAAC,kBAAkB,SAAS,CAAC,eAAe,OAAO;GAC5E,MAAM,SAAS;GAEf,IAAI,CAAC,eAAe,OAAO;GAC3B,mBAAmB,OAAO,SAAS,EAAE,GAAG,OAAO,iBAAiB,CAAC;EACnE,CACF;EAEA,MAAM,cAAc,eAAe;GACjC,IAAI,MAAM,QAAQ,SAAS,eAAe,CAAC,MAAM,QAAQ,OAAO;GAChE,OAAO;IAAC;IAAQ;IAAS;GAAS,CAAC,CAAC,SAAS,MAAM,QAAQ,UAAU,EAAE;EACzE,CAAC;EAED,MAAM,UAAU,eAAe,CAAC,CAAC,MAAM,QAAQ,SAAS,KAAK,CAAC;EAE9D,MAAM,SAAS,IAAI,KAAK;EACxB,IAAI;EAEJ,MAAM,aAAa,YAAY;GAC7B,MAAM,OAAO,MAAM,QAAQ,SAAS,KAAK;GACzC,IAAI,CAAC,MAAM;GACX,IAAI;IACF,MAAM,UAAU,UAAU,UAAU,IAAI;IACxC,OAAO,QAAQ;IACf,QAAM,QAAQ,SAAS;IACvB,aAAa,SAAS;IACtB,YAAY,iBAAiB;KAC3B,OAAO,QAAQ;IACjB,GAAG,GAAI;GACT,QAAQ;IACN,QAAM,MAAM,eAAe;GAC7B;EACF;EAEA,sBAAsB,aAAa,SAAS,CAAC;;GA5M3C,OAAA,UAAA,GAAA,mBA4FM,OAAA,EA3FH,OAAK,eAAA;IAAG,MAAA,GAAA,CAAG,CAAC,EAAC,SAAA;IAAa,MAAA,GAAA,CAAG,CAAC,GAAE,WAAY,QAAA,QAAQ,IAAI;IAAG,MAAA,GAAA,CAAG,CAAC,GAAE,aAAc,YAAA,KAAW;GAAA,CAAA,EAAA,GAE3F,CAAA,mBAAA,cAAA,GACgB,QAAA,QAAQ,SAAI,UAAA,UAAA,GAC1B,mBAWM,OAAA;;IAXA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,gBAAA,CAAA;GACL,GAAA,CAAA,QAAA,QAAQ,aAAa,UAAA,UAAA,GAAhC,mBAQM,OAAA;;IARmC,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,qBAAA,CAAA;GACnD,GAAA,EAAA,UAAA,IAAA,GAAA,mBAME,UAAA,MAAA,WALc,QAAA,QAAQ,cAAf,QAAG;IADZ,OAAA,UAAA,GAAA,mBAME,OAAA;KAJC,KAAK,IAAI;KACT,KAAK,IAAI;KACT,KAAK,IAAI;KACT,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,oBAAA,CAAA;;GAGN,CAAA,GAAA,GAAA,EAAA,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,GAAA,QAAA,QAAQ,WAAA,UAAA,GAAnB,mBAAsF,OAAA;;IAAzD,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;GAAqB,GAAA,gBAAA,QAAA,QAAQ,OAAO,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,CAAA,GAAA,CAKjF,MAAA,UAAA,GAAA,mBAuEW,UAAA,EAAA,KAAA,EAAA,GAAA;IAxEX,mBAAA,gBAAA;IAGU,QAAA,QAAQ,aAAA,UAAA,GADhB,mBAyBM,OAAA;;KAvBH,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,WAAA,GAAe,MAAA,GAAA,CAAG,CAAC,GAAE,YAAa,kBAAA,KAAiB,CAAA,CAAA;;KAEjE,mBASM,OAAA;MATA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,kBAAA,CAAA;MAAuB,SAAO;;MAC9C,YAGQ,MAAA,KAAA,GAAA,EAHA,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,gBAAA,GAAoB,MAAA,GAAA,CAAG,CAAC,GAAE,WAAY,WAAA,KAAU,CAAA,CAAA,EAAA,GAAA;OACtC,SAAA,cAAA,CAAd,WAAA,SAAA,UAAA,GAAf,YAA6B,MAAA,OAAA,GAAA,EAAA,KAAA,EAAA,CAAA,MAAA,UAAA,GAC7B,YAAqB,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA;;;MAEvB,mBAEO,QAAA,EAFA,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,iBAAA,GAAqB,WAAA,SAAU,SAAA,CAAA,EAAA,GAC9C,gBAAA,WAAA,QAAU,SAAA,MAAA,GAAA,CAAA;MAEf,YAAiE,MAAA,KAAA,GAAA,EAAzD,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA,EAAA,GAAA;OAAqC,SAAA,cAAA,CAAd,YAAc,MAAA,UAAA,CAAA,CAAA,CAAA;;;;KAE3D,mBAAA,wCAAA;KACW,kBAAA,SAAA,UAAA,GAAX,mBASM,OAAA;;MATyB,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA;KACzC,GAAA,CAAA,YAOU,MAAA,OAAA,GAAA;MANJ,SAAA;MAAJ,KAAI;MACJ,mBAAgB;MACf,UAAQ;MACO,gBAAA;;MAEmD,SAAA,cAAA,CAAnE,mBAAmE,OAAA,EAA7D,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,gBAAA,CAAA,EAAA,GAAuB,gBAAA,QAAA,QAAQ,SAAS,GAAA,CAAA,CAAA,CAAA;;;;IAMxD,QAAA,QAAQ,WAAW,YAAA,SAAA,UAAA,GAD3B,YAQE,MAAA,cAAA,GARF,WAQE;;KANA,MAAK;KACJ,OAAO,MAAA,GAAA,CAAG,CAAC,EAAC,iBAAA;KACZ,SAAS,QAAA,QAAQ;KACjB,OAAK,CAAG,YAAA;KACR,MAAM;IACC,GAAA,QAAA,aAAa,GAAA,MAAA,IAAA;KAAA;KAAA;KAAA;IAAA,CAAA,KAAA,mBAAA,QAAA,IAAA;KAGvB,UAAA,IAAA,GAAA,mBAKE,UAAA,MAAA,WAJe,QAAA,QAAQ,aAAS,CAAA,IAAzB,SAAI;KADb,OAAA,UAAA,GAAA,YAKE,aAAA;MAHC,KAAK,KAAK;MACV,aAAW;MACX,YAAU,aAAa,KAAI,WAAY,KAAK,IAAI,QAAQ;;;IAGhD,QAAA,QAAQ,WAAM,WAAA,UAAA,GAAzB,mBAAkF,OAAA;;KAA1C,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,gBAAA,CAAA;IAAoB,GAAA,QAAI,CAAA,KAC5D,QAAA,QAAQ,WAAM,aAAA,UAAA,GAA9B,mBAEM,OAAA;;KAFyC,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,gBAAA,CAAA;IAAoB,GAAA,WAE/E,CAAA,KAAA,mBAAA,QAAA,IAAA;IAEW,YAAA,SAAA,UAAA,GAAX,mBAoBM,OAAA;;KApBmB,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,iBAAA,CAAA;IACnC,GAAA,CAAA,YASE,MAAA,OAAA,GAAA;KARA,MAAK;KACL,OAAA;KACA,QAAA;KACC,MAAM,OAAA,QAAM,YAAA;KACZ,MAAM,OAAA,QAAS,MAAA,WAAA,IAAc,MAAA,IAAA;KAC7B,UAAQ,CAAG,QAAA;KACX,OAAO,OAAA,QAAM,QAAA;KACb,SAAO;;;;;;IAEV,CAAA,GAAA,YAQE,MAAA,OAAA,GAAA;KAPA,MAAK;KACL,OAAA;KACA,QAAA;KACA,MAAK;KACJ,MAAM,MAAA,OAAA;KACP,OAAM;KACL,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,YAAA"}
|