@veltra/ai 2.1.10 → 2.2.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 (33) hide show
  1. package/dist/chat/use-chat.d.ts +15 -0
  2. package/dist/chat/use-chat.js +6 -0
  3. package/dist/chat/use-chat.js.map +1 -1
  4. package/dist/components/ai-chat/ai-chat.js +3 -1
  5. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  6. package/dist/components/ai-chat/chat-input.js +160 -77
  7. package/dist/components/ai-chat/chat-input.js.map +1 -1
  8. package/dist/components/ai-chat/format-duration.js +13 -0
  9. package/dist/components/ai-chat/format-duration.js.map +1 -0
  10. package/dist/components/ai-chat/message-item.js +32 -23
  11. package/dist/components/ai-chat/message-item.js.map +1 -1
  12. package/dist/components/ai-chat/message-list.js +40 -5
  13. package/dist/components/ai-chat/message-list.js.map +1 -1
  14. package/dist/components/ai-chat/model-picker.js +31 -95
  15. package/dist/components/ai-chat/model-picker.js.map +1 -1
  16. package/dist/components/ai-chat/reasoning-picker.js +68 -0
  17. package/dist/components/ai-chat/reasoning-picker.js.map +1 -0
  18. package/dist/components/ai-chat/style2.css +123 -68
  19. package/dist/providers/types.d.ts +2 -0
  20. package/dist/types/ai-chat.d.ts +2 -1
  21. package/package.json +1 -1
  22. package/src/chat/use-chat.ts +6 -0
  23. package/src/components/ai-chat/__test__/ai-chat.test.ts +79 -28
  24. package/src/components/ai-chat/ai-chat.vue +2 -0
  25. package/src/components/ai-chat/chat-input.vue +91 -16
  26. package/src/components/ai-chat/format-duration.ts +8 -0
  27. package/src/components/ai-chat/message-item.vue +6 -0
  28. package/src/components/ai-chat/message-list.vue +48 -1
  29. package/src/components/ai-chat/model-picker.vue +11 -110
  30. package/src/components/ai-chat/reasoning-picker.vue +70 -0
  31. package/src/components/ai-chat/style.scss +137 -72
  32. package/src/providers/types.ts +2 -0
  33. package/src/types/ai-chat.ts +2 -1
@@ -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=\"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\n <span :class=\"[cls.e('input-clear-wrap'), bem.is('disabled', !clearable)]\">\n <UPopConfirm\n title=\"清空当前对话?进行中的生成将被中止。\"\n confirm-text=\"清空\"\n direction=\"top\"\n alignment=\"start\"\n @confirm=\"emit('clear')\"\n >\n <template #reference>\n <UIcon :class=\"cls.e('input-clear')\" title=\"清除会话\">\n <Clear />\n </UIcon>\n </template>\n </UPopConfirm>\n </span>\n\n <span v-if=\"tokenUsage\" :class=\"cls.e('input-usage')\" :title=\"usageText\">\n {{ usageText }}\n </span>\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, UPopConfirm } from '@veltra/desktop'\nimport { Attach, Clear, Close, Up } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, ref, shallowRef, watch } from 'vue'\n\nimport type { ChatAttachment, ChatTokenUsage } 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 clearable?: boolean\n /** 会话累计 token;无 usage 时为 null,不展示 */\n tokenUsage?: ChatTokenUsage | null\n /** 是否展示缓存命中 / 未命中(默认只显示总 token) */\n tokenUsageDetail?: boolean\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 (e: 'clear'): 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/**\n * 用量数字:不到 1000 原样;≥1000 用 K;≥100 万用 M。\n * 最多 1 位小数,整数不写 `.0`(1000 → 1K,1500 → 1.5K)。\n */\nconst formatTokenCount = (n: number) => {\n const scaled = (value: number, unit: 'K' | 'M') => {\n const rounded = Math.round(value * 10) / 10\n return `${Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)}${unit}`\n }\n if (n < 1000) return String(n)\n if (n < 1_000_000) {\n const k = Math.round((n / 1000) * 10) / 10\n // 四舍五入后会变成 1000K 时改用 M\n return k >= 1000 ? scaled(n / 1_000_000, 'M') : scaled(n / 1000, 'K')\n }\n return scaled(n / 1_000_000, 'M')\n}\n\n/** 会话累计:总 token;明细再拼有数据的缓存项,缺字段不写 0 */\nconst usageText = computed(() => {\n const usage = props.tokenUsage\n if (!usage) return ''\n const parts = [`总 token ${formatTokenCount(usage.totalTokens)}`]\n if (props.tokenUsageDetail) {\n if (usage.cacheHitTokens != null) {\n parts.push(`缓存命中 ${formatTokenCount(usage.cacheHitTokens)}`)\n }\n if (usage.cacheMissTokens != null) {\n parts.push(`缓存未命中 ${formatTokenCount(usage.cacheMissTokens)}`)\n }\n }\n return parts.join(' · ')\n})\n\n/** 生成中提示用户消息将进入队列 */\nconst placeholderText = computed(() => {\n if (props.running) return '会话进行中,发送的消息将进入待发送队列'\n return props.placeholder ?? '输入消息,Enter 发送,Shift + Enter 换行'\n})\n\nconst MAX_INPUT_HEIGHT = 160\n\n/** 多行自适应高度,上限 160px */\nconst autoResize = () => {\n const el = textareaRef.value\n if (!el) return\n el.style.height = 'auto'\n const nextHeight = Math.min(el.scrollHeight, MAX_INPUT_HEIGHT)\n el.style.height = `${nextHeight}px`\n el.style.overflowY = el.scrollHeight > MAX_INPUT_HEIGHT ? 'auto' : 'hidden'\n}\n\nwatch(placeholderText, () => {\n void nextTick(autoResize)\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
+ {"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\n <span :class=\"[cls.e('input-clear-wrap'), bem.is('disabled', !clearable)]\">\n <UPopConfirm\n title=\"清空当前对话?进行中的生成将被中止。\"\n confirm-text=\"清空\"\n direction=\"top\"\n alignment=\"start\"\n @confirm=\"emit('clear')\"\n >\n <template #reference>\n <UIcon :class=\"cls.e('input-clear')\" title=\"清除会话\">\n <Clear />\n </UIcon>\n </template>\n </UPopConfirm>\n </span>\n </div>\n\n <!-- 右簇:token 用量环 → 模型/推理选择 → 发送或停止(互斥) -->\n <div :class=\"cls.e('input-toolbar-right')\">\n <UDropdown\n v-if=\"tokenUsage\"\n width=\"auto\"\n min-width=\"200px\"\n :content-class=\"cls.e('usage-panel')\"\n >\n <template #trigger>\n <button type=\"button\" :class=\"cls.e('usage-trigger')\" aria-label=\"Token 用量\">\n <svg :class=\"cls.e('usage-ring')\" viewBox=\"0 0 36 36\">\n <circle :class=\"cls.e('usage-ring-track')\" cx=\"18\" cy=\"18\" r=\"15.5\" />\n <circle\n :class=\"cls.e('usage-ring-value')\"\n cx=\"18\"\n cy=\"18\"\n r=\"15.5\"\n pathLength=\"100\"\n transform=\"rotate(-90 18 18)\"\n :stroke-dasharray=\"`${contextPercent ?? 0} 100`\"\n :stroke-linecap=\"contextPercent ? 'round' : undefined\"\n />\n </svg>\n </button>\n </template>\n\n <template #content>\n <div :class=\"cls.e('usage-list')\">\n <div v-if=\"contextText\" :class=\"cls.e('usage-row')\">\n <span :class=\"cls.e('usage-label')\">上下文占用</span>\n <span :class=\"cls.e('usage-value')\">{{ contextText }}</span>\n </div>\n <div v-for=\"row in usageRows\" :key=\"row.label\" :class=\"cls.e('usage-row')\">\n <span :class=\"cls.e('usage-label')\">{{ row.label }}</span>\n <span :class=\"cls.e('usage-value')\">{{ row.value }}</span>\n </div>\n </div>\n </template>\n </UDropdown>\n\n <ModelPicker v-if=\"models?.length\" v-model:model=\"model\" :models=\"models\" />\n\n <ReasoningPicker\n v-if=\"models?.length\"\n v-model:reasoning-level=\"reasoningLevel\"\n :levels=\"reasoningLevels\"\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, UDropdown, UFilePicker, UIcon, UPopConfirm } from '@veltra/desktop'\nimport { Attach, Clear, Close, Up } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, ref, shallowRef, watch } from 'vue'\n\nimport type { ChatAttachment, ChatTokenUsage } from '../../chat/types'\nimport type { ChatModelOption } from '../../providers'\nimport { AiChatDIKey } from './di'\nimport ModelPicker from './model-picker.vue'\nimport ReasoningPicker from './reasoning-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 clearable?: boolean\n /** 会话累计 token;无 usage 时为 null,不展示 */\n tokenUsage?: ChatTokenUsage | null\n /** 最近一次单次请求的 token(未累计);配合模型 contextWindow 计算用量环占比 */\n lastRequestUsage?: ChatTokenUsage | null\n /** 是否展示缓存命中 / 未命中(默认只显示总 token) */\n tokenUsageDetail?: boolean\n}>()\n\nconst model = defineModel<string>('model')\nconst reasoningLevel = defineModel<string>('reasoningLevel')\n\n/** 当前模型的推理等级列表,交给独立选择器;不足两档时选择器自身不渲染 */\nconst reasoningLevels = computed(() => {\n return props.models?.find((m) => m.id === model.value)?.reasoningLevels ?? []\n})\n\nconst emit = defineEmits<{\n (e: 'send', content: string, attachments: ChatAttachment[]): void\n (e: 'abort'): void\n (e: 'clear'): 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/**\n * 用量数字:不到 1000 原样;≥1000 用 K;≥100 万用 M。\n * 最多 1 位小数,整数不写 `.0`(1000 → 1K,1500 → 1.5K)。\n */\nconst formatTokenCount = (n: number) => {\n const scaled = (value: number, unit: 'K' | 'M') => {\n const rounded = Math.round(value * 10) / 10\n return `${Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)}${unit}`\n }\n if (n < 1000) return String(n)\n if (n < 1_000_000) {\n const k = Math.round((n / 1000) * 10) / 10\n // 四舍五入后会变成 1000K 时改用 M\n return k >= 1000 ? scaled(n / 1_000_000, 'M') : scaled(n / 1000, 'K')\n }\n return scaled(n / 1_000_000, 'M')\n}\n\n/** 百分比:最多 1 位小数,整数不写 `.0` */\nconst formatPercent = (p: number) => {\n const rounded = Math.round(p * 10) / 10\n return `${Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)}%`\n}\n\n/** 当前模型声明的上下文窗口;未声明则用量环不填充、面板不显示占用 */\nconst contextWindow = computed(() => {\n return props.models?.find((m) => m.id === model.value)?.contextWindow\n})\n\n/** 上下文占用百分比:最近一次请求总 token / 窗口上限;缺数据时为 null */\nconst contextPercent = computed(() => {\n const win = contextWindow.value\n const total = props.lastRequestUsage?.totalTokens\n if (!win || total == null) return null\n return Math.min(100, (total / win) * 100)\n})\n\n/** 面板占用行文案:1.5K / 128K · 1.2% */\nconst contextText = computed(() => {\n const percent = contextPercent.value\n const win = contextWindow.value\n const total = props.lastRequestUsage?.totalTokens\n if (percent == null || !win || total == null) return ''\n return `${formatTokenCount(total)} / ${formatTokenCount(win)} · ${formatPercent(percent)}`\n})\n\n/** 明细面板行:总 / 输入 / 输出;明细开关下再拼有数据的缓存项,缺字段不写 0 */\nconst usageRows = computed(() => {\n const usage = props.tokenUsage\n if (!usage) return []\n const rows = [\n { label: '总 token', value: formatTokenCount(usage.totalTokens) },\n { label: '输入', value: formatTokenCount(usage.promptTokens) },\n { label: '输出', value: formatTokenCount(usage.completionTokens) }\n ]\n if (props.tokenUsageDetail) {\n if (usage.cacheHitTokens != null) {\n rows.push({ label: '缓存命中', value: formatTokenCount(usage.cacheHitTokens) })\n }\n if (usage.cacheMissTokens != null) {\n rows.push({ label: '缓存未命中', value: formatTokenCount(usage.cacheMissTokens) })\n }\n }\n return rows\n})\n\n/** 生成中提示用户消息将进入队列 */\nconst placeholderText = computed(() => {\n if (props.running) return '会话进行中,发送的消息将进入待发送队列'\n return props.placeholder ?? '输入消息,Enter 发送,Shift + Enter 换行'\n})\n\nconst MAX_INPUT_HEIGHT = 160\n\n/** 多行自适应高度,上限 160px */\nconst autoResize = () => {\n const el = textareaRef.value\n if (!el) return\n el.style.height = 'auto'\n const nextHeight = Math.min(el.scrollHeight, MAX_INPUT_HEIGHT)\n el.style.height = `${nextHeight}px`\n el.style.overflowY = el.scrollHeight > MAX_INPUT_HEIGHT ? 'auto' : 'hidden'\n}\n\nwatch(placeholderText, () => {\n void nextTick(autoResize)\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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"}
@@ -0,0 +1,13 @@
1
+ //#region src/components/ai-chat/format-duration.ts
2
+ /** 格式化工作时长:整秒显示,≥60s 显示 Xm Ys */
3
+ const formatDuration = (ms) => {
4
+ const seconds = Math.round(ms / 1e3);
5
+ if (seconds < 60) return `${seconds}s`;
6
+ const minutes = Math.floor(seconds / 60);
7
+ const rest = seconds % 60;
8
+ return rest ? `${minutes}m ${rest}s` : `${minutes}m`;
9
+ };
10
+ //#endregion
11
+ export { formatDuration };
12
+
13
+ //# sourceMappingURL=format-duration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-duration.js","names":[],"sources":["../../../src/components/ai-chat/format-duration.ts"],"sourcesContent":["/** 格式化工作时长:整秒显示,≥60s 显示 Xm Ys */\nexport const formatDuration = (ms: number): string => {\n const seconds = Math.round(ms / 1000)\n if (seconds < 60) return `${seconds}s`\n const minutes = Math.floor(seconds / 60)\n const rest = seconds % 60\n return rest ? `${minutes}m ${rest}s` : `${minutes}m`\n}\n"],"mappings":";;AACA,MAAa,kBAAkB,OAAuB;CACpD,MAAM,UAAU,KAAK,MAAM,KAAK,GAAI;CACpC,IAAI,UAAU,IAAI,OAAO,GAAG,QAAQ;CACpC,MAAM,UAAU,KAAK,MAAM,UAAU,EAAE;CACvC,MAAM,OAAO,UAAU;CACvB,OAAO,OAAO,GAAG,QAAQ,IAAI,KAAK,KAAK,GAAG,QAAQ;AACpD"}
@@ -1,4 +1,5 @@
1
1
  import { AiChatDIKey } from "./di.js";
2
+ import { formatDuration } from "./format-duration.js";
2
3
  import _sfc_main$1 from "./tool-call.js";
3
4
  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
5
  import { ArrowRight, CircleCheck, Copy, InfoCircle, Loading, Refresh } from "@veltra/icons/normal";
@@ -13,6 +14,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
13
14
  props: {
14
15
  message: {},
15
16
  isLast: { type: Boolean },
17
+ duration: {},
16
18
  rendererProps: {}
17
19
  },
18
20
  emits: ["respond", "regenerate"],
@@ -170,29 +172,36 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
170
172
  showActions.value ? (openBlock(), createElementBlock("div", {
171
173
  key: 4,
172
174
  class: normalizeClass(unref(cls).e("message-actions"))
173
- }, [createVNode(unref(UButton), {
174
- size: "small",
175
- plain: "",
176
- circle: "",
177
- type: copied.value ? "success" : "primary",
178
- icon: copied.value ? unref(CircleCheck) : unref(Copy),
179
- disabled: !canCopy.value,
180
- title: copied.value ? "已复制" : "复制",
181
- onClick: handleCopy
182
- }, null, 8, [
183
- "type",
184
- "icon",
185
- "disabled",
186
- "title"
187
- ]), createVNode(unref(UButton), {
188
- size: "small",
189
- plain: "",
190
- circle: "",
191
- type: "primary",
192
- icon: unref(Refresh),
193
- title: "重新生成",
194
- onClick: _cache[0] || (_cache[0] = ($event) => emit("regenerate"))
195
- }, null, 8, ["icon"])], 2)) : createCommentVNode("v-if", true)
175
+ }, [
176
+ __props.duration != null ? (openBlock(), createElementBlock("span", {
177
+ key: 0,
178
+ class: normalizeClass(unref(cls).e("message-duration"))
179
+ }, " 用时 " + toDisplayString(unref(formatDuration)(__props.duration)), 3)) : createCommentVNode("v-if", true),
180
+ createVNode(unref(UButton), {
181
+ size: "small",
182
+ plain: "",
183
+ circle: "",
184
+ type: copied.value ? "success" : "primary",
185
+ icon: copied.value ? unref(CircleCheck) : unref(Copy),
186
+ disabled: !canCopy.value,
187
+ title: copied.value ? "已复制" : "复制",
188
+ onClick: handleCopy
189
+ }, null, 8, [
190
+ "type",
191
+ "icon",
192
+ "disabled",
193
+ "title"
194
+ ]),
195
+ createVNode(unref(UButton), {
196
+ size: "small",
197
+ plain: "",
198
+ circle: "",
199
+ type: "primary",
200
+ icon: unref(Refresh),
201
+ title: "重新生成",
202
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("regenerate"))
203
+ }, null, 8, ["icon"])
204
+ ], 2)) : createCommentVNode("v-if", true)
196
205
  ], 64))], 2);
197
206
  };
198
207
  }
@@ -1 +1 @@
1
- {"version":3,"file":"message-item.js","names":[],"sources":["../../../src/components/ai-chat/message-item.vue"],"sourcesContent":["<template>\n <div\n :class=\"[cls.e('message'), cls.em('message', message.role), bem.is('streaming', isStreaming)]\"\n >\n <!-- 用户消息:右对齐气泡 -->\n <template v-if=\"message.role === 'user'\">\n <div :class=\"cls.e('message-bubble')\">\n <div v-if=\"message.attachments?.length\" :class=\"cls.e('message-attachments')\">\n <img\n v-for=\"att in message.attachments\"\n :key=\"att.dataUrl\"\n :src=\"att.dataUrl\"\n :alt=\"att.name\"\n :class=\"cls.e('message-attachment')\"\n />\n </div>\n <div v-if=\"message.content\" :class=\"cls.e('message-text')\">{{ message.content }}</div>\n </div>\n </template>\n\n <!-- assistant 消息 -->\n <template v-else>\n <div\n v-if=\"message.reasoning\"\n :class=\"[cls.e('reasoning'), bem.is('expanded', reasoningExpanded)]\"\n >\n <div :class=\"cls.e('reasoning-header')\" @click=\"toggleReasoning\">\n <UIcon :class=\"[cls.e('reasoning-icon'), bem.is('loading', isThinking)]\">\n <Loading v-if=\"isThinking\" />\n <InfoCircle v-else />\n </UIcon>\n <span :class=\"[cls.e('reasoning-title'), isThinking && 'u-shine']\">\n {{ isThinking ? '思考中…' : '思考过程' }}\n </span>\n <!-- 折叠态在头部右侧滚动展示最新一行思考 -->\n <span v-if=\"latestReasoningLine\" :class=\"cls.e('reasoning-line')\">\n {{ latestReasoningLine }}\n </span>\n <UIcon :class=\"cls.e('reasoning-chevron')\"><ArrowRight /></UIcon>\n </div>\n <!-- 折叠时完全卸载内容 DOM:思考文本可能很长,常驻挂载浪费内存与渲染成本 -->\n <div v-if=\"reasoningExpanded\" :class=\"cls.e('reasoning-content')\">\n <UScroll\n ref=\"reasoningScrollRef\"\n container-style=\"max-height: 220px\"\n @scroll=\"handleReasoningScroll\"\n @wheel.passive=\"handleReasoningWheel\"\n >\n <div :class=\"cls.e('reasoning-text')\">{{ message.reasoning }}</div>\n </UScroll>\n </div>\n </div>\n\n <MarkdownRender\n v-if=\"message.content || isStreaming\"\n mode=\"chat\"\n :content=\"message.content\"\n :final=\"!isStreaming\"\n :fade=\"false\"\n v-bind=\"rendererProps\"\n />\n\n <ToolCallCard\n v-for=\"call in message.toolCalls ?? []\"\n :key=\"call.id\"\n :tool-call=\"call\"\n @respond=\"(approved) => emit('respond', call.id, approved)\"\n />\n\n <div v-if=\"message.status === 'error'\" :class=\"cls.e('message-status')\">生成出错</div>\n <div v-else-if=\"message.status === 'aborted'\" :class=\"cls.e('message-status')\">\n 已停止生成\n </div>\n\n <div v-if=\"showActions\" :class=\"cls.e('message-actions')\">\n <UButton\n size=\"small\"\n plain\n circle\n :type=\"copied ? 'success' : 'primary'\"\n :icon=\"copied ? CircleCheck : Copy\"\n :disabled=\"!canCopy\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"handleCopy\"\n />\n <UButton\n size=\"small\"\n plain\n circle\n type=\"primary\"\n :icon=\"Refresh\"\n title=\"重新生成\"\n @click=\"emit('regenerate')\"\n />\n </div>\n </template>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { message as toast, UButton, UIcon, UScroll } from '@veltra/desktop'\nimport type { ScrollPosition } from '@veltra/desktop'\nimport { ArrowRight, CircleCheck, Copy, InfoCircle, Loading, Refresh } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport MarkdownRender from 'markstream-vue'\nimport { computed, inject, nextTick, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport { AiChatDIKey } from './di'\nimport ToolCallCard from './tool-call.vue'\n\ndefineOptions({ name: 'UAiChatMessageItem' })\n\nconst props = defineProps<{\n message: ChatMessage\n /** 是否为最后一条可见消息(决定是否显示重新生成) */\n isLast: boolean\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{\n (e: 'respond', toolCallId: string, approved: boolean): void\n (e: 'regenerate'): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst isStreaming = computed(() => props.message.status === 'streaming')\n/** 思考中:流式且正文还未开始输出 */\nconst isThinking = computed(() => isStreaming.value && !props.message.content)\n\n/** 思考默认折叠(含流式进行中),用户点击头部展开 */\nconst reasoningExpanded = ref(false)\n\n/** 折叠时头部右侧滚动展示的最新一行思考:取最后一行非空文本,随流式输出持续刷新 */\nconst latestReasoningLine = computed(() => {\n if (!isThinking.value || reasoningExpanded.value) return ''\n const lines = (props.message.reasoning ?? '').split('\\n')\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i]?.trim()\n if (line) return line\n }\n return ''\n})\n\n/** 思考区吸附底部:用户在思考区内上滚时取消吸附,回到底部后恢复 */\nconst reasoningStick = ref(true)\n\n/** 思考内容滚动区:展开且流式输出时吸附底部,始终展示最新思考 */\nconst reasoningScrollRef = useTemplateRef<InstanceType<typeof UScroll>>('reasoningScrollRef')\n\nconst toggleReasoning = async () => {\n reasoningExpanded.value = !reasoningExpanded.value\n if (!reasoningExpanded.value) return\n // 重新展开时恢复吸附并定位到最新思考\n reasoningStick.value = true\n await nextTick()\n reasoningScrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n}\n\nconst handleReasoningWheel = (e: WheelEvent) => {\n if (e.deltaY < 0) reasoningStick.value = false\n}\n\nconst handleReasoningScroll = (position: Required<ScrollPosition>) => {\n if (position.sh - position.ch - position.y < 8) reasoningStick.value = true\n}\n\nwatch(\n () => props.message.reasoning,\n async () => {\n if (!isThinking.value || !reasoningExpanded.value || !reasoningStick.value) return\n await nextTick()\n // 等待 DOM 更新期间用户可能已上滚,滚动前重新确认吸附状态\n if (!reasoningStick.value) return\n reasoningScrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }\n)\n\nconst showActions = computed(() => {\n if (props.message.role !== 'assistant' || !props.isLast) return false\n return ['done', 'error', 'aborted'].includes(props.message.status ?? '')\n})\n\nconst canCopy = computed(() => !!props.message.content?.trim())\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\n\nconst handleCopy = async () => {\n const text = props.message.content?.trim()\n if (!text) return\n try {\n await navigator.clipboard.writeText(text)\n copied.value = true\n toast.success('已复制到剪贴板')\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => {\n copied.value = false\n }, 2000)\n } catch {\n toast.error('复制失败,请检查浏览器权限')\n }\n}\n\nonBeforeUnmount(() => clearTimeout(copyTimer))\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"message-item.js","names":[],"sources":["../../../src/components/ai-chat/message-item.vue"],"sourcesContent":["<template>\n <div\n :class=\"[cls.e('message'), cls.em('message', message.role), bem.is('streaming', isStreaming)]\"\n >\n <!-- 用户消息:右对齐气泡 -->\n <template v-if=\"message.role === 'user'\">\n <div :class=\"cls.e('message-bubble')\">\n <div v-if=\"message.attachments?.length\" :class=\"cls.e('message-attachments')\">\n <img\n v-for=\"att in message.attachments\"\n :key=\"att.dataUrl\"\n :src=\"att.dataUrl\"\n :alt=\"att.name\"\n :class=\"cls.e('message-attachment')\"\n />\n </div>\n <div v-if=\"message.content\" :class=\"cls.e('message-text')\">{{ message.content }}</div>\n </div>\n </template>\n\n <!-- assistant 消息 -->\n <template v-else>\n <div\n v-if=\"message.reasoning\"\n :class=\"[cls.e('reasoning'), bem.is('expanded', reasoningExpanded)]\"\n >\n <div :class=\"cls.e('reasoning-header')\" @click=\"toggleReasoning\">\n <UIcon :class=\"[cls.e('reasoning-icon'), bem.is('loading', isThinking)]\">\n <Loading v-if=\"isThinking\" />\n <InfoCircle v-else />\n </UIcon>\n <span :class=\"[cls.e('reasoning-title'), isThinking && 'u-shine']\">\n {{ isThinking ? '思考中…' : '思考过程' }}\n </span>\n <!-- 折叠态在头部右侧滚动展示最新一行思考 -->\n <span v-if=\"latestReasoningLine\" :class=\"cls.e('reasoning-line')\">\n {{ latestReasoningLine }}\n </span>\n <UIcon :class=\"cls.e('reasoning-chevron')\"><ArrowRight /></UIcon>\n </div>\n <!-- 折叠时完全卸载内容 DOM:思考文本可能很长,常驻挂载浪费内存与渲染成本 -->\n <div v-if=\"reasoningExpanded\" :class=\"cls.e('reasoning-content')\">\n <UScroll\n ref=\"reasoningScrollRef\"\n container-style=\"max-height: 220px\"\n @scroll=\"handleReasoningScroll\"\n @wheel.passive=\"handleReasoningWheel\"\n >\n <div :class=\"cls.e('reasoning-text')\">{{ message.reasoning }}</div>\n </UScroll>\n </div>\n </div>\n\n <MarkdownRender\n v-if=\"message.content || isStreaming\"\n mode=\"chat\"\n :content=\"message.content\"\n :final=\"!isStreaming\"\n :fade=\"false\"\n v-bind=\"rendererProps\"\n />\n\n <ToolCallCard\n v-for=\"call in message.toolCalls ?? []\"\n :key=\"call.id\"\n :tool-call=\"call\"\n @respond=\"(approved) => emit('respond', call.id, approved)\"\n />\n\n <div v-if=\"message.status === 'error'\" :class=\"cls.e('message-status')\">生成出错</div>\n <div v-else-if=\"message.status === 'aborted'\" :class=\"cls.e('message-status')\">\n 已停止生成\n </div>\n\n <div v-if=\"showActions\" :class=\"cls.e('message-actions')\">\n <span v-if=\"duration != null\" :class=\"cls.e('message-duration')\">\n 用时 {{ formatDuration(duration) }}\n </span>\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 { formatDuration } from './format-duration'\nimport ToolCallCard from './tool-call.vue'\n\ndefineOptions({ name: 'UAiChatMessageItem' })\n\nconst props = defineProps<{\n message: ChatMessage\n /** 是否为最后一条可见消息(决定是否显示重新生成) */\n isLast: boolean\n /** 本轮总用时(ms;仅 live 轮次有,历史回放无计时数据) */\n duration?: number\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{\n (e: 'respond', toolCallId: string, approved: boolean): void\n (e: 'regenerate'): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst isStreaming = computed(() => props.message.status === 'streaming')\n/** 思考中:流式且正文还未开始输出 */\nconst isThinking = computed(() => isStreaming.value && !props.message.content)\n\n/** 思考默认折叠(含流式进行中),用户点击头部展开 */\nconst reasoningExpanded = ref(false)\n\n/** 折叠时头部右侧滚动展示的最新一行思考:取最后一行非空文本,随流式输出持续刷新 */\nconst latestReasoningLine = computed(() => {\n if (!isThinking.value || reasoningExpanded.value) return ''\n const lines = (props.message.reasoning ?? '').split('\\n')\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i]?.trim()\n if (line) return line\n }\n return ''\n})\n\n/** 思考区吸附底部:用户在思考区内上滚时取消吸附,回到底部后恢复 */\nconst reasoningStick = ref(true)\n\n/** 思考内容滚动区:展开且流式输出时吸附底部,始终展示最新思考 */\nconst reasoningScrollRef = useTemplateRef<InstanceType<typeof UScroll>>('reasoningScrollRef')\n\nconst toggleReasoning = async () => {\n reasoningExpanded.value = !reasoningExpanded.value\n if (!reasoningExpanded.value) return\n // 重新展开时恢复吸附并定位到最新思考\n reasoningStick.value = true\n await nextTick()\n reasoningScrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n}\n\nconst handleReasoningWheel = (e: WheelEvent) => {\n if (e.deltaY < 0) reasoningStick.value = false\n}\n\nconst handleReasoningScroll = (position: Required<ScrollPosition>) => {\n if (position.sh - position.ch - position.y < 8) reasoningStick.value = true\n}\n\nwatch(\n () => props.message.reasoning,\n async () => {\n if (!isThinking.value || !reasoningExpanded.value || !reasoningStick.value) return\n await nextTick()\n // 等待 DOM 更新期间用户可能已上滚,滚动前重新确认吸附状态\n if (!reasoningStick.value) return\n reasoningScrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }\n)\n\nconst showActions = computed(() => {\n if (props.message.role !== 'assistant' || !props.isLast) return false\n return ['done', 'error', 'aborted'].includes(props.message.status ?? '')\n})\n\nconst canCopy = computed(() => !!props.message.content?.trim())\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\n\nconst handleCopy = async () => {\n const text = props.message.content?.trim()\n if (!text) return\n try {\n await navigator.clipboard.writeText(text)\n copied.value = true\n toast.success('已复制到剪贴板')\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => {\n copied.value = false\n }, 2000)\n } catch {\n toast.error('复制失败,请检查浏览器权限')\n }\n}\n\nonBeforeUnmount(() => clearTimeout(copyTimer))\n</script>\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { AiChatDIKey } from "./di.js";
2
2
  import _sfc_main$1 from "../ai-orb/ai-orb.js";
3
+ import { formatDuration } from "./format-duration.js";
3
4
  import _sfc_main$2 from "./message-item.js";
4
5
  import _sfc_main$3 from "./turn-process.js";
5
6
  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";
@@ -113,6 +114,22 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
113
114
  clearTimeout(finishTimer);
114
115
  clearTimeout(jumpTimer);
115
116
  };
117
+ /** 工作计时的起算时间戳(running 升起时记录) */
118
+ let workStartAt = 0;
119
+ /** 进行中的已逝时长(100ms 跳动刷新) */
120
+ const elapsedMs = ref(0);
121
+ let tickTimer;
122
+ /** 各轮次总用时(key 同 turns;仅 live 轮次有,历史回放无计时数据) */
123
+ const turnDurations = shallowRef(/* @__PURE__ */ new Map());
124
+ const stopTick = () => {
125
+ clearInterval(tickTimer);
126
+ tickTimer = void 0;
127
+ };
128
+ /** 收尾停留期间展示的末轮总用时 */
129
+ const lastTurnDuration = computed(() => {
130
+ const last = turns.value[turns.value.length - 1];
131
+ return last ? turnDurations.value.get(last.key) : void 0;
132
+ });
116
133
  /** 列表末尾的工作球:生成中 / 收尾停留 / 跳回过渡 */
117
134
  const showWorking = computed(() => !!(props.running || finishing.value || workingLeaving.value));
118
135
  /** 输入框上方的空闲欢迎:含工作开始时的缩小离开帧 */
@@ -145,6 +162,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
145
162
  watch(() => props.running, (running, prev) => {
146
163
  if (running) {
147
164
  clearJumpTimers();
165
+ workStartAt = Date.now();
166
+ elapsedMs.value = 0;
167
+ stopTick();
168
+ tickTimer = setInterval(() => {
169
+ elapsedMs.value = Date.now() - workStartAt;
170
+ }, 100);
148
171
  const idleWasVisible = !prev && !finishing.value && !workingLeaving.value;
149
172
  finishing.value = false;
150
173
  workingLeaving.value = false;
@@ -158,6 +181,13 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
158
181
  return;
159
182
  }
160
183
  if (!prev) return;
184
+ stopTick();
185
+ const lastTurn = turns.value[turns.value.length - 1];
186
+ if (lastTurn) {
187
+ const next = new Map(turnDurations.value);
188
+ next.set(lastTurn.key, Date.now() - workStartAt);
189
+ turnDurations.value = next;
190
+ }
161
191
  if (visibleMessages.value.length === 0) {
162
192
  finishing.value = false;
163
193
  workingLeaving.value = false;
@@ -187,6 +217,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
187
217
  watch(() => visibleMessages.value.length, (len) => {
188
218
  if (len > 0) return;
189
219
  clearJumpTimers();
220
+ turnDurations.value = /* @__PURE__ */ new Map();
190
221
  finishing.value = false;
191
222
  workingLeaving.value = false;
192
223
  idleLeaving.value = false;
@@ -199,6 +230,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
199
230
  onBeforeUnmount(() => {
200
231
  stopWelcomeRotation();
201
232
  clearJumpTimers();
233
+ stopTick();
202
234
  });
203
235
  const scrollRef = shallowRef();
204
236
  /** 是否吸附底部(用户上翻浏览历史时取消吸附) */
@@ -261,6 +293,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
261
293
  return openBlock(), createElementBlock("div", { class: normalizeClass(unref(cls).e("list-wrap")) }, [
262
294
  createVNode(unref(UScroll), {
263
295
  class: normalizeClass(unref(cls).e("list")),
296
+ "content-class": unref(cls).e("list-content"),
264
297
  ref_key: "scrollRef",
265
298
  ref: scrollRef,
266
299
  onScroll: handleScroll,
@@ -297,12 +330,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
297
330
  key: 3,
298
331
  message: turn.answerMsg,
299
332
  "is-last": turn.isLast,
333
+ duration: turnDurations.value.get(turn.key),
300
334
  "renderer-props": __props.rendererProps,
301
335
  onRespond: _cache[5] || (_cache[5] = (id, approved) => emit("respond", id, approved)),
302
336
  onRegenerate: _cache[6] || (_cache[6] = ($event) => emit("regenerate"))
303
337
  }, null, 8, [
304
338
  "message",
305
339
  "is-last",
340
+ "duration",
306
341
  "renderer-props"
307
342
  ])) : createCommentVNode("v-if", true)
308
343
  ], 64);
@@ -315,13 +350,13 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
315
350
  ref: "workingOrb",
316
351
  size: ORB_SIZE,
317
352
  status: workingOrbStatus.value
318
- }, null, 8, ["status"]), __props.running ? (openBlock(), createElementBlock("span", {
319
- key: 0,
320
- class: normalizeClass([unref(cls).e("working-text"), "u-shine"])
321
- }, "工作中…", 2)) : createCommentVNode("v-if", true)], 2)) : createCommentVNode("v-if", true)
353
+ }, null, 8, ["status"]), __props.running ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("span", { class: normalizeClass([unref(cls).e("working-text"), "u-shine"]) }, "工作中…", 2), createElementVNode("span", { class: normalizeClass(unref(cls).e("working-time")) }, toDisplayString(unref(formatDuration)(elapsedMs.value)), 3)], 64)) : lastTurnDuration.value != null ? (openBlock(), createElementBlock("span", {
354
+ key: 1,
355
+ class: normalizeClass(unref(cls).e("working-time"))
356
+ }, " 用时 " + toDisplayString(unref(formatDuration)(lastTurnDuration.value)), 3)) : createCommentVNode("v-if", true)], 2)) : createCommentVNode("v-if", true)
322
357
  ]),
323
358
  _: 1
324
- }, 8, ["class"]),
359
+ }, 8, ["class", "content-class"]),
325
360
  createCommentVNode(" 空闲:始终钉在滚动容器外、输入框上方;工作开始时缩小离开、结束后放大回来 "),
326
361
  showIdleWelcome.value ? (openBlock(), createElementBlock("div", {
327
362
  key: 0,
@@ -1 +1 @@
1
- {"version":3,"file":"message-list.js","names":[],"sources":["../../../src/components/ai-chat/message-list.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('list-wrap')\">\n <UScroll\n :class=\"cls.e('list')\"\n ref=\"scrollRef\"\n @scroll=\"handleScroll\"\n @wheel.passive=\"handleWheel\"\n >\n <template v-for=\"turn in turns\" :key=\"turn.key\">\n <MessageItem\n v-if=\"turn.userMsg\"\n :message=\"turn.userMsg\"\n :is-last=\"false\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n\n <!-- 最终答案开始输出后,本轮之前的思考/工具调用过程收进「已完成」折叠块 -->\n <TurnProcess\n v-if=\"turn.processCollapsed\"\n :messages=\"turn.processMsgs\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n />\n <template v-else>\n <MessageItem\n v-for=\"msg in turn.processMsgs\"\n :key=\"msg.id\"\n :message=\"msg\"\n :is-last=\"false\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n </template>\n\n <MessageItem\n v-if=\"turn.answerMsg\"\n :message=\"turn.answerMsg\"\n :is-last=\"turn.isLast\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n </template>\n\n <!-- 工作中:活体球从输入框上方跳到最新会话下面;结束停留后再跳回 -->\n <div v-if=\"showWorking\" :class=\"[cls.e('working'), bem.is('leaving', workingLeaving)]\">\n <UAiOrb ref=\"workingOrb\" :size=\"ORB_SIZE\" :status=\"workingOrbStatus\" />\n <span v-if=\"running\" :class=\"[cls.e('working-text'), 'u-shine']\">工作中…</span>\n </div>\n </UScroll>\n\n <!-- 空闲:始终钉在滚动容器外、输入框上方;工作开始时缩小离开、结束后放大回来 -->\n <div\n v-if=\"showIdleWelcome\"\n :class=\"[cls.e('welcome'), bem.is('leaving', idleLeaving), bem.is('enter', idleEnter)]\"\n >\n <slot name=\"welcome\">\n <div :class=\"cls.e('welcome-inner')\">\n <UAiOrb :size=\"ORB_SIZE\" @click=\"cycleWelcome\" />\n <!-- 快捷提问逐条展示在球右侧并自动轮换:点文案发送,点球立即换下一条。\n 固定锚点布局:球位置钉死不动,气泡只向右延展,避免轮换时水平瞬移 -->\n <div v-if=\"welcomeItems.length\" :class=\"cls.e('welcome-item-wrap')\">\n <Transition :name=\"cls.em('welcome', 'swap')\" mode=\"out-in\">\n <button\n v-if=\"currentWelcomeItem\"\n :key=\"welcomeIndex\"\n type=\"button\"\n :class=\"cls.e('welcome-item')\"\n :disabled=\"readonly\"\n @click=\"!readonly && emit('welcome-click', currentWelcomeItem)\"\n >\n {{ currentWelcomeItem }}\n </button>\n </Transition>\n </div>\n </div>\n </slot>\n </div>\n\n <!-- 用户上翻浏览时悬浮入口:一键回到最新消息 -->\n <Transition :name=\"cls.em('to-latest', 'fade')\">\n <button\n v-if=\"showToLatest\"\n type=\"button\"\n :class=\"cls.e('to-latest')\"\n title=\"回到最新\"\n @click=\"scrollToLatest\"\n >\n <UIcon><ArrowDown /></UIcon>\n </button>\n </Transition>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon, UScroll } from '@veltra/desktop'\nimport type { ScrollPosition } from '@veltra/desktop'\nimport { ArrowDown } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport {\n computed,\n inject,\n nextTick,\n onBeforeUnmount,\n ref,\n shallowRef,\n useTemplateRef,\n watch\n} from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport type { AiOrbExposed, AiOrbReaction, AiOrbStatus } from '../../types/ai-orb'\nimport UAiOrb from '../ai-orb/ai-orb.vue'\nimport { AiChatDIKey } from './di'\nimport MessageItem from './message-item.vue'\nimport TurnProcess from './turn-process.vue'\n\ndefineOptions({ name: 'UAiChatMessageList' })\n\n/** 空闲欢迎 / 工作指示共用活体球直径(px) */\nconst ORB_SIZE = 48\n/** 生成结束 / 失败后工作球停留再跳回输入框上方 */\nconst WORKING_LINGER_MS = 2500\n/** 两处活体球对向缩放的时长(与样式中的 jump 过渡一致) */\nconst JUMP_MS = 380\n\nconst props = defineProps<{\n messages: ChatMessage[]\n /** 空闲时输入框上方的快捷提问;字符串按单项处理 */\n welcome?: string | string[]\n /** 是否生成中(控制底部工作状态指示) */\n running?: boolean\n /** 只读时欢迎语不可点击发送 */\n readonly?: boolean\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{\n (e: 'respond', toolCallId: string, approved: boolean): void\n (e: 'regenerate'): void\n /** 点击欢迎项(快捷提问) */\n (e: 'welcome-click', text: string): void\n}>()\n\ndefineSlots<{\n /** 空闲欢迎区插槽(输入框上方;工作中跳到列表末尾) */\n welcome(): any\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** tool 消息不单独渲染,其内容体现在工具卡片的结果区 */\nconst visibleMessages = computed(() => props.messages.filter((msg) => msg.role !== 'tool'))\n\n/** 一轮对话:以 user 消息为界,含本轮全部 assistant 消息(每次工具循环产生一条) */\ninterface ChatTurn {\n /** 轮次 key(取用户消息或首条 assistant 消息 id) */\n key: string\n userMsg?: ChatMessage\n /** 最终答案之前的过程消息(思考 + 中间文本 + 工具卡片) */\n processMsgs: ChatMessage[]\n /** 本轮最终答案(最后一条 assistant 消息) */\n answerMsg?: ChatMessage\n /** 过程消息是否收进「已完成」折叠块 */\n processCollapsed: boolean\n /** answerMsg 是否为全局最后一条可见消息 */\n isLast: boolean\n}\n\nconst isFinalStatus = (msg?: ChatMessage) =>\n msg?.status === 'done' || msg?.status === 'error' || msg?.status === 'aborted'\n\nconst turns = computed<ChatTurn[]>(() => {\n const grouped: { key: string; userMsg?: ChatMessage; assistants: ChatMessage[] }[] = []\n for (const msg of visibleMessages.value) {\n if (msg.role === 'user') {\n grouped.push({ key: msg.id, userMsg: msg, assistants: [] })\n continue\n }\n const last = grouped[grouped.length - 1]\n // 兼容 assistant 开头的历史会话:归入无用户消息的轮次\n if (last) last.assistants.push(msg)\n else grouped.push({ key: msg.id, assistants: [msg] })\n }\n return grouped.map((turn, index) => {\n const processMsgs = turn.assistants.slice(0, -1)\n const answerMsg = turn.assistants[turn.assistants.length - 1]\n const isLastTurn = index === grouped.length - 1\n // 该轮结束(非最后一轮 / 答案进入终态)或最终答案开始输出时折叠过程。\n // content 只增不减,折叠后不会回弹;regenerate 删除本轮 assistant 重跑,条件自然复位\n const processCollapsed =\n processMsgs.length > 0 &&\n (!isLastTurn || !!answerMsg?.content || (!props.running && isFinalStatus(answerMsg)))\n return {\n key: turn.key,\n userMsg: turn.userMsg,\n processMsgs,\n answerMsg,\n processCollapsed,\n isLast: isLastTurn && !!answerMsg\n }\n })\n})\n\nconst welcomeItems = computed(() => {\n const welcome = props.welcome\n if (!welcome) return []\n return (Array.isArray(welcome) ? welcome : [welcome]).filter(Boolean)\n})\n\n/** 欢迎语逐条轮换:当前展示项的下标 */\nconst welcomeIndex = ref(0)\n\n/** 当前展示的欢迎语 */\nconst currentWelcomeItem = computed(() => {\n const items = welcomeItems.value\n return items.length ? items[welcomeIndex.value % items.length] : undefined\n})\n\nlet welcomeTimer: ReturnType<typeof setInterval> | undefined\n\nconst stopWelcomeRotation = () => {\n clearInterval(welcomeTimer)\n welcomeTimer = undefined\n}\n\nconst startWelcomeRotation = () => {\n stopWelcomeRotation()\n if (welcomeItems.value.length > 1) {\n welcomeTimer = setInterval(() => {\n welcomeIndex.value += 1\n }, 4000)\n }\n}\n\n/** 点击欢迎球:不等自动轮换,立即换下一条,并重置计时 */\nconst cycleWelcome = () => {\n if (welcomeItems.value.length <= 1) return\n welcomeIndex.value += 1\n startWelcomeRotation()\n}\n\n/** 回答完毕 / 失败后工作球短暂停留,播完表情再跳回输入框上方 */\nconst finishing = ref(false)\n/** 列表中的球正在缩小离开 */\nconst workingLeaving = ref(false)\n/** 输入框上方的球正在缩小离开 */\nconst idleLeaving = ref(false)\n/** 输入框上方的球从工作位放大回来(首屏出现不播) */\nconst idleEnter = ref(false)\n\nlet finishTimer: ReturnType<typeof setTimeout> | undefined\nlet jumpTimer: ReturnType<typeof setTimeout> | undefined\n\nconst clearJumpTimers = () => {\n clearTimeout(finishTimer)\n clearTimeout(jumpTimer)\n}\n\n/** 列表末尾的工作球:生成中 / 收尾停留 / 跳回过渡 */\nconst showWorking = computed(() => !!(props.running || finishing.value || workingLeaving.value))\n/** 输入框上方的空闲欢迎:含工作开始时的缩小离开帧 */\nconst showIdleWelcome = computed(() => (!props.running && !finishing.value) || idleLeaving.value)\n\nconst workingOrbRef = useTemplateRef<AiOrbExposed>('workingOrb')\n\n/** 工具调用失败的 id 去重集合:同一次失败只播一次沮丧表情 */\nconst reactedToolErrorIds = new Set<string>()\n\nconst toolErrorIds = computed(() => {\n const ids: string[] = []\n for (const msg of props.messages) {\n if (msg.role !== 'assistant' || !msg.toolCalls) continue\n for (const call of msg.toolCalls) {\n if (call.status === 'error') ids.push(call.id)\n }\n }\n return ids\n})\n\n// 工具调用失败 → 工作球闭眼摇头(frustrated)\nwatch(toolErrorIds, (ids) => {\n for (const id of ids) {\n if (reactedToolErrorIds.has(id)) continue\n reactedToolErrorIds.add(id)\n workingOrbRef.value?.react('frustrated')\n }\n})\n\n/** 工作球状态:收尾停留 → idle;正文输出中 → speaking;其余生成阶段 → thinking */\nconst workingOrbStatus = computed<AiOrbStatus>(() => {\n if (finishing.value) return 'idle'\n const last = visibleMessages.value[visibleMessages.value.length - 1]\n if (last?.role === 'assistant' && last.status === 'streaming' && last.content) {\n return 'speaking'\n }\n return 'thinking'\n})\n\nwatch(\n () => props.running,\n (running, prev) => {\n if (running) {\n // 工作开始:立即从输入框上方跳到列表,无停留\n clearJumpTimers()\n const idleWasVisible = !prev && !finishing.value && !workingLeaving.value\n finishing.value = false\n workingLeaving.value = false\n idleEnter.value = false\n if (idleWasVisible) {\n idleLeaving.value = true\n jumpTimer = setTimeout(() => {\n idleLeaving.value = false\n }, JUMP_MS)\n } else {\n idleLeaving.value = false\n }\n return\n }\n if (!prev) return\n\n // 清除会话后列表已空:跳过结束停留,立刻回到欢迎区\n if (visibleMessages.value.length === 0) {\n finishing.value = false\n workingLeaving.value = false\n idleLeaving.value = false\n idleEnter.value = false\n return\n }\n\n finishing.value = true\n idleLeaving.value = false\n const last = visibleMessages.value[visibleMessages.value.length - 1]\n let reaction: AiOrbReaction | undefined\n if (last?.role === 'assistant') {\n if (last.status === 'done') reaction = 'happy'\n else if (last.status === 'error') reaction = 'frustrated'\n }\n if (reaction) {\n nextTick(() => workingOrbRef.value?.react(reaction))\n }\n // 停留结束 → 列表中缩小、输入框上方放大\n finishTimer = setTimeout(() => {\n finishing.value = false\n workingLeaving.value = true\n idleEnter.value = true\n jumpTimer = setTimeout(() => {\n workingLeaving.value = false\n idleEnter.value = false\n }, JUMP_MS)\n }, WORKING_LINGER_MS)\n }\n)\n\n// 消息被清空(clear):立刻收掉工作球,欢迎区恢复\nwatch(\n () => visibleMessages.value.length,\n (len) => {\n if (len > 0) return\n clearJumpTimers()\n finishing.value = false\n workingLeaving.value = false\n idleLeaving.value = false\n idleEnter.value = false\n }\n)\n\n// 空闲欢迎区展示期间自动轮换;跳到工作位后停止\nwatch(\n () => [welcomeItems.value.length, showIdleWelcome.value] as const,\n () => {\n if (showIdleWelcome.value) startWelcomeRotation()\n else stopWelcomeRotation()\n },\n { immediate: true }\n)\n\nonBeforeUnmount(() => {\n stopWelcomeRotation()\n clearJumpTimers()\n})\n\nconst scrollRef = shallowRef<{ scrollTo: (position: ScrollPosition) => void }>()\n/** 是否吸附底部(用户上翻浏览历史时取消吸附) */\nconst stickToBottom = ref(true)\n/** 上一次滚动位置:用于区分用户上翻与自动吸底触发的 scroll 事件 */\nlet lastScrollY = 0\n\n/** 上翻阅读时展示回到底部的悬浮按钮 */\nconst showToLatest = computed(() => !stickToBottom.value && visibleMessages.value.length > 0)\n\nconst handleScroll = (position: Required<ScrollPosition>) => {\n const atBottom = position.sh - position.ch - position.y < 60\n if (atBottom) {\n stickToBottom.value = true\n } else if (position.y < lastScrollY) {\n // 只有「向上滚」才算用户离开底部的意图:自动吸底的 scroll 事件可能滞后于\n // 流式内容增长(sh 已变大、y 仍是旧底部),按位置判断会误判成未吸底\n stickToBottom.value = false\n }\n lastScrollY = position.y\n}\n\n/**\n * 用户向上滚动轮 = 明确的离开底部意图,立即取消吸附。\n * 不能等 scroll 事件再按方向判断:流式期间每个 chunk 都会排一次吸底滚动,\n * 滞后的 scrollTo 会把用户拉回底部并触发 atBottom 重新吸附,导致「上滚动不了」\n */\nconst handleWheel = (e: WheelEvent) => {\n if (e.deltaY < 0) stickToBottom.value = false\n}\n\n/** 点击悬浮按钮:恢复吸附并回到底部 */\nconst scrollToLatest = async () => {\n stickToBottom.value = true\n await nextTick()\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n}\n\n// 消息数量或流式内容变化时,若处于吸附状态则滚动到底部\nwatch(\n () => {\n const list = visibleMessages.value\n const last = list[list.length - 1]\n return [\n list.length,\n last?.content.length ?? 0,\n last?.reasoning?.length ?? 0,\n last?.toolCalls?.length ?? 0,\n props.running\n ].join('|')\n },\n async () => {\n // 用户刚发出消息时强制回到底部(无论之前是否在上翻)\n const list = visibleMessages.value\n if (list[list.length - 1]?.role === 'user') stickToBottom.value = true\n if (!stickToBottom.value) return\n await nextTick()\n // 等待 DOM 更新期间用户可能已上滚,滚动前重新确认吸附状态,避免把用户拉回底部\n if (!stickToBottom.value) return\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }\n)\n\nlet trailingScrollTimer: ReturnType<typeof setTimeout> | undefined\n\n// 生成收尾时动作区出现 / markdown 终态渲染 / 思考块折叠会再次改变高度,\n// 同帧滚动可能够不到最终底部,追加一次延迟兜底滚动\nwatch(\n () => props.running,\n async (running) => {\n if (running) return\n clearTimeout(trailingScrollTimer)\n if (!stickToBottom.value) return\n await nextTick()\n if (!stickToBottom.value) return\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n trailingScrollTimer = setTimeout(() => {\n if (stickToBottom.value) scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }, 150)\n }\n)\n\nonBeforeUnmount(() => clearTimeout(trailingScrollTimer))\n</script>\n"],"mappings":"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"}
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 :content-class=\"cls.e('list-content')\"\n ref=\"scrollRef\"\n @scroll=\"handleScroll\"\n @wheel.passive=\"handleWheel\"\n >\n <template v-for=\"turn in turns\" :key=\"turn.key\">\n <MessageItem\n v-if=\"turn.userMsg\"\n :message=\"turn.userMsg\"\n :is-last=\"false\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n\n <!-- 最终答案开始输出后,本轮之前的思考/工具调用过程收进「已完成」折叠块 -->\n <TurnProcess\n v-if=\"turn.processCollapsed\"\n :messages=\"turn.processMsgs\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n />\n <template v-else>\n <MessageItem\n v-for=\"msg in turn.processMsgs\"\n :key=\"msg.id\"\n :message=\"msg\"\n :is-last=\"false\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n </template>\n\n <MessageItem\n v-if=\"turn.answerMsg\"\n :message=\"turn.answerMsg\"\n :is-last=\"turn.isLast\"\n :duration=\"turnDurations.get(turn.key)\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n </template>\n\n <!-- 工作中:活体球从输入框上方跳到最新会话下面;结束停留后再跳回 -->\n <div v-if=\"showWorking\" :class=\"[cls.e('working'), bem.is('leaving', workingLeaving)]\">\n <UAiOrb ref=\"workingOrb\" :size=\"ORB_SIZE\" :status=\"workingOrbStatus\" />\n <template v-if=\"running\">\n <span :class=\"[cls.e('working-text'), 'u-shine']\">工作中…</span>\n <span :class=\"cls.e('working-time')\">{{ formatDuration(elapsedMs) }}</span>\n </template>\n <span v-else-if=\"lastTurnDuration != null\" :class=\"cls.e('working-time')\">\n 用时 {{ formatDuration(lastTurnDuration) }}\n </span>\n </div>\n </UScroll>\n\n <!-- 空闲:始终钉在滚动容器外、输入框上方;工作开始时缩小离开、结束后放大回来 -->\n <div\n v-if=\"showIdleWelcome\"\n :class=\"[cls.e('welcome'), bem.is('leaving', idleLeaving), bem.is('enter', idleEnter)]\"\n >\n <slot name=\"welcome\">\n <div :class=\"cls.e('welcome-inner')\">\n <UAiOrb :size=\"ORB_SIZE\" @click=\"cycleWelcome\" />\n <!-- 快捷提问逐条展示在球右侧并自动轮换:点文案发送,点球立即换下一条。\n 固定锚点布局:球位置钉死不动,气泡只向右延展,避免轮换时水平瞬移 -->\n <div v-if=\"welcomeItems.length\" :class=\"cls.e('welcome-item-wrap')\">\n <Transition :name=\"cls.em('welcome', 'swap')\" mode=\"out-in\">\n <button\n v-if=\"currentWelcomeItem\"\n :key=\"welcomeIndex\"\n type=\"button\"\n :class=\"cls.e('welcome-item')\"\n :disabled=\"readonly\"\n @click=\"!readonly && emit('welcome-click', currentWelcomeItem)\"\n >\n {{ currentWelcomeItem }}\n </button>\n </Transition>\n </div>\n </div>\n </slot>\n </div>\n\n <!-- 用户上翻浏览时悬浮入口:一键回到最新消息 -->\n <Transition :name=\"cls.em('to-latest', 'fade')\">\n <button\n v-if=\"showToLatest\"\n type=\"button\"\n :class=\"cls.e('to-latest')\"\n title=\"回到最新\"\n @click=\"scrollToLatest\"\n >\n <UIcon><ArrowDown /></UIcon>\n </button>\n </Transition>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon, UScroll } from '@veltra/desktop'\nimport type { ScrollPosition } from '@veltra/desktop'\nimport { ArrowDown } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport {\n computed,\n inject,\n nextTick,\n onBeforeUnmount,\n ref,\n shallowRef,\n useTemplateRef,\n watch\n} from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport type { AiOrbExposed, AiOrbReaction, AiOrbStatus } from '../../types/ai-orb'\nimport UAiOrb from '../ai-orb/ai-orb.vue'\nimport { AiChatDIKey } from './di'\nimport { formatDuration } from './format-duration'\nimport MessageItem from './message-item.vue'\nimport TurnProcess from './turn-process.vue'\n\ndefineOptions({ name: 'UAiChatMessageList' })\n\n/** 空闲欢迎 / 工作指示共用活体球直径(px) */\nconst ORB_SIZE = 48\n/** 生成结束 / 失败后工作球停留再跳回输入框上方 */\nconst WORKING_LINGER_MS = 2500\n/** 两处活体球对向缩放的时长(与样式中的 jump 过渡一致) */\nconst JUMP_MS = 380\n\nconst props = defineProps<{\n messages: ChatMessage[]\n /** 空闲时输入框上方的快捷提问;字符串按单项处理 */\n welcome?: string | string[]\n /** 是否生成中(控制底部工作状态指示) */\n running?: boolean\n /** 只读时欢迎语不可点击发送 */\n readonly?: boolean\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{\n (e: 'respond', toolCallId: string, approved: boolean): void\n (e: 'regenerate'): void\n /** 点击欢迎项(快捷提问) */\n (e: 'welcome-click', text: string): void\n}>()\n\ndefineSlots<{\n /** 空闲欢迎区插槽(输入框上方;工作中跳到列表末尾) */\n welcome(): any\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** tool 消息不单独渲染,其内容体现在工具卡片的结果区 */\nconst visibleMessages = computed(() => props.messages.filter((msg) => msg.role !== 'tool'))\n\n/** 一轮对话:以 user 消息为界,含本轮全部 assistant 消息(每次工具循环产生一条) */\ninterface ChatTurn {\n /** 轮次 key(取用户消息或首条 assistant 消息 id) */\n key: string\n userMsg?: ChatMessage\n /** 最终答案之前的过程消息(思考 + 中间文本 + 工具卡片) */\n processMsgs: ChatMessage[]\n /** 本轮最终答案(最后一条 assistant 消息) */\n answerMsg?: ChatMessage\n /** 过程消息是否收进「已完成」折叠块 */\n processCollapsed: boolean\n /** answerMsg 是否为全局最后一条可见消息 */\n isLast: boolean\n}\n\nconst isFinalStatus = (msg?: ChatMessage) =>\n msg?.status === 'done' || msg?.status === 'error' || msg?.status === 'aborted'\n\nconst turns = computed<ChatTurn[]>(() => {\n const grouped: { key: string; userMsg?: ChatMessage; assistants: ChatMessage[] }[] = []\n for (const msg of visibleMessages.value) {\n if (msg.role === 'user') {\n grouped.push({ key: msg.id, userMsg: msg, assistants: [] })\n continue\n }\n const last = grouped[grouped.length - 1]\n // 兼容 assistant 开头的历史会话:归入无用户消息的轮次\n if (last) last.assistants.push(msg)\n else grouped.push({ key: msg.id, assistants: [msg] })\n }\n return grouped.map((turn, index) => {\n const processMsgs = turn.assistants.slice(0, -1)\n const answerMsg = turn.assistants[turn.assistants.length - 1]\n const isLastTurn = index === grouped.length - 1\n // 该轮结束(非最后一轮 / 答案进入终态)或最终答案开始输出时折叠过程。\n // content 只增不减,折叠后不会回弹;regenerate 删除本轮 assistant 重跑,条件自然复位\n const processCollapsed =\n processMsgs.length > 0 &&\n (!isLastTurn || !!answerMsg?.content || (!props.running && isFinalStatus(answerMsg)))\n return {\n key: turn.key,\n userMsg: turn.userMsg,\n processMsgs,\n answerMsg,\n processCollapsed,\n isLast: isLastTurn && !!answerMsg\n }\n })\n})\n\nconst welcomeItems = computed(() => {\n const welcome = props.welcome\n if (!welcome) return []\n return (Array.isArray(welcome) ? welcome : [welcome]).filter(Boolean)\n})\n\n/** 欢迎语逐条轮换:当前展示项的下标 */\nconst welcomeIndex = ref(0)\n\n/** 当前展示的欢迎语 */\nconst currentWelcomeItem = computed(() => {\n const items = welcomeItems.value\n return items.length ? items[welcomeIndex.value % items.length] : undefined\n})\n\nlet welcomeTimer: ReturnType<typeof setInterval> | undefined\n\nconst stopWelcomeRotation = () => {\n clearInterval(welcomeTimer)\n welcomeTimer = undefined\n}\n\nconst startWelcomeRotation = () => {\n stopWelcomeRotation()\n if (welcomeItems.value.length > 1) {\n welcomeTimer = setInterval(() => {\n welcomeIndex.value += 1\n }, 4000)\n }\n}\n\n/** 点击欢迎球:不等自动轮换,立即换下一条,并重置计时 */\nconst cycleWelcome = () => {\n if (welcomeItems.value.length <= 1) return\n welcomeIndex.value += 1\n startWelcomeRotation()\n}\n\n/** 回答完毕 / 失败后工作球短暂停留,播完表情再跳回输入框上方 */\nconst finishing = ref(false)\n/** 列表中的球正在缩小离开 */\nconst workingLeaving = ref(false)\n/** 输入框上方的球正在缩小离开 */\nconst idleLeaving = ref(false)\n/** 输入框上方的球从工作位放大回来(首屏出现不播) */\nconst idleEnter = ref(false)\n\nlet finishTimer: ReturnType<typeof setTimeout> | undefined\nlet jumpTimer: ReturnType<typeof setTimeout> | undefined\n\nconst clearJumpTimers = () => {\n clearTimeout(finishTimer)\n clearTimeout(jumpTimer)\n}\n\n/** 工作计时的起算时间戳(running 升起时记录) */\nlet workStartAt = 0\n/** 进行中的已逝时长(100ms 跳动刷新) */\nconst elapsedMs = ref(0)\nlet tickTimer: ReturnType<typeof setInterval> | undefined\n\n/** 各轮次总用时(key 同 turns;仅 live 轮次有,历史回放无计时数据) */\nconst turnDurations = shallowRef(new Map<string, number>())\n\nconst stopTick = () => {\n clearInterval(tickTimer)\n tickTimer = undefined\n}\n\n/** 收尾停留期间展示的末轮总用时 */\nconst lastTurnDuration = computed(() => {\n const last = turns.value[turns.value.length - 1]\n return last ? turnDurations.value.get(last.key) : undefined\n})\n\n/** 列表末尾的工作球:生成中 / 收尾停留 / 跳回过渡 */\nconst showWorking = computed(() => !!(props.running || finishing.value || workingLeaving.value))\n/** 输入框上方的空闲欢迎:含工作开始时的缩小离开帧 */\nconst showIdleWelcome = computed(() => (!props.running && !finishing.value) || idleLeaving.value)\n\nconst workingOrbRef = useTemplateRef<AiOrbExposed>('workingOrb')\n\n/** 工具调用失败的 id 去重集合:同一次失败只播一次沮丧表情 */\nconst reactedToolErrorIds = new Set<string>()\n\nconst toolErrorIds = computed(() => {\n const ids: string[] = []\n for (const msg of props.messages) {\n if (msg.role !== 'assistant' || !msg.toolCalls) continue\n for (const call of msg.toolCalls) {\n if (call.status === 'error') ids.push(call.id)\n }\n }\n return ids\n})\n\n// 工具调用失败 → 工作球闭眼摇头(frustrated)\nwatch(toolErrorIds, (ids) => {\n for (const id of ids) {\n if (reactedToolErrorIds.has(id)) continue\n reactedToolErrorIds.add(id)\n workingOrbRef.value?.react('frustrated')\n }\n})\n\n/** 工作球状态:收尾停留 → idle;正文输出中 → speaking;其余生成阶段 → thinking */\nconst workingOrbStatus = computed<AiOrbStatus>(() => {\n if (finishing.value) return 'idle'\n const last = visibleMessages.value[visibleMessages.value.length - 1]\n if (last?.role === 'assistant' && last.status === 'streaming' && last.content) {\n return 'speaking'\n }\n return 'thinking'\n})\n\nwatch(\n () => props.running,\n (running, prev) => {\n if (running) {\n // 工作开始:立即从输入框上方跳到列表,无停留\n clearJumpTimers()\n // 工作计时开始:100ms 跳动刷新已逝时长\n workStartAt = Date.now()\n elapsedMs.value = 0\n stopTick()\n tickTimer = setInterval(() => {\n elapsedMs.value = Date.now() - workStartAt\n }, 100)\n const idleWasVisible = !prev && !finishing.value && !workingLeaving.value\n finishing.value = false\n workingLeaving.value = false\n idleEnter.value = false\n if (idleWasVisible) {\n idleLeaving.value = true\n jumpTimer = setTimeout(() => {\n idleLeaving.value = false\n }, JUMP_MS)\n } else {\n idleLeaving.value = false\n }\n return\n }\n if (!prev) return\n\n stopTick()\n // 记录本轮总用时(key 同 turns:regenerate 同 key 覆盖,队列接续各自计时)\n const lastTurn = turns.value[turns.value.length - 1]\n if (lastTurn) {\n const next = new Map(turnDurations.value)\n next.set(lastTurn.key, Date.now() - workStartAt)\n turnDurations.value = next\n }\n\n // 清除会话后列表已空:跳过结束停留,立刻回到欢迎区\n if (visibleMessages.value.length === 0) {\n finishing.value = false\n workingLeaving.value = false\n idleLeaving.value = false\n idleEnter.value = false\n return\n }\n\n finishing.value = true\n idleLeaving.value = false\n const last = visibleMessages.value[visibleMessages.value.length - 1]\n let reaction: AiOrbReaction | undefined\n if (last?.role === 'assistant') {\n if (last.status === 'done') reaction = 'happy'\n else if (last.status === 'error') reaction = 'frustrated'\n }\n if (reaction) {\n nextTick(() => workingOrbRef.value?.react(reaction))\n }\n // 停留结束 → 列表中缩小、输入框上方放大\n finishTimer = setTimeout(() => {\n finishing.value = false\n workingLeaving.value = true\n idleEnter.value = true\n jumpTimer = setTimeout(() => {\n workingLeaving.value = false\n idleEnter.value = false\n }, JUMP_MS)\n }, WORKING_LINGER_MS)\n }\n)\n\n// 消息被清空(clear):立刻收掉工作球,欢迎区恢复\nwatch(\n () => visibleMessages.value.length,\n (len) => {\n if (len > 0) return\n clearJumpTimers()\n turnDurations.value = new Map()\n finishing.value = false\n workingLeaving.value = false\n idleLeaving.value = false\n idleEnter.value = false\n }\n)\n\n// 空闲欢迎区展示期间自动轮换;跳到工作位后停止\nwatch(\n () => [welcomeItems.value.length, showIdleWelcome.value] as const,\n () => {\n if (showIdleWelcome.value) startWelcomeRotation()\n else stopWelcomeRotation()\n },\n { immediate: true }\n)\n\nonBeforeUnmount(() => {\n stopWelcomeRotation()\n clearJumpTimers()\n stopTick()\n})\n\nconst scrollRef = shallowRef<{ scrollTo: (position: ScrollPosition) => void }>()\n/** 是否吸附底部(用户上翻浏览历史时取消吸附) */\nconst stickToBottom = ref(true)\n/** 上一次滚动位置:用于区分用户上翻与自动吸底触发的 scroll 事件 */\nlet lastScrollY = 0\n\n/** 上翻阅读时展示回到底部的悬浮按钮 */\nconst showToLatest = computed(() => !stickToBottom.value && visibleMessages.value.length > 0)\n\nconst handleScroll = (position: Required<ScrollPosition>) => {\n const atBottom = position.sh - position.ch - position.y < 60\n if (atBottom) {\n stickToBottom.value = true\n } else if (position.y < lastScrollY) {\n // 只有「向上滚」才算用户离开底部的意图:自动吸底的 scroll 事件可能滞后于\n // 流式内容增长(sh 已变大、y 仍是旧底部),按位置判断会误判成未吸底\n stickToBottom.value = false\n }\n lastScrollY = position.y\n}\n\n/**\n * 用户向上滚动轮 = 明确的离开底部意图,立即取消吸附。\n * 不能等 scroll 事件再按方向判断:流式期间每个 chunk 都会排一次吸底滚动,\n * 滞后的 scrollTo 会把用户拉回底部并触发 atBottom 重新吸附,导致「上滚动不了」\n */\nconst handleWheel = (e: WheelEvent) => {\n if (e.deltaY < 0) stickToBottom.value = false\n}\n\n/** 点击悬浮按钮:恢复吸附并回到底部 */\nconst scrollToLatest = async () => {\n stickToBottom.value = true\n await nextTick()\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n}\n\n// 消息数量或流式内容变化时,若处于吸附状态则滚动到底部\nwatch(\n () => {\n const list = visibleMessages.value\n const last = list[list.length - 1]\n return [\n list.length,\n last?.content.length ?? 0,\n last?.reasoning?.length ?? 0,\n last?.toolCalls?.length ?? 0,\n props.running\n ].join('|')\n },\n async () => {\n // 用户刚发出消息时强制回到底部(无论之前是否在上翻)\n const list = visibleMessages.value\n if (list[list.length - 1]?.role === 'user') stickToBottom.value = true\n if (!stickToBottom.value) return\n await nextTick()\n // 等待 DOM 更新期间用户可能已上滚,滚动前重新确认吸附状态,避免把用户拉回底部\n if (!stickToBottom.value) return\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }\n)\n\nlet trailingScrollTimer: ReturnType<typeof setTimeout> | undefined\n\n// 生成收尾时动作区出现 / markdown 终态渲染 / 思考块折叠会再次改变高度,\n// 同帧滚动可能够不到最终底部,追加一次延迟兜底滚动\nwatch(\n () => props.running,\n async (running) => {\n if (running) return\n clearTimeout(trailingScrollTimer)\n if (!stickToBottom.value) return\n await nextTick()\n if (!stickToBottom.value) return\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n trailingScrollTimer = setTimeout(() => {\n if (stickToBottom.value) scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }, 150)\n }\n)\n\nonBeforeUnmount(() => clearTimeout(trailingScrollTimer))\n</script>\n"],"mappings":";;;;;;;;;;;AAoIA,MAAM,WAAW;;AAEjB,MAAM,oBAAoB;;AAE1B,MAAM,UAAU;;;;;;;;;;;;;;;;;;EAEhB,MAAM,QAAQ;EAYd,MAAM,OAAO;EAab,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;;EAGpC,MAAM,kBAAkB,eAAe,MAAM,SAAS,QAAQ,QAAQ,IAAI,SAAS,MAAM,CAAC;;EAiB1F,MAAM,iBAAiB,QACrB,KAAK,WAAW,UAAU,KAAK,WAAW,WAAW,KAAK,WAAW;EAEvE,MAAM,QAAQ,eAA2B;GACvC,MAAM,UAA+E,CAAC;GACtF,KAAK,MAAM,OAAO,gBAAgB,OAAO;IACvC,IAAI,IAAI,SAAS,QAAQ;KACvB,QAAQ,KAAK;MAAE,KAAK,IAAI;MAAI,SAAS;MAAK,YAAY,CAAC;KAAE,CAAC;KAC1D;IACF;IACA,MAAM,OAAO,QAAQ,QAAQ,SAAS;IAEtC,IAAI,MAAM,KAAK,WAAW,KAAK,GAAG;SAC7B,QAAQ,KAAK;KAAE,KAAK,IAAI;KAAI,YAAY,CAAC,GAAG;IAAE,CAAC;GACtD;GACA,OAAO,QAAQ,KAAK,MAAM,UAAU;IAClC,MAAM,cAAc,KAAK,WAAW,MAAM,GAAG,EAAE;IAC/C,MAAM,YAAY,KAAK,WAAW,KAAK,WAAW,SAAS;IAC3D,MAAM,aAAa,UAAU,QAAQ,SAAS;IAG9C,MAAM,mBACJ,YAAY,SAAS,MACpB,CAAC,cAAc,CAAC,CAAC,WAAW,WAAY,CAAC,MAAM,WAAW,cAAc,SAAS;IACpF,OAAO;KACL,KAAK,KAAK;KACV,SAAS,KAAK;KACd;KACA;KACA;KACA,QAAQ,cAAc,CAAC,CAAC;IAC1B;GACF,CAAC;EACH,CAAC;EAED,MAAM,eAAe,eAAe;GAClC,MAAM,UAAU,MAAM;GACtB,IAAI,CAAC,SAAS,OAAO,CAAC;GACtB,QAAQ,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO,EAAA,CAAG,OAAO,OAAO;EACtE,CAAC;;EAGD,MAAM,eAAe,IAAI,CAAC;;EAG1B,MAAM,qBAAqB,eAAe;GACxC,MAAM,QAAQ,aAAa;GAC3B,OAAO,MAAM,SAAS,MAAM,aAAa,QAAQ,MAAM,UAAU,KAAA;EACnE,CAAC;EAED,IAAI;EAEJ,MAAM,4BAA4B;GAChC,cAAc,YAAY;GAC1B,eAAe,KAAA;EACjB;EAEA,MAAM,6BAA6B;GACjC,oBAAoB;GACpB,IAAI,aAAa,MAAM,SAAS,GAC9B,eAAe,kBAAkB;IAC/B,aAAa,SAAS;GACxB,GAAG,GAAI;EAEX;;EAGA,MAAM,qBAAqB;GACzB,IAAI,aAAa,MAAM,UAAU,GAAG;GACpC,aAAa,SAAS;GACtB,qBAAqB;EACvB;;EAGA,MAAM,YAAY,IAAI,KAAK;;EAE3B,MAAM,iBAAiB,IAAI,KAAK;;EAEhC,MAAM,cAAc,IAAI,KAAK;;EAE7B,MAAM,YAAY,IAAI,KAAK;EAE3B,IAAI;EACJ,IAAI;EAEJ,MAAM,wBAAwB;GAC5B,aAAa,WAAW;GACxB,aAAa,SAAS;EACxB;;EAGA,IAAI,cAAc;;EAElB,MAAM,YAAY,IAAI,CAAC;EACvB,IAAI;;EAGJ,MAAM,gBAAgB,2BAAW,IAAI,IAAoB,CAAC;EAE1D,MAAM,iBAAiB;GACrB,cAAc,SAAS;GACvB,YAAY,KAAA;EACd;;EAGA,MAAM,mBAAmB,eAAe;GACtC,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM,SAAS;GAC9C,OAAO,OAAO,cAAc,MAAM,IAAI,KAAK,GAAG,IAAI,KAAA;EACpD,CAAC;;EAGD,MAAM,cAAc,eAAe,CAAC,EAAE,MAAM,WAAW,UAAU,SAAS,eAAe,MAAM;;EAE/F,MAAM,kBAAkB,eAAgB,CAAC,MAAM,WAAW,CAAC,UAAU,SAAU,YAAY,KAAK;EAEhG,MAAM,gBAAgB,eAA6B,YAAY;;EAG/D,MAAM,sCAAsB,IAAI,IAAY;EAE5C,MAAM,eAAe,eAAe;GAClC,MAAM,MAAgB,CAAC;GACvB,KAAK,MAAM,OAAO,MAAM,UAAU;IAChC,IAAI,IAAI,SAAS,eAAe,CAAC,IAAI,WAAW;IAChD,KAAK,MAAM,QAAQ,IAAI,WACrB,IAAI,KAAK,WAAW,SAAS,IAAI,KAAK,KAAK,EAAE;GAEjD;GACA,OAAO;EACT,CAAC;EAGD,MAAM,eAAe,QAAQ;GAC3B,KAAK,MAAM,MAAM,KAAK;IACpB,IAAI,oBAAoB,IAAI,EAAE,GAAG;IACjC,oBAAoB,IAAI,EAAE;IAC1B,cAAc,OAAO,MAAM,YAAY;GACzC;EACF,CAAC;;EAGD,MAAM,mBAAmB,eAA4B;GACnD,IAAI,UAAU,OAAO,OAAO;GAC5B,MAAM,OAAO,gBAAgB,MAAM,gBAAgB,MAAM,SAAS;GAClE,IAAI,MAAM,SAAS,eAAe,KAAK,WAAW,eAAe,KAAK,SACpE,OAAO;GAET,OAAO;EACT,CAAC;EAED,YACQ,MAAM,UACX,SAAS,SAAS;GACjB,IAAI,SAAS;IAEX,gBAAgB;IAEhB,cAAc,KAAK,IAAI;IACvB,UAAU,QAAQ;IAClB,SAAS;IACT,YAAY,kBAAkB;KAC5B,UAAU,QAAQ,KAAK,IAAI,IAAI;IACjC,GAAG,GAAG;IACN,MAAM,iBAAiB,CAAC,QAAQ,CAAC,UAAU,SAAS,CAAC,eAAe;IACpE,UAAU,QAAQ;IAClB,eAAe,QAAQ;IACvB,UAAU,QAAQ;IAClB,IAAI,gBAAgB;KAClB,YAAY,QAAQ;KACpB,YAAY,iBAAiB;MAC3B,YAAY,QAAQ;KACtB,GAAG,OAAO;IACZ,OACE,YAAY,QAAQ;IAEtB;GACF;GACA,IAAI,CAAC,MAAM;GAEX,SAAS;GAET,MAAM,WAAW,MAAM,MAAM,MAAM,MAAM,SAAS;GAClD,IAAI,UAAU;IACZ,MAAM,OAAO,IAAI,IAAI,cAAc,KAAK;IACxC,KAAK,IAAI,SAAS,KAAK,KAAK,IAAI,IAAI,WAAW;IAC/C,cAAc,QAAQ;GACxB;GAGA,IAAI,gBAAgB,MAAM,WAAW,GAAG;IACtC,UAAU,QAAQ;IAClB,eAAe,QAAQ;IACvB,YAAY,QAAQ;IACpB,UAAU,QAAQ;IAClB;GACF;GAEA,UAAU,QAAQ;GAClB,YAAY,QAAQ;GACpB,MAAM,OAAO,gBAAgB,MAAM,gBAAgB,MAAM,SAAS;GAClE,IAAI;GACJ,IAAI,MAAM,SAAS,aAAa;IAC9B,IAAI,KAAK,WAAW,QAAQ,WAAW;SAClC,IAAI,KAAK,WAAW,SAAS,WAAW;GAC/C;GACA,IAAI,UACF,eAAe,cAAc,OAAO,MAAM,QAAQ,CAAC;GAGrD,cAAc,iBAAiB;IAC7B,UAAU,QAAQ;IAClB,eAAe,QAAQ;IACvB,UAAU,QAAQ;IAClB,YAAY,iBAAiB;KAC3B,eAAe,QAAQ;KACvB,UAAU,QAAQ;IACpB,GAAG,OAAO;GACZ,GAAG,iBAAiB;EACtB,CACF;EAGA,YACQ,gBAAgB,MAAM,SAC3B,QAAQ;GACP,IAAI,MAAM,GAAG;GACb,gBAAgB;GAChB,cAAc,wBAAQ,IAAI,IAAI;GAC9B,UAAU,QAAQ;GAClB,eAAe,QAAQ;GACvB,YAAY,QAAQ;GACpB,UAAU,QAAQ;EACpB,CACF;EAGA,YACQ,CAAC,aAAa,MAAM,QAAQ,gBAAgB,KAAK,SACjD;GACJ,IAAI,gBAAgB,OAAO,qBAAqB;QAC3C,oBAAoB;EAC3B,GACA,EAAE,WAAW,KAAK,CACpB;EAEA,sBAAsB;GACpB,oBAAoB;GACpB,gBAAgB;GAChB,SAAS;EACX,CAAC;EAED,MAAM,YAAY,WAA6D;;EAE/E,MAAM,gBAAgB,IAAI,IAAI;;EAE9B,IAAI,cAAc;;EAGlB,MAAM,eAAe,eAAe,CAAC,cAAc,SAAS,gBAAgB,MAAM,SAAS,CAAC;EAE5F,MAAM,gBAAgB,aAAuC;GAE3D,IADiB,SAAS,KAAK,SAAS,KAAK,SAAS,IAAI,IAExD,cAAc,QAAQ;QACjB,IAAI,SAAS,IAAI,aAGtB,cAAc,QAAQ;GAExB,cAAc,SAAS;EACzB;;;;;;EAOA,MAAM,eAAe,MAAkB;GACrC,IAAI,EAAE,SAAS,GAAG,cAAc,QAAQ;EAC1C;;EAGA,MAAM,iBAAiB,YAAY;GACjC,cAAc,QAAQ;GACtB,MAAM,SAAS;GACf,UAAU,OAAO,SAAS,EAAE,GAAG,OAAO,iBAAiB,CAAC;EAC1D;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;IAC3B,MAAM;GACR,CAAC,CAAC,KAAK,GAAG;EACZ,GACA,YAAY;GAEV,MAAM,OAAO,gBAAgB;GAC7B,IAAI,KAAK,KAAK,SAAS,EAAE,EAAE,SAAS,QAAQ,cAAc,QAAQ;GAClE,IAAI,CAAC,cAAc,OAAO;GAC1B,MAAM,SAAS;GAEf,IAAI,CAAC,cAAc,OAAO;GAC1B,UAAU,OAAO,SAAS,EAAE,GAAG,OAAO,iBAAiB,CAAC;EAC1D,CACF;EAEA,IAAI;EAIJ,YACQ,MAAM,SACZ,OAAO,YAAY;GACjB,IAAI,SAAS;GACb,aAAa,mBAAmB;GAChC,IAAI,CAAC,cAAc,OAAO;GAC1B,MAAM,SAAS;GACf,IAAI,CAAC,cAAc,OAAO;GAC1B,UAAU,OAAO,SAAS,EAAE,GAAG,OAAO,iBAAiB,CAAC;GACxD,sBAAsB,iBAAiB;IACrC,IAAI,cAAc,OAAO,UAAU,OAAO,SAAS,EAAE,GAAG,OAAO,iBAAiB,CAAC;GACnF,GAAG,GAAG;EACR,CACF;EAEA,sBAAsB,aAAa,mBAAmB,CAAC;;GAlgBrD,OAAA,UAAA,GAAA,mBAqGM,OAAA,EArGA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,WAAA,CAAA,EAAA,GAAA;IAChB,YA0DU,MAAA,OAAA,GAAA;KAzDP,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,MAAA,CAAA;KACZ,iBAAe,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA;KACjB,SAAA;KAAJ,KAAI;KACH,UAAQ;KACO,gBAAA;;KAEN,SAAA,cAAqB;OAA/B,UAAA,IAAA,GAAA,mBAsCW,UAAA,MAAA,WAtCc,MAAA,QAAR,SAAI;OAAiB,OAAA,UAAA,GAAA,mBAAA,UAAA,EAAA,KAAA,KAAK,IAAA,GAAA;QAEjC,KAAK,WADb,UAAA,GAAA,YAOE,aAAA;;SALC,SAAS,KAAK;SACd,WAAS;SACT,kBAAgB,QAAA;SAChB,WAAO,OAAA,OAAA,OAAA,MAAG,IAAI,aAAa,KAAI,WAAY,IAAI,QAAQ;SACvD,cAAU,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,YAAA;;QAGnB,mBAAA,sCAAA;QAEQ,KAAK,oBADb,UAAA,GAAA,YAKE,aAAA;;SAHC,UAAU,KAAK;SACf,kBAAgB,QAAA;SAChB,WAAO,OAAA,OAAA,OAAA,MAAG,IAAI,aAAa,KAAI,WAAY,IAAI,QAAQ;QAGxD,GAAA,MAAA,GAAA,CAAA,YAAA,gBAAA,CAAA,MAAA,UAAA,IAAA,GAAA,mBAQE,UAAA,EAAA,KAAA,EAAA,GAAA,WAPc,KAAK,cAAZ,QAAG;SADZ,OAAA,UAAA,GAAA,YAQE,aAAA;UANC,KAAK,IAAI;UACT,SAAS;UACT,WAAS;UACT,kBAAgB,QAAA;UAChB,WAAO,OAAA,OAAA,OAAA,MAAG,IAAI,aAAa,KAAI,WAAY,IAAI,QAAQ;UACvD,cAAU,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,YAAA;;;QAKb,KAAK,aADb,UAAA,GAAA,YAQE,aAAA;;SANC,SAAS,KAAK;SACd,WAAS,KAAK;SACd,UAAU,cAAA,MAAc,IAAI,KAAK,GAAG;SACpC,kBAAgB,QAAA;SAChB,WAAO,OAAA,OAAA,OAAA,MAAG,IAAI,aAAa,KAAI,WAAY,IAAI,QAAQ;SACvD,cAAU,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,YAAA;;;;;;;;;MAIrB,mBAAA,kCAAA;MACW,YAAA,SAAX,UAAA,GAAA,mBASM,OAAA;;OATmB,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,SAAA,GAAa,MAAA,GAAA,CAAG,CAAC,GAAE,WAAY,eAAA,KAAc,CAAA,CAAA;MACjF,GAAA,CAAA,YAAuE,aAAA;OAA/D,KAAI;OAAc,MAAM;OAAW,QAAQ,iBAAA;MACnC,GAAA,MAAA,GAAA,CAAA,QAAA,CAAA,GAAA,QAAA,WAAhB,UAAA,GAAA,mBAGW,UAAA,EAAA,KAAA,EAAA,GAAA,CAFT,mBAA6D,QAAA,EAAtD,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,GAAA,SAAA,CAAA,EAA8B,GAAA,QAAI,CAAA,GACtD,mBAA2E,QAAA,EAApE,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA,EAAqB,GAAA,gBAAA,MAAA,cAAA,CAAc,CAAC,UAAA,KAAS,CAAA,GAAA,CAAA,CAAA,GAAA,EAEjD,KAAA,iBAAA,SAAgB,QAAjC,UAAA,GAAA,mBAEO,QAAA;;OAFqC,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;MAAkB,GAAA,SACrE,gBAAG,MAAA,cAAA,CAAc,CAAC,iBAAA,KAAgB,CAAA,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA;;;;IAK3C,mBAAA,wCAAA;IAEQ,gBAAA,SADR,UAAA,GAAA,mBAyBM,OAAA;;KAvBH,OAAK,eAAA;MAAG,MAAA,GAAA,CAAG,CAAC,EAAC,SAAA;MAAa,MAAA,GAAA,CAAG,CAAC,GAAE,WAAY,YAAA,KAAW;MAAG,MAAA,GAAA,CAAG,CAAC,GAAE,SAAU,UAAA,KAAS;KAAA,CAAA;IAEpF,GAAA,CAAA,WAoBO,KAAA,QAAA,WAAA,CAAA,SAAA,CAnBL,mBAkBM,OAAA,EAlBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,eAAA,CAAA,EAAA,GAAA;KAChB,YAAiD,aAAA;MAAxC,MAAM;MAAW,SAAO;;KACjC,mBAAA,sFAAA;KAEW,aAAA,MAAa,UAAxB,UAAA,GAAA,mBAaM,OAAA;;MAb2B,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA;KAC3C,GAAA,CAAA,YAWa,YAAA;MAXA,MAAM,MAAA,GAAA,CAAG,CAAC,GAAE,WAAA,MAAA;MAAqB,MAAK;;MACjD,SAAA,cASS,CARD,mBAAA,SADR,UAAA,GAAA,mBASS,UAAA;OAPN,KAAK,aAAA;OACN,MAAK;OACJ,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;OACZ,UAAU,QAAA;OACV,SAAK,OAAA,OAAA,OAAA,MAAA,WAAA,CAAG,QAAA,YAAY,KAAI,iBAAkB,mBAAA,KAAkB;MAE1D,GAAA,gBAAA,mBAAA,KAAkB,GAAA,IAAA,UAAA,KAAA,mBAAA,QAAA,IAAA,CAAA,CAAA;;;;IAQjC,mBAAA,wBAAA;IACA,YAUa,YAAA,EAVA,MAAM,MAAA,GAAA,CAAG,CAAC,GAAE,aAAA,MAAA,EAAA,GAAA;KACvB,SAAA,cAQS,CAPD,aAAA,SADR,UAAA,GAAA,mBAQS,UAAA;;MANP,MAAK;MACJ,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,WAAA,CAAA;MACb,OAAM;MACL,SAAO;KAER,GAAA,CAAA,YAA4B,MAAA,KAAA,GAAA,MAAA;MAArB,SAAA,cAAa,CAAb,YAAa,MAAA,SAAA,CAAA,CAAA,CAAA"}