@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.
Files changed (87) hide show
  1. package/dist/chat/index.d.ts +3 -3
  2. package/dist/chat/transports/index.d.ts +2 -2
  3. package/dist/chat/transports/openai.d.ts +16 -12
  4. package/dist/chat/transports/openai.js +56 -15
  5. package/dist/chat/transports/openai.js.map +1 -1
  6. package/dist/chat/types.d.ts +41 -2
  7. package/dist/chat/use-chat.d.ts +26 -1
  8. package/dist/chat/use-chat.js +132 -13
  9. package/dist/chat/use-chat.js.map +1 -1
  10. package/dist/components/ai-chat/ai-chat.js +222 -41
  11. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  12. package/dist/components/ai-chat/ask-question.js +2 -2
  13. package/dist/components/ai-chat/ask-question.js.map +1 -1
  14. package/dist/components/ai-chat/chat-input.js +81 -44
  15. package/dist/components/ai-chat/chat-input.js.map +1 -1
  16. package/dist/components/ai-chat/di.js.map +1 -1
  17. package/dist/components/ai-chat/message-item.js +101 -23
  18. package/dist/components/ai-chat/message-item.js.map +1 -1
  19. package/dist/components/ai-chat/message-list.js +217 -32
  20. package/dist/components/ai-chat/message-list.js.map +1 -1
  21. package/dist/components/ai-chat/model-picker.js +140 -0
  22. package/dist/components/ai-chat/model-picker.js.map +1 -0
  23. package/dist/components/ai-chat/queue-list.js +70 -0
  24. package/dist/components/ai-chat/queue-list.js.map +1 -0
  25. package/dist/components/ai-chat/side-panel.js +96 -0
  26. package/dist/components/ai-chat/side-panel.js.map +1 -0
  27. package/dist/components/ai-chat/style.d.ts +6 -0
  28. package/dist/components/ai-chat/style.js +6 -0
  29. package/dist/components/ai-chat/style2.css +552 -41
  30. package/dist/components/ai-chat/tool-call.js +61 -27
  31. package/dist/components/ai-chat/tool-call.js.map +1 -1
  32. package/dist/components/ai-orb/ai-orb.js +94 -0
  33. package/dist/components/ai-orb/ai-orb.js.map +1 -0
  34. package/dist/components/ai-orb/orb-renderer.js +411 -0
  35. package/dist/components/ai-orb/orb-renderer.js.map +1 -0
  36. package/dist/components/ai-orb/style.d.ts +1 -0
  37. package/dist/components/ai-orb/style.js +1 -0
  38. package/dist/components/ai-orb/style2.css +23 -0
  39. package/dist/index.d.ts +8 -6
  40. package/dist/index.js +2 -2
  41. package/dist/providers/types.d.ts +47 -0
  42. package/dist/style.js +1 -0
  43. package/dist/tools/ask-question/ask-question.d.ts +2 -17
  44. package/dist/tools/ask-question/index.d.ts +2 -2
  45. package/dist/tools/index.d.ts +3 -2
  46. package/dist/tools/index.js +10 -0
  47. package/dist/tools/index.js.map +1 -0
  48. package/dist/types/ai-chat.d.ts +35 -7
  49. package/dist/types/ai-orb.d.ts +27 -0
  50. package/dist/types/index.d.ts +2 -1
  51. package/package.json +8 -8
  52. package/src/chat/__test__/use-chat.test.ts +295 -9
  53. package/src/chat/transports/__test__/openai.test.ts +317 -0
  54. package/src/chat/transports/openai.ts +88 -26
  55. package/src/chat/types.ts +41 -1
  56. package/src/chat/use-chat.ts +177 -14
  57. package/src/components/ai-chat/__test__/ai-chat.test.ts +690 -12
  58. package/src/components/ai-chat/ai-chat.vue +241 -31
  59. package/src/components/ai-chat/chat-input.vue +78 -31
  60. package/src/components/ai-chat/di.ts +2 -0
  61. package/src/components/ai-chat/message-item.vue +98 -11
  62. package/src/components/ai-chat/message-list.vue +261 -21
  63. package/src/components/ai-chat/model-picker.vue +177 -0
  64. package/src/components/ai-chat/queue-list.vue +63 -0
  65. package/src/components/ai-chat/side-panel.vue +76 -0
  66. package/src/components/ai-chat/style.scss +621 -43
  67. package/src/components/ai-chat/style.ts +6 -0
  68. package/src/components/ai-chat/tool-call.vue +86 -42
  69. package/src/components/ai-orb/__test__/ai-orb.test.ts +251 -0
  70. package/src/components/ai-orb/ai-orb.vue +107 -0
  71. package/src/components/ai-orb/index.ts +1 -0
  72. package/src/components/ai-orb/orb-renderer.ts +505 -0
  73. package/src/components/ai-orb/style.scss +10 -0
  74. package/src/components/ai-orb/style.ts +1 -0
  75. package/src/components/index.ts +1 -0
  76. package/src/index.ts +8 -1
  77. package/src/providers/index.ts +1 -0
  78. package/src/providers/types.ts +46 -0
  79. package/src/style.ts +1 -0
  80. package/src/tools/ask-question/__test__/ask-question.test.ts +4 -4
  81. package/src/tools/index.ts +14 -1
  82. package/src/types/ai-chat.ts +35 -6
  83. package/src/types/ai-orb.ts +27 -0
  84. package/src/types/index.ts +1 -0
  85. package/dist/components/ai-chat/ai-chat.vue.d.ts +0 -36
  86. package/dist/components/ai-chat/index.d.ts +0 -2
  87. package/dist/components/index.d.ts +0 -3
@@ -1,33 +1,46 @@
1
1
  import { AiChatDIKey } from "./di.js";
2
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, ref, renderList, shallowRef, unref, vModelText, withCtx, withDirectives, withKeys, withModifiers } from "vue";
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
- emits: ["send", "abort"],
20
- setup(__props, { emit: __emit }) {
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 fileRef = shallowRef();
28
- const canSend = computed(() => {
29
- return !props.running && (!!text.value.trim() || attachments.value.length > 0);
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 (!canSend.value) return;
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
- const handleFiles = (event) => {
45
- const input = event.target;
46
- const files = Array.from(input.files ?? []);
47
- input.value = "";
48
- const maxSize = props.maxAttachmentSize ?? 10 * 1024 * 1024;
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: __props.placeholder ?? "输入消息,Enter 发送,Shift + Enter 换行",
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(UIcon), {
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
- onChange: handleFiles
113
- }, null, 40, _hoisted_3),
114
- __props.running ? (openBlock(), createBlock(unref(UButton), {
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
- onClick: _cache[2] || (_cache[2] = ($event) => emit("abort"))
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: 1,
159
+ key: "send",
160
+ size: "small",
124
161
  type: "primary",
125
162
  circle: "",
126
- disabled: !canSend.value,
163
+ disabled: !hasContent.value,
164
+ icon: unref(Up),
165
+ title: __props.running ? "加入待发送队列" : void 0,
127
166
  onClick: handleSend
128
- }, {
129
- default: withCtx(() => [createVNode(unref(UIcon), null, {
130
- default: withCtx(() => [createVNode(unref(Send))]),
131
- _: 1
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=\"placeholder ?? '输入消息,Enter 发送,Shift + Enter 换行'\"\n rows=\"1\"\n @keydown.enter.exact.prevent=\"handleSend\"\n @input=\"autoResize\"\n />\n\n <div :class=\"cls.e('input-toolbar')\">\n <UIcon :class=\"cls.e('input-attach')\" title=\"添加图片\" @click=\"fileRef?.click()\">\n <Attach />\n </UIcon>\n <input\n ref=\"fileRef\"\n type=\"file\"\n hidden\n multiple\n :accept=\"accept ?? 'image/*'\"\n @change=\"handleFiles\"\n />\n\n <UButton v-if=\"running\" type=\"danger\" circle @click=\"emit('abort')\">\n <span :class=\"cls.e('input-stop')\" />\n </UButton>\n <UButton v-else type=\"primary\" circle :disabled=\"!canSend\" @click=\"handleSend\">\n <UIcon><Send /></UIcon>\n </UButton>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UIcon } from '@veltra/desktop'\nimport { Attach, Close, Send } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, shallowRef } from 'vue'\n\nimport type { ChatAttachment } from '../../chat/types'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatInput' })\n\nconst props = defineProps<{\n /** 是否生成中(显示停止按钮) */\n running: boolean\n /** 输入框占位文本 */\n placeholder?: string\n /** 附件 accept 类型 */\n accept?: string\n /** 单个附件最大字节数 */\n maxAttachmentSize?: number\n}>()\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>()\nconst fileRef = shallowRef<HTMLInputElement>()\n\nconst canSend = computed(() => {\n return !props.running && (!!text.value.trim() || attachments.value.length > 0)\n})\n\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 (!canSend.value) return\n emit('send', text.value.trim(), attachments.value)\n text.value = ''\n attachments.value = []\n autoResize()\n}\n\nconst handleFiles = (event: Event) => {\n const input = event.target as HTMLInputElement\n const files = Array.from(input.files ?? [])\n input.value = ''\n\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
+ {"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":";AAcA,MAAa,cAA2C,OAAO,aAAa"}
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, vShow, watch, withCtx, withDirectives } from "vue";
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") reasoningExpanded.value = true;
29
- else if (status) reasoningExpanded.value = false;
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
- createVNode(unref(UIcon), { class: normalizeClass([unref(cls).e("reasoning-icon"), isThinking.value && unref(cls).is("loading")]) }, {
70
- default: withCtx(() => [isThinking.value ? (openBlock(), createBlock(unref(Loading), { key: 0 })) : (openBlock(), createBlock(unref(InfoCircle), { key: 1 }))]),
71
- _: 1
72
- }, 8, ["class"]),
73
- createElementVNode("span", null, toDisplayString(isThinking.value ? "思考中…" : "思考过程"), 1),
74
- createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("reasoning-chevron")) }, {
75
- default: withCtx(() => [createVNode(unref(ArrowDown))]),
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
- }, 8, ["class"])
78
- ], 2), withDirectives(createElementVNode("div", { class: normalizeClass(unref(cls).e("reasoning-content")) }, toDisplayString(__props.message.reasoning), 3), [[vShow, reasoningExpanded.value]])], 2)) : createCommentVNode("v-if", true),
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
- onClick: _cache[1] || (_cache[1] = ($event) => emit("regenerate"))
109
- }, [createVNode(unref(UIcon), { title: "重新生成" }, {
110
- default: withCtx(() => [createVNode(unref(Refresh))]),
111
- _: 1
112
- })], 2)) : createCommentVNode("v-if", true)
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"}