@veltra/ai 2.1.1 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/transports/openai.js +18 -17
- package/dist/chat/transports/openai.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js +1 -1
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/message-item.js +2 -7
- package/dist/components/ai-chat/message-item.js.map +1 -1
- package/dist/components/ai-chat/message-list.js +164 -65
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/style2.css +111 -31
- package/dist/components/ai-chat/tool-call.js +2 -6
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/components/ai-chat/turn-process.js +56 -0
- package/dist/components/ai-chat/turn-process.js.map +1 -0
- package/dist/components/ai-orb/orb-renderer.js +88 -8
- package/dist/components/ai-orb/orb-renderer.js.map +1 -1
- package/dist/types/ai-chat.d.ts +1 -1
- package/package.json +15 -6
- package/src/components/ai-chat/__test__/ai-chat.test.ts +136 -35
- package/src/components/ai-chat/ai-chat.vue +2 -2
- package/src/components/ai-chat/message-item.vue +3 -3
- package/src/components/ai-chat/message-list.vue +186 -66
- package/src/components/ai-chat/style.scss +149 -41
- package/src/components/ai-chat/tool-call.vue +1 -10
- package/src/components/ai-chat/turn-process.vue +47 -0
- package/src/components/ai-orb/__test__/ai-orb.test.ts +28 -3
- package/src/components/ai-orb/orb-renderer.ts +113 -5
- package/src/tools/ask-question/__test__/ask-question.test.ts +13 -3
- package/src/types/ai-chat.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-list.js","names":[],"sources":["../../../src/components/ai-chat/message-list.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('list-wrap')\">\n <UScroll\n :class=\"cls.e('list')\"\n ref=\"scrollRef\"\n always\n @scroll=\"handleScroll\"\n @wheel.passive=\"handleWheel\"\n >\n <div v-if=\"!visibleMessages.length\" :class=\"cls.e('welcome')\">\n <slot name=\"welcome\">\n <div :class=\"cls.e('welcome-inner')\">\n <UAiOrb :size=\"WELCOME_ORB_SIZE\" @click=\"cycleWelcome\" />\n <!-- 快捷提问逐条展示在球右侧并自动轮换:点文案发送,点球立即换下一条。\n 固定锚点布局:球位置钉死不动,气泡只向右延展,避免轮换时水平瞬移 -->\n <div v-if=\"welcomeItems.length\" :class=\"cls.e('welcome-item-wrap')\">\n <Transition :name=\"cls.em('welcome', 'swap')\" mode=\"out-in\">\n <button\n v-if=\"currentWelcomeItem\"\n :key=\"welcomeIndex\"\n type=\"button\"\n :class=\"cls.e('welcome-item')\"\n @click=\"emit('welcome-click', currentWelcomeItem)\"\n >\n {{ currentWelcomeItem }}\n </button>\n </Transition>\n </div>\n </div>\n </slot>\n </div>\n\n <MessageItem\n v-for=\"(msg, index) in visibleMessages\"\n :key=\"msg.id\"\n :message=\"msg\"\n :is-last=\"index === visibleMessages.length - 1\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n\n <!-- 工作状态统一指示:生成中在列表末尾展示活体球 + 文案;结束 / 失败停留片刻再隐藏 -->\n <div v-if=\"running || finishing\" :class=\"cls.e('working')\">\n <UAiOrb ref=\"workingOrb\" :size=\"WORKING_ORB_SIZE\" :status=\"workingOrbStatus\" />\n <span v-if=\"running\" :class=\"[cls.e('working-text'), 'u-shine']\">工作中…</span>\n </div>\n </UScroll>\n\n <!-- 用户上翻浏览时悬浮入口:一键回到最新消息 -->\n <Transition :name=\"cls.em('to-latest', 'fade')\">\n <button v-if=\"showToLatest\" type=\"button\" :class=\"cls.e('to-latest')\" @click=\"scrollToLatest\">\n <UIcon><ArrowDown /></UIcon>\n <span>最新消息</span>\n </button>\n </Transition>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon, UScroll } from '@veltra/desktop'\nimport type { ScrollPosition } from '@veltra/desktop'\nimport { ArrowDown } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport {\n computed,\n inject,\n nextTick,\n onBeforeUnmount,\n ref,\n shallowRef,\n useTemplateRef,\n watch\n} from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport type { AiOrbExposed, AiOrbReaction, AiOrbStatus } from '../../types/ai-orb'\nimport UAiOrb from '../ai-orb/ai-orb.vue'\nimport { AiChatDIKey } from './di'\nimport MessageItem from './message-item.vue'\n\ndefineOptions({ name: 'UAiChatMessageList' })\n\n/** 欢迎区活体球直径(px) */\nconst WELCOME_ORB_SIZE = 64\n/** 工作指示活体球直径(px) */\nconst WORKING_ORB_SIZE = 48\n/** 生成结束 / 失败后工作球停留再隐藏 */\nconst WORKING_LINGER_MS = 2500\n\nconst props = defineProps<{\n messages: ChatMessage[]\n /** 空状态欢迎项;字符串按单项处理 */\n welcome?: string | string[]\n /** 是否生成中(控制底部工作状态指示) */\n running?: boolean\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{\n (e: 'respond', toolCallId: string, approved: boolean): void\n (e: 'regenerate'): void\n /** 点击欢迎项(快捷提问) */\n (e: 'welcome-click', text: string): void\n}>()\n\ndefineSlots<{\n /** 空状态欢迎区插槽 */\n welcome(): any\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** tool 消息不单独渲染,其内容体现在工具卡片的结果区 */\nconst visibleMessages = computed(() => props.messages.filter((msg) => msg.role !== 'tool'))\n\nconst welcomeItems = computed(() => {\n const welcome = props.welcome\n if (!welcome) return []\n return (Array.isArray(welcome) ? welcome : [welcome]).filter(Boolean)\n})\n\n/** 欢迎语逐条轮换:当前展示项的下标 */\nconst welcomeIndex = ref(0)\n\n/** 当前展示的欢迎语 */\nconst currentWelcomeItem = computed(() => {\n const items = welcomeItems.value\n return items.length ? items[welcomeIndex.value % items.length] : undefined\n})\n\nlet welcomeTimer: ReturnType<typeof setInterval> | undefined\n\nconst stopWelcomeRotation = () => {\n clearInterval(welcomeTimer)\n welcomeTimer = undefined\n}\n\nconst startWelcomeRotation = () => {\n stopWelcomeRotation()\n if (welcomeItems.value.length > 1) {\n welcomeTimer = setInterval(() => {\n welcomeIndex.value += 1\n }, 4000)\n }\n}\n\n/** 点击欢迎球:不等自动轮换,立即换下一条,并重置计时 */\nconst cycleWelcome = () => {\n if (welcomeItems.value.length <= 1) return\n welcomeIndex.value += 1\n startWelcomeRotation()\n}\n\n// 欢迎区展示期间自动轮换;出现消息后停止\nwatch(\n () => [welcomeItems.value.length, visibleMessages.value.length] as const,\n () => {\n if (visibleMessages.value.length > 0) stopWelcomeRotation()\n else startWelcomeRotation()\n },\n { immediate: true }\n)\n\nonBeforeUnmount(stopWelcomeRotation)\n\nconst workingOrbRef = useTemplateRef<AiOrbExposed>('workingOrb')\n\n/** 工具调用失败的 id 去重集合:同一次失败只播一次沮丧表情 */\nconst reactedToolErrorIds = new Set<string>()\n\nconst toolErrorIds = computed(() => {\n const ids: string[] = []\n for (const msg of props.messages) {\n if (msg.role !== 'assistant' || !msg.toolCalls) continue\n for (const call of msg.toolCalls) {\n if (call.status === 'error') ids.push(call.id)\n }\n }\n return ids\n})\n\n// 工具调用失败 → 工作球闭眼摇头(frustrated)\nwatch(toolErrorIds, (ids) => {\n for (const id of ids) {\n if (reactedToolErrorIds.has(id)) continue\n reactedToolErrorIds.add(id)\n workingOrbRef.value?.react('frustrated')\n }\n})\n\n/** 回答完毕 / 失败后工作球短暂停留,播完表情再隐藏 */\nconst finishing = ref(false)\nlet finishTimer: ReturnType<typeof setTimeout> | undefined\n\nconst clearFinishing = () => {\n clearTimeout(finishTimer)\n finishing.value = false\n}\n\n/** 工作球状态:收尾停留 → idle;正文输出中 → speaking;其余生成阶段 → thinking */\nconst workingOrbStatus = computed<AiOrbStatus>(() => {\n if (finishing.value) return 'idle'\n const last = visibleMessages.value[visibleMessages.value.length - 1]\n if (last?.role === 'assistant' && last.status === 'streaming' && last.content) {\n return 'speaking'\n }\n return 'thinking'\n})\n\nwatch(\n () => props.running,\n (running, prev) => {\n if (running) {\n // 新一轮生成开始:立即结束收尾停留\n clearFinishing()\n return\n }\n if (!prev) return\n\n finishing.value = true\n const last = visibleMessages.value[visibleMessages.value.length - 1]\n let reaction: AiOrbReaction | undefined\n if (last?.role === 'assistant') {\n if (last.status === 'done') reaction = 'happy'\n else if (last.status === 'error') reaction = 'frustrated'\n }\n if (reaction) {\n nextTick(() => workingOrbRef.value?.react(reaction))\n }\n finishTimer = setTimeout(clearFinishing, WORKING_LINGER_MS)\n }\n)\n\nonBeforeUnmount(clearFinishing)\n\nconst scrollRef = shallowRef<{ scrollTo: (position: ScrollPosition) => void }>()\n/** 是否吸附底部(用户上翻浏览历史时取消吸附) */\nconst stickToBottom = ref(true)\n/** 上一次滚动位置:用于区分用户上翻与自动吸底触发的 scroll 事件 */\nlet lastScrollY = 0\n\n/** 上翻阅读时展示「最新消息」悬浮按钮 */\nconst showToLatest = computed(() => !stickToBottom.value && visibleMessages.value.length > 0)\n\nconst handleScroll = (position: Required<ScrollPosition>) => {\n const atBottom = position.sh - position.ch - position.y < 60\n if (atBottom) {\n stickToBottom.value = true\n } else if (position.y < lastScrollY) {\n // 只有「向上滚」才算用户离开底部的意图:自动吸底的 scroll 事件可能滞后于\n // 流式内容增长(sh 已变大、y 仍是旧底部),按位置判断会误判成未吸底\n stickToBottom.value = false\n }\n lastScrollY = position.y\n}\n\n/**\n * 用户向上滚动轮 = 明确的离开底部意图,立即取消吸附。\n * 不能等 scroll 事件再按方向判断:流式期间每个 chunk 都会排一次吸底滚动,\n * 滞后的 scrollTo 会把用户拉回底部并触发 atBottom 重新吸附,导致「上滚动不了」\n */\nconst handleWheel = (e: WheelEvent) => {\n if (e.deltaY < 0) stickToBottom.value = false\n}\n\n/** 点击悬浮按钮:恢复吸附并回到底部 */\nconst scrollToLatest = async () => {\n stickToBottom.value = true\n await nextTick()\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n}\n\n// 消息数量或流式内容变化时,若处于吸附状态则滚动到底部\nwatch(\n () => {\n const list = visibleMessages.value\n const last = list[list.length - 1]\n return [\n list.length,\n last?.content.length ?? 0,\n last?.reasoning?.length ?? 0,\n last?.toolCalls?.length ?? 0,\n props.running\n ].join('|')\n },\n async () => {\n // 用户刚发出消息时强制回到底部(无论之前是否在上翻)\n const list = visibleMessages.value\n if (list[list.length - 1]?.role === 'user') stickToBottom.value = true\n if (!stickToBottom.value) return\n await nextTick()\n // 等待 DOM 更新期间用户可能已上滚,滚动前重新确认吸附状态,避免把用户拉回底部\n if (!stickToBottom.value) return\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }\n)\n\nlet trailingScrollTimer: ReturnType<typeof setTimeout> | undefined\n\n// 生成收尾时动作区出现 / markdown 终态渲染 / 思考块折叠会再次改变高度,\n// 同帧滚动可能够不到最终底部,追加一次延迟兜底滚动\nwatch(\n () => props.running,\n async (running) => {\n if (running) return\n clearTimeout(trailingScrollTimer)\n if (!stickToBottom.value) return\n await nextTick()\n if (!stickToBottom.value) return\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n trailingScrollTimer = setTimeout(() => {\n if (stickToBottom.value) scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }, 150)\n }\n)\n\nonBeforeUnmount(() => clearTimeout(trailingScrollTimer))\n</script>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"message-list.js","names":[],"sources":["../../../src/components/ai-chat/message-list.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('list-wrap')\">\n <UScroll\n :class=\"cls.e('list')\"\n ref=\"scrollRef\"\n always\n @scroll=\"handleScroll\"\n @wheel.passive=\"handleWheel\"\n >\n <template v-for=\"turn in turns\" :key=\"turn.key\">\n <MessageItem\n v-if=\"turn.userMsg\"\n :message=\"turn.userMsg\"\n :is-last=\"false\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n\n <!-- 最终答案开始输出后,本轮之前的思考/工具调用过程收进「已完成」折叠块 -->\n <TurnProcess\n v-if=\"turn.processCollapsed\"\n :messages=\"turn.processMsgs\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n />\n <template v-else>\n <MessageItem\n v-for=\"msg in turn.processMsgs\"\n :key=\"msg.id\"\n :message=\"msg\"\n :is-last=\"false\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n </template>\n\n <MessageItem\n v-if=\"turn.answerMsg\"\n :message=\"turn.answerMsg\"\n :is-last=\"turn.isLast\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n </template>\n\n <!-- 工作中:活体球从输入框上方跳到最新会话下面;结束停留后再跳回 -->\n <div v-if=\"showWorking\" :class=\"[cls.e('working'), bem.is('leaving', workingLeaving)]\">\n <UAiOrb ref=\"workingOrb\" :size=\"ORB_SIZE\" :status=\"workingOrbStatus\" />\n <span v-if=\"running\" :class=\"[cls.e('working-text'), 'u-shine']\">工作中…</span>\n </div>\n </UScroll>\n\n <!-- 空闲:始终钉在滚动容器外、输入框上方;工作开始时缩小离开、结束后放大回来 -->\n <div\n v-if=\"showIdleWelcome\"\n :class=\"[cls.e('welcome'), bem.is('leaving', idleLeaving), bem.is('enter', idleEnter)]\"\n >\n <slot name=\"welcome\">\n <div :class=\"cls.e('welcome-inner')\">\n <UAiOrb :size=\"ORB_SIZE\" @click=\"cycleWelcome\" />\n <!-- 快捷提问逐条展示在球右侧并自动轮换:点文案发送,点球立即换下一条。\n 固定锚点布局:球位置钉死不动,气泡只向右延展,避免轮换时水平瞬移 -->\n <div v-if=\"welcomeItems.length\" :class=\"cls.e('welcome-item-wrap')\">\n <Transition :name=\"cls.em('welcome', 'swap')\" mode=\"out-in\">\n <button\n v-if=\"currentWelcomeItem\"\n :key=\"welcomeIndex\"\n type=\"button\"\n :class=\"cls.e('welcome-item')\"\n @click=\"emit('welcome-click', currentWelcomeItem)\"\n >\n {{ currentWelcomeItem }}\n </button>\n </Transition>\n </div>\n </div>\n </slot>\n </div>\n\n <!-- 用户上翻浏览时悬浮入口:一键回到最新消息 -->\n <Transition :name=\"cls.em('to-latest', 'fade')\">\n <button v-if=\"showToLatest\" type=\"button\" :class=\"cls.e('to-latest')\" @click=\"scrollToLatest\">\n <UIcon><ArrowDown /></UIcon>\n <span>最新消息</span>\n </button>\n </Transition>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon, UScroll } from '@veltra/desktop'\nimport type { ScrollPosition } from '@veltra/desktop'\nimport { ArrowDown } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport {\n computed,\n inject,\n nextTick,\n onBeforeUnmount,\n ref,\n shallowRef,\n useTemplateRef,\n watch\n} from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport type { AiOrbExposed, AiOrbReaction, AiOrbStatus } from '../../types/ai-orb'\nimport UAiOrb from '../ai-orb/ai-orb.vue'\nimport { AiChatDIKey } from './di'\nimport MessageItem from './message-item.vue'\nimport TurnProcess from './turn-process.vue'\n\ndefineOptions({ name: 'UAiChatMessageList' })\n\n/** 空闲欢迎 / 工作指示共用活体球直径(px) */\nconst ORB_SIZE = 48\n/** 生成结束 / 失败后工作球停留再跳回输入框上方 */\nconst WORKING_LINGER_MS = 2500\n/** 两处活体球对向缩放的时长(与样式中的 jump 过渡一致) */\nconst JUMP_MS = 380\n\nconst props = defineProps<{\n messages: ChatMessage[]\n /** 空闲时输入框上方的快捷提问;字符串按单项处理 */\n welcome?: string | string[]\n /** 是否生成中(控制底部工作状态指示) */\n running?: boolean\n /** 透传给 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 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// 空闲欢迎区展示期间自动轮换;跳到工作位后停止\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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"}
|
|
@@ -72,6 +72,16 @@
|
|
|
72
72
|
opacity: 1;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
@keyframes u-ai-chat-orb-jump-in {
|
|
76
|
+
from {
|
|
77
|
+
opacity: 0;
|
|
78
|
+
transform: scale(0);
|
|
79
|
+
}
|
|
80
|
+
to {
|
|
81
|
+
opacity: 1;
|
|
82
|
+
transform: scale(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
75
85
|
.u-ai-chat__panel-header {
|
|
76
86
|
flex: none;
|
|
77
87
|
display: flex;
|
|
@@ -168,20 +178,34 @@
|
|
|
168
178
|
flex: 1;
|
|
169
179
|
min-height: 0;
|
|
170
180
|
}
|
|
171
|
-
.u-ai-chat__list .u-scroll__content {
|
|
172
|
-
min-height: 100%;
|
|
173
|
-
display: flex;
|
|
174
|
-
flex-direction: column;
|
|
175
|
-
}
|
|
176
181
|
.u-ai-chat__welcome {
|
|
177
|
-
flex:
|
|
182
|
+
flex-shrink: 0;
|
|
183
|
+
width: 100%;
|
|
184
|
+
height: 48px;
|
|
178
185
|
display: flex;
|
|
179
|
-
justify-content:
|
|
180
|
-
align-items:
|
|
186
|
+
justify-content: flex-start;
|
|
187
|
+
align-items: center;
|
|
181
188
|
flex-wrap: nowrap;
|
|
182
189
|
color: var(--u-text-color-assist);
|
|
183
|
-
padding: var(--u-gap-default)
|
|
184
|
-
|
|
190
|
+
padding: 0 var(--u-gap-default);
|
|
191
|
+
}
|
|
192
|
+
.u-ai-chat__welcome .u-ai-orb {
|
|
193
|
+
transform-origin: center;
|
|
194
|
+
}
|
|
195
|
+
.u-ai-chat__welcome.is-enter .u-ai-orb {
|
|
196
|
+
animation: u-ai-chat-orb-jump-in 0.38s ease both;
|
|
197
|
+
}
|
|
198
|
+
.u-ai-chat__welcome.is-leaving {
|
|
199
|
+
pointer-events: none;
|
|
200
|
+
}
|
|
201
|
+
.u-ai-chat__welcome.is-leaving .u-ai-orb {
|
|
202
|
+
opacity: 0;
|
|
203
|
+
transform: scale(0);
|
|
204
|
+
transition: opacity 0.28s ease, transform 0.38s ease;
|
|
205
|
+
}
|
|
206
|
+
.u-ai-chat__welcome.is-leaving .u-ai-chat__welcome-item-wrap {
|
|
207
|
+
opacity: 0;
|
|
208
|
+
transition: opacity 0.2s ease;
|
|
185
209
|
}
|
|
186
210
|
.u-ai-chat__welcome-inner {
|
|
187
211
|
display: flex;
|
|
@@ -194,12 +218,15 @@
|
|
|
194
218
|
.u-ai-chat__welcome-item-wrap {
|
|
195
219
|
flex: 1;
|
|
196
220
|
min-width: 0;
|
|
221
|
+
height: 48px;
|
|
197
222
|
display: flex;
|
|
198
223
|
align-items: center;
|
|
199
|
-
min-height: 54px;
|
|
200
224
|
}
|
|
201
225
|
.u-ai-chat__welcome-item {
|
|
202
226
|
max-width: 100%;
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
text-overflow: ellipsis;
|
|
229
|
+
white-space: nowrap;
|
|
203
230
|
padding: var(--u-gap-small) var(--u-gap-default);
|
|
204
231
|
border: none;
|
|
205
232
|
border-radius: var(--u-radius-large);
|
|
@@ -235,10 +262,34 @@
|
|
|
235
262
|
flex-wrap: nowrap;
|
|
236
263
|
gap: var(--u-gap-small);
|
|
237
264
|
padding: 0 var(--u-gap-default) var(--u-gap-small);
|
|
265
|
+
width: fit-content;
|
|
266
|
+
}
|
|
267
|
+
.u-ai-chat__working .u-ai-orb {
|
|
268
|
+
transform-origin: center;
|
|
269
|
+
animation: u-ai-chat-orb-jump-in 0.38s ease both;
|
|
270
|
+
}
|
|
271
|
+
.u-ai-chat__working.is-leaving {
|
|
272
|
+
pointer-events: none;
|
|
273
|
+
}
|
|
274
|
+
.u-ai-chat__working.is-leaving .u-ai-orb {
|
|
275
|
+
animation: none;
|
|
276
|
+
opacity: 0;
|
|
277
|
+
transform: scale(0);
|
|
278
|
+
transition: opacity 0.28s ease, transform 0.38s ease;
|
|
238
279
|
}
|
|
239
280
|
.u-ai-chat__working-text {
|
|
240
281
|
font-size: var(--u-font-size-main-small);
|
|
241
282
|
}
|
|
283
|
+
@media (prefers-reduced-motion: reduce) {
|
|
284
|
+
.u-ai-chat .u-ai-chat__welcome.is-enter .u-ai-orb,
|
|
285
|
+
.u-ai-chat .u-ai-chat__working .u-ai-orb {
|
|
286
|
+
animation: none;
|
|
287
|
+
}
|
|
288
|
+
.u-ai-chat .u-ai-chat__welcome.is-leaving .u-ai-orb,
|
|
289
|
+
.u-ai-chat .u-ai-chat__working.is-leaving .u-ai-orb {
|
|
290
|
+
transition: none;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
242
293
|
.u-ai-chat__to-latest {
|
|
243
294
|
position: absolute;
|
|
244
295
|
top: var(--u-gap-small);
|
|
@@ -308,9 +359,7 @@
|
|
|
308
359
|
border-radius: var(--u-radius-default);
|
|
309
360
|
display: block;
|
|
310
361
|
}
|
|
311
|
-
.u-ai-
|
|
312
|
-
font-size: 13px;
|
|
313
|
-
line-height: 1.6;
|
|
362
|
+
.u-ai-chat .markstream-vue {
|
|
314
363
|
--ms-text-body: 13px;
|
|
315
364
|
--ms-leading-body: 1.6;
|
|
316
365
|
--ms-text-h1: 1.55em;
|
|
@@ -336,9 +385,14 @@
|
|
|
336
385
|
--ms-flow-admonition-y: 0.7em;
|
|
337
386
|
--ms-flow-diagram-y: 0.7em;
|
|
338
387
|
}
|
|
388
|
+
.u-ai-chat .markstream-vue .list-disc {
|
|
389
|
+
margin-top: var(--ms-flow-list-y);
|
|
390
|
+
margin-bottom: var(--ms-flow-list-y);
|
|
391
|
+
padding-left: var(--ms-flow-list-indent);
|
|
392
|
+
}
|
|
339
393
|
.u-ai-chat__message-status {
|
|
340
394
|
margin-top: var(--u-gap-small);
|
|
341
|
-
color: var(--u-text-color-
|
|
395
|
+
color: var(--u-text-color-second);
|
|
342
396
|
font-size: var(--u-font-size-main-small);
|
|
343
397
|
}
|
|
344
398
|
.u-ai-chat__message-actions {
|
|
@@ -348,7 +402,7 @@
|
|
|
348
402
|
}
|
|
349
403
|
.u-ai-chat__reasoning {
|
|
350
404
|
margin-bottom: 4px;
|
|
351
|
-
color: var(--u-text-color-
|
|
405
|
+
color: var(--u-text-color-second);
|
|
352
406
|
}
|
|
353
407
|
.u-ai-chat__reasoning.is-expanded .u-ai-chat__reasoning-chevron {
|
|
354
408
|
transform: rotate(90deg);
|
|
@@ -382,30 +436,59 @@
|
|
|
382
436
|
font-size: var(--u-font-size-main-small);
|
|
383
437
|
line-height: 1.55;
|
|
384
438
|
}
|
|
439
|
+
.u-ai-chat__process {
|
|
440
|
+
padding: 4px var(--u-gap-default);
|
|
441
|
+
color: var(--u-text-color-second);
|
|
442
|
+
}
|
|
443
|
+
.u-ai-chat__process.is-expanded .u-ai-chat__process-chevron {
|
|
444
|
+
transform: rotate(90deg);
|
|
445
|
+
}
|
|
446
|
+
.u-ai-chat__process-header {
|
|
447
|
+
display: flex;
|
|
448
|
+
justify-content: flex-start;
|
|
449
|
+
align-items: center;
|
|
450
|
+
flex-wrap: nowrap;
|
|
451
|
+
gap: 4px;
|
|
452
|
+
cursor: pointer;
|
|
453
|
+
user-select: none;
|
|
454
|
+
font-size: var(--u-font-size-main-small);
|
|
455
|
+
}
|
|
456
|
+
.u-ai-chat__process-icon {
|
|
457
|
+
color: var(--u-color-success);
|
|
458
|
+
}
|
|
459
|
+
.u-ai-chat__process-title {
|
|
460
|
+
line-height: 1.5;
|
|
461
|
+
}
|
|
462
|
+
.u-ai-chat__process-chevron {
|
|
463
|
+
font-size: 12px;
|
|
464
|
+
transition: transform 0.2s;
|
|
465
|
+
}
|
|
466
|
+
.u-ai-chat__process-content {
|
|
467
|
+
margin-top: 4px;
|
|
468
|
+
padding-left: var(--u-gap-small);
|
|
469
|
+
border-left: var(--u-border-muted);
|
|
470
|
+
}
|
|
471
|
+
.u-ai-chat__process-content .u-ai-chat__message {
|
|
472
|
+
padding-left: 0;
|
|
473
|
+
padding-right: 0;
|
|
474
|
+
}
|
|
385
475
|
.u-ai-chat__tool-call {
|
|
386
476
|
margin: 4px 0;
|
|
387
|
-
background-color: var(--u-bg-color-top);
|
|
388
477
|
font-size: var(--u-font-size-main-small);
|
|
389
478
|
}
|
|
390
|
-
.u-ai-chat__tool-call + .u-ai-chat__tool-call {
|
|
479
|
+
.u-ai-chat .u-ai-chat__tool-call + .u-ai-chat__tool-call {
|
|
391
480
|
margin-top: 4px;
|
|
392
481
|
}
|
|
393
|
-
.u-ai-chat__tool-call .u-collapse__header {
|
|
482
|
+
.u-ai-chat .u-ai-chat__tool-call .u-collapse__header {
|
|
394
483
|
gap: 6px;
|
|
395
484
|
padding: 5px var(--u-gap-small);
|
|
396
485
|
font-weight: 400;
|
|
397
486
|
letter-spacing: normal;
|
|
398
487
|
}
|
|
399
|
-
.u-ai-chat__tool-call .u-collapse__content {
|
|
400
|
-
border-top: var(--u-border-muted);
|
|
488
|
+
.u-ai-chat .u-ai-chat__tool-call .u-collapse__content {
|
|
401
489
|
padding: var(--u-gap-small);
|
|
402
490
|
line-height: 1.5;
|
|
403
491
|
}
|
|
404
|
-
.u-ai-chat__tool-call-chevron {
|
|
405
|
-
flex-shrink: 0;
|
|
406
|
-
font-size: 12px;
|
|
407
|
-
color: var(--u-text-color-assist);
|
|
408
|
-
}
|
|
409
492
|
.u-ai-chat__tool-call-status {
|
|
410
493
|
flex-shrink: 0;
|
|
411
494
|
font-size: 13px;
|
|
@@ -688,14 +771,11 @@
|
|
|
688
771
|
width: 100%;
|
|
689
772
|
max-width: 800px;
|
|
690
773
|
box-sizing: border-box;
|
|
691
|
-
margin:
|
|
774
|
+
margin: 0 0 var(--u-gap-default);
|
|
692
775
|
border-radius: var(--u-radius-large);
|
|
693
776
|
background-color: var(--u-bg-color-top);
|
|
777
|
+
box-shadow: var(--u-shadow-sm);
|
|
694
778
|
padding: var(--u-gap-small);
|
|
695
|
-
transition: box-shadow 0.15s ease;
|
|
696
|
-
}
|
|
697
|
-
.u-ai-chat__input-area:focus-within {
|
|
698
|
-
box-shadow: 0 0 0 1px var(--u-color-primary);
|
|
699
779
|
}
|
|
700
780
|
.u-ai-chat__input {
|
|
701
781
|
width: 100%;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AiChatDIKey } from "./di.js";
|
|
2
2
|
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, ref, resolveDynamicComponent, toDisplayString, unref, watch, withCtx, withKeys, withModifiers } from "vue";
|
|
3
|
-
import {
|
|
3
|
+
import { CircleCheckFilled, CircleClose, Close, Loading, WarningFilled } from "@veltra/icons/normal";
|
|
4
4
|
import { UButton, UCollapseItem, UIcon } from "@veltra/desktop";
|
|
5
5
|
import { bem } from "@veltra/utils";
|
|
6
6
|
//#region src/components/ai-chat/tool-call.vue
|
|
@@ -152,11 +152,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
152
152
|
}, {
|
|
153
153
|
default: withCtx(() => [..._cache[8] || (_cache[8] = [createTextVNode("拒绝", -1)])]),
|
|
154
154
|
_: 1
|
|
155
|
-
})], 34)) : createCommentVNode("v-if", true)
|
|
156
|
-
createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("tool-call-chevron")) }, {
|
|
157
|
-
default: withCtx(() => [createVNode(unref(ArrowDown))]),
|
|
158
|
-
_: 1
|
|
159
|
-
}, 8, ["class"])
|
|
155
|
+
})], 34)) : createCommentVNode("v-if", true)
|
|
160
156
|
]),
|
|
161
157
|
default: withCtx(() => [isPanelTool.value ? (openBlock(), createElementBlock("div", {
|
|
162
158
|
key: 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-call.js","names":[],"sources":["../../../src/components/ai-chat/tool-call.vue"],"sourcesContent":["<template>\n <UCollapseItem\n v-model=\"isExpanded\"\n :class=\"[cls.e('tool-call'), bem.is(toolCall.status)]\"\n :destroy-on-collapse=\"destroyOnCollapse\"\n @change=\"markUserToggled\"\n >\n <template #header>\n <UIcon :class=\"[cls.e('tool-call-status'), statusClass]\">\n <component :is=\"headerIcon\" />\n </UIcon>\n <span :class=\"[cls.e('tool-call-name'), isActive && 'u-shine']\">\n {{ tool?.label ?? toolCall.name }}\n </span>\n <span v-if=\"summary\" :class=\"cls.e('tool-call-summary')\">{{ summary }}</span>\n\n <span\n v-if=\"toolCall.status === 'awaiting-confirm'\"\n :class=\"cls.e('tool-call-confirm')\"\n @click.stop\n @keydown.enter.stop\n @keydown.space.stop\n >\n <UButton size=\"small\" type=\"primary\" @click=\"emit('respond', true)\">允许</UButton>\n <UButton size=\"small\" text @click=\"emit('respond', false)\">拒绝</UButton>\n </span>\n\n <UIcon :class=\"cls.e('tool-call-chevron')\"><ArrowDown /></UIcon>\n </template>\n\n <!-- 面板工具:render 展示在右侧侧边面板,卡片 body 仅提供查看入口 -->\n <div v-if=\"isPanelTool\" :class=\"cls.e('tool-call-panel-entry')\">\n <span :class=\"cls.e('tool-call-panel-hint')\">内容展示在右侧面板</span>\n <UButton size=\"small\" text type=\"primary\" @click=\"di?.openPanel(toolCall.id)\">\n 查看面板\n </UButton>\n </div>\n <!-- 自定义渲染:工具定义的 render 优先于 tool-<name> 插槽,均替换整个 body -->\n <component :is=\"tool.render\" v-else-if=\"tool?.render\" :tool-call=\"toolCall\" />\n <component :is=\"slotBody\" v-else-if=\"slotBody && hasResult\" />\n <template v-else>\n <template v-if=\"prettyArguments\">\n <div :class=\"cls.e('tool-call-section')\">参数</div>\n <pre :class=\"cls.e('tool-call-code')\">{{ prettyArguments }}</pre>\n </template>\n\n <template v-if=\"hasResult\">\n <div :class=\"cls.e('tool-call-section')\">\n {{ toolCall.status === 'error' ? '错误' : '结果' }}\n </div>\n <pre :class=\"cls.e('tool-call-code')\">{{ displayResult }}</pre>\n </template>\n </template>\n </UCollapseItem>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UCollapseItem, UIcon } from '@veltra/desktop'\nimport {\n ArrowDown,\n CircleCheckFilled,\n CircleClose,\n Close,\n Loading,\n WarningFilled\n} from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, watch, type Component } from 'vue'\n\nimport type { ChatToolCall } from '../../chat/types'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatToolCall' })\n\nconst props = defineProps<{ toolCall: ChatToolCall }>()\n\nconst emit = defineEmits<{ (e: 'respond', approved: boolean): void }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 当前调用对应的工具定义(解析 icon/label/render/autoCollapse/terminal) */\nconst tool = computed(() => di?.tools.value[props.toolCall.name])\n\nconst isExpanded = ref(false)\n/** 用户手动切换过后,不再随 status 自动展开/折叠 */\nlet userToggled: boolean = false\n\n/** UCollapseItem 的 change 仅在用户点击头部时触发(程序化 v-model 变更不触发) */\nconst markUserToggled = () => {\n userToggled = true\n}\n\n/** 面板工具:render 展示在右侧侧边面板,卡片 body 仅保留「查看面板」入口 */\nconst isPanelTool = computed(() => tool.value?.renderTo === 'panel')\n\n/** 完成后是否自动折叠:缺省面板工具折叠;有 render 时不折叠,否则折叠 */\nconst autoCollapse = computed(\n () => tool.value?.autoCollapse ?? (isPanelTool.value || !tool.value?.render)\n)\n\nwatch(\n () => props.toolCall.status,\n (status) => {\n if (userToggled) return\n if (status === 'running' || status === 'awaiting-confirm') {\n isExpanded.value = true\n } else {\n isExpanded.value = !autoCollapse.value\n }\n },\n { immediate: true }\n)\n\nconst STATUS_META: Record<string, { icon: Component; class: string }> = {\n pending: { icon: Loading, class: 'is-active' },\n running: { icon: Loading, class: 'is-active' },\n 'awaiting-confirm': { icon: WarningFilled, class: 'is-warning' },\n success: { icon: CircleCheckFilled, class: 'is-success' },\n error: { icon: CircleClose, class: 'is-danger' },\n rejected: { icon: Close, class: 'is-danger' }\n}\n\nconst statusIcon = computed(() => STATUS_META[props.toolCall.status]?.icon ?? Loading)\nconst statusClass = computed(() => STATUS_META[props.toolCall.status]?.class ?? '')\n\n/** 进行中(pending/running):工具名带扫光效果 */\nconst isActive = computed(() => {\n return props.toolCall.status === 'pending' || props.toolCall.status === 'running'\n})\n\n/** 终态(成功/失败/拒绝) */\nconst isSettled = computed(() => {\n return ['success', 'error', 'rejected'].includes(props.toolCall.status)\n})\n\n/**\n * 折叠后卸载内容 DOM:仅终态且非面板工具时启用——\n * 进行中/待确认要保留内容状态(确认按钮、表单),面板工具的 body 是「查看面板」入口必须可达\n */\nconst destroyOnCollapse = computed(() => isSettled.value && !isPanelTool.value)\n\n/** 头部图标:工具自定义图标优先,缺省用状态图标 */\nconst headerIcon = computed(() => tool.value?.icon ?? statusIcon.value)\n\nconst parsedArguments = computed<Record<string, unknown> | null>(() => {\n if (!props.toolCall.arguments) return null\n try {\n return JSON.parse(props.toolCall.arguments)\n } catch {\n return null\n }\n})\n\n/** 折叠态参数摘要:取第一个字符串参数值 */\nconst summary = computed(() => {\n const args = parsedArguments.value\n if (!args) return ''\n for (const value of Object.values(args)) {\n if (typeof value === 'string' && value) {\n return value.length > 40 ? `${value.slice(0, 40)}…` : value\n }\n }\n return ''\n})\n\nconst prettyArguments = computed(() => {\n const args = parsedArguments.value\n if (args) return JSON.stringify(args, null, 2)\n return props.toolCall.arguments || ''\n})\n\nconst hasResult = computed(() => {\n return props.toolCall.result != null || props.toolCall.error != null\n})\n\nconst displayResult = computed(() => {\n const raw = props.toolCall.error ?? props.toolCall.result ?? ''\n try {\n return JSON.stringify(JSON.parse(raw), null, 2)\n } catch {\n return raw\n }\n})\n\n/** 使用方通过 tool-<name> 插槽自定义该工具的展示(有结果时替换整个 body) */\nconst slotBody = computed(() => {\n const slot = di?.slots[`tool-${props.toolCall.name}`]\n return slot ? () => slot({ toolCall: props.toolCall }) : undefined\n})\n</script>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tool-call.js","names":[],"sources":["../../../src/components/ai-chat/tool-call.vue"],"sourcesContent":["<template>\n <UCollapseItem\n v-model=\"isExpanded\"\n :class=\"[cls.e('tool-call'), bem.is(toolCall.status)]\"\n :destroy-on-collapse=\"destroyOnCollapse\"\n @change=\"markUserToggled\"\n >\n <template #header>\n <UIcon :class=\"[cls.e('tool-call-status'), statusClass]\">\n <component :is=\"headerIcon\" />\n </UIcon>\n <span :class=\"[cls.e('tool-call-name'), isActive && 'u-shine']\">\n {{ tool?.label ?? toolCall.name }}\n </span>\n <span v-if=\"summary\" :class=\"cls.e('tool-call-summary')\">{{ summary }}</span>\n\n <span\n v-if=\"toolCall.status === 'awaiting-confirm'\"\n :class=\"cls.e('tool-call-confirm')\"\n @click.stop\n @keydown.enter.stop\n @keydown.space.stop\n >\n <UButton size=\"small\" type=\"primary\" @click=\"emit('respond', true)\">允许</UButton>\n <UButton size=\"small\" text @click=\"emit('respond', false)\">拒绝</UButton>\n </span>\n </template>\n\n <!-- 面板工具:render 展示在右侧侧边面板,卡片 body 仅提供查看入口 -->\n <div v-if=\"isPanelTool\" :class=\"cls.e('tool-call-panel-entry')\">\n <span :class=\"cls.e('tool-call-panel-hint')\">内容展示在右侧面板</span>\n <UButton size=\"small\" text type=\"primary\" @click=\"di?.openPanel(toolCall.id)\">\n 查看面板\n </UButton>\n </div>\n <!-- 自定义渲染:工具定义的 render 优先于 tool-<name> 插槽,均替换整个 body -->\n <component :is=\"tool.render\" v-else-if=\"tool?.render\" :tool-call=\"toolCall\" />\n <component :is=\"slotBody\" v-else-if=\"slotBody && hasResult\" />\n <template v-else>\n <template v-if=\"prettyArguments\">\n <div :class=\"cls.e('tool-call-section')\">参数</div>\n <pre :class=\"cls.e('tool-call-code')\">{{ prettyArguments }}</pre>\n </template>\n\n <template v-if=\"hasResult\">\n <div :class=\"cls.e('tool-call-section')\">\n {{ toolCall.status === 'error' ? '错误' : '结果' }}\n </div>\n <pre :class=\"cls.e('tool-call-code')\">{{ displayResult }}</pre>\n </template>\n </template>\n </UCollapseItem>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UCollapseItem, UIcon } from '@veltra/desktop'\nimport { CircleCheckFilled, CircleClose, Close, Loading, WarningFilled } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, watch, type Component } from 'vue'\n\nimport type { ChatToolCall } from '../../chat/types'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatToolCall' })\n\nconst props = defineProps<{ toolCall: ChatToolCall }>()\n\nconst emit = defineEmits<{ (e: 'respond', approved: boolean): void }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 当前调用对应的工具定义(解析 icon/label/render/autoCollapse/terminal) */\nconst tool = computed(() => di?.tools.value[props.toolCall.name])\n\nconst isExpanded = ref(false)\n/** 用户手动切换过后,不再随 status 自动展开/折叠 */\nlet userToggled: boolean = false\n\n/** UCollapseItem 的 change 仅在用户点击头部时触发(程序化 v-model 变更不触发) */\nconst markUserToggled = () => {\n userToggled = true\n}\n\n/** 面板工具:render 展示在右侧侧边面板,卡片 body 仅保留「查看面板」入口 */\nconst isPanelTool = computed(() => tool.value?.renderTo === 'panel')\n\n/** 完成后是否自动折叠:缺省面板工具折叠;有 render 时不折叠,否则折叠 */\nconst autoCollapse = computed(\n () => tool.value?.autoCollapse ?? (isPanelTool.value || !tool.value?.render)\n)\n\nwatch(\n () => props.toolCall.status,\n (status) => {\n if (userToggled) return\n if (status === 'running' || status === 'awaiting-confirm') {\n isExpanded.value = true\n } else {\n isExpanded.value = !autoCollapse.value\n }\n },\n { immediate: true }\n)\n\nconst STATUS_META: Record<string, { icon: Component; class: string }> = {\n pending: { icon: Loading, class: 'is-active' },\n running: { icon: Loading, class: 'is-active' },\n 'awaiting-confirm': { icon: WarningFilled, class: 'is-warning' },\n success: { icon: CircleCheckFilled, class: 'is-success' },\n error: { icon: CircleClose, class: 'is-danger' },\n rejected: { icon: Close, class: 'is-danger' }\n}\n\nconst statusIcon = computed(() => STATUS_META[props.toolCall.status]?.icon ?? Loading)\nconst statusClass = computed(() => STATUS_META[props.toolCall.status]?.class ?? '')\n\n/** 进行中(pending/running):工具名带扫光效果 */\nconst isActive = computed(() => {\n return props.toolCall.status === 'pending' || props.toolCall.status === 'running'\n})\n\n/** 终态(成功/失败/拒绝) */\nconst isSettled = computed(() => {\n return ['success', 'error', 'rejected'].includes(props.toolCall.status)\n})\n\n/**\n * 折叠后卸载内容 DOM:仅终态且非面板工具时启用——\n * 进行中/待确认要保留内容状态(确认按钮、表单),面板工具的 body 是「查看面板」入口必须可达\n */\nconst destroyOnCollapse = computed(() => isSettled.value && !isPanelTool.value)\n\n/** 头部图标:工具自定义图标优先,缺省用状态图标 */\nconst headerIcon = computed(() => tool.value?.icon ?? statusIcon.value)\n\nconst parsedArguments = computed<Record<string, unknown> | null>(() => {\n if (!props.toolCall.arguments) return null\n try {\n return JSON.parse(props.toolCall.arguments)\n } catch {\n return null\n }\n})\n\n/** 折叠态参数摘要:取第一个字符串参数值 */\nconst summary = computed(() => {\n const args = parsedArguments.value\n if (!args) return ''\n for (const value of Object.values(args)) {\n if (typeof value === 'string' && value) {\n return value.length > 40 ? `${value.slice(0, 40)}…` : value\n }\n }\n return ''\n})\n\nconst prettyArguments = computed(() => {\n const args = parsedArguments.value\n if (args) return JSON.stringify(args, null, 2)\n return props.toolCall.arguments || ''\n})\n\nconst hasResult = computed(() => {\n return props.toolCall.result != null || props.toolCall.error != null\n})\n\nconst displayResult = computed(() => {\n const raw = props.toolCall.error ?? props.toolCall.result ?? ''\n try {\n return JSON.stringify(JSON.parse(raw), null, 2)\n } catch {\n return raw\n }\n})\n\n/** 使用方通过 tool-<name> 插槽自定义该工具的展示(有结果时替换整个 body) */\nconst slotBody = computed(() => {\n const slot = di?.slots[`tool-${props.toolCall.name}`]\n return slot ? () => slot({ toolCall: props.toolCall }) : undefined\n})\n</script>\n"],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AiChatDIKey } from "./di.js";
|
|
2
|
+
import _sfc_main$1 from "./message-item.js";
|
|
3
|
+
import { Fragment, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, ref, renderList, unref, withCtx } from "vue";
|
|
4
|
+
import { ArrowRight, CircleCheck } from "@veltra/icons/normal";
|
|
5
|
+
import { UIcon } from "@veltra/desktop";
|
|
6
|
+
import { bem } from "@veltra/utils";
|
|
7
|
+
//#region src/components/ai-chat/turn-process.vue
|
|
8
|
+
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
9
|
+
name: "UAiChatTurnProcess",
|
|
10
|
+
__name: "turn-process",
|
|
11
|
+
props: {
|
|
12
|
+
messages: {},
|
|
13
|
+
rendererProps: {}
|
|
14
|
+
},
|
|
15
|
+
emits: ["respond"],
|
|
16
|
+
setup(__props, { emit: __emit }) {
|
|
17
|
+
const emit = __emit;
|
|
18
|
+
const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
|
|
19
|
+
const expanded = ref(false);
|
|
20
|
+
return (_ctx, _cache) => {
|
|
21
|
+
return openBlock(), createElementBlock("div", { class: normalizeClass([unref(cls).e("process"), unref(bem).is("expanded", expanded.value)]) }, [
|
|
22
|
+
createElementVNode("div", {
|
|
23
|
+
class: normalizeClass(unref(cls).e("process-header")),
|
|
24
|
+
onClick: _cache[0] || (_cache[0] = ($event) => expanded.value = !expanded.value)
|
|
25
|
+
}, [
|
|
26
|
+
createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("process-icon")) }, {
|
|
27
|
+
default: withCtx(() => [createVNode(unref(CircleCheck))]),
|
|
28
|
+
_: 1
|
|
29
|
+
}, 8, ["class"]),
|
|
30
|
+
createElementVNode("span", { class: normalizeClass(unref(cls).e("process-title")) }, "已完成", 2),
|
|
31
|
+
createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("process-chevron")) }, {
|
|
32
|
+
default: withCtx(() => [createVNode(unref(ArrowRight))]),
|
|
33
|
+
_: 1
|
|
34
|
+
}, 8, ["class"])
|
|
35
|
+
], 2),
|
|
36
|
+
createCommentVNode(" 折叠时完全卸载过程 DOM:历史轮次的思考/工具卡片可能很多,常驻挂载浪费渲染成本 "),
|
|
37
|
+
expanded.value ? (openBlock(), createElementBlock("div", {
|
|
38
|
+
key: 0,
|
|
39
|
+
class: normalizeClass(unref(cls).e("process-content"))
|
|
40
|
+
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.messages, (msg) => {
|
|
41
|
+
return openBlock(), createBlock(_sfc_main$1, {
|
|
42
|
+
key: msg.id,
|
|
43
|
+
message: msg,
|
|
44
|
+
"is-last": false,
|
|
45
|
+
"renderer-props": __props.rendererProps,
|
|
46
|
+
onRespond: _cache[1] || (_cache[1] = (id, approved) => emit("respond", id, approved))
|
|
47
|
+
}, null, 8, ["message", "renderer-props"]);
|
|
48
|
+
}), 128))], 2)) : createCommentVNode("v-if", true)
|
|
49
|
+
], 2);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
//#endregion
|
|
54
|
+
export { _sfc_main as default };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=turn-process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-process.js","names":[],"sources":["../../../src/components/ai-chat/turn-process.vue"],"sourcesContent":["<template>\n <div :class=\"[cls.e('process'), bem.is('expanded', expanded)]\">\n <div :class=\"cls.e('process-header')\" @click=\"expanded = !expanded\">\n <UIcon :class=\"cls.e('process-icon')\"><CircleCheck /></UIcon>\n <span :class=\"cls.e('process-title')\">已完成</span>\n <UIcon :class=\"cls.e('process-chevron')\"><ArrowRight /></UIcon>\n </div>\n <!-- 折叠时完全卸载过程 DOM:历史轮次的思考/工具卡片可能很多,常驻挂载浪费渲染成本 -->\n <div v-if=\"expanded\" :class=\"cls.e('process-content')\">\n <MessageItem\n v-for=\"msg in messages\"\n :key=\"msg.id\"\n :message=\"msg\"\n :is-last=\"false\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n />\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon } from '@veltra/desktop'\nimport { ArrowRight, CircleCheck } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { inject, ref } from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport { AiChatDIKey } from './di'\nimport MessageItem from './message-item.vue'\n\ndefineOptions({ name: 'UAiChatTurnProcess' })\n\ndefineProps<{\n /** 本轮最终答案之前的过程消息(思考 + 中间文本 + 工具卡片) */\n messages: ChatMessage[]\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{ (e: 'respond', toolCallId: string, approved: boolean): void }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst expanded = ref(false)\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;EAwCA,MAAM,OAAO;EAGb,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;EAEpC,MAAM,WAAW,IAAI,KAAK;;GA5CxB,OAAA,UAAA,GAAA,mBAiBM,OAAA,EAjBA,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,SAAA,GAAa,MAAA,GAAA,CAAG,CAAC,GAAE,YAAa,SAAA,KAAQ,CAAA,CAAA,EAAA,GAAA;IACzD,mBAIM,OAAA;KAJA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,gBAAA,CAAA;KAAqB,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,SAAA,QAAQ,CAAI,SAAA;;KACxD,YAA6D,MAAA,KAAA,GAAA,EAArD,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA,EAAA,GAAA;MAAiC,SAAA,cAAA,CAAf,YAAe,MAAA,WAAA,CAAA,CAAA,CAAA;;;KACrD,mBAAgD,QAAA,EAAzC,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,eAAA,CAAA,EAAA,GAAmB,OAAG,CAAA;KACzC,YAA+D,MAAA,KAAA,GAAA,EAAvD,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,iBAAA,CAAA,EAAA,GAAA;MAAmC,SAAA,cAAA,CAAd,YAAc,MAAA,UAAA,CAAA,CAAA,CAAA;;;;IAEzD,mBAAA,6CAAA;IACW,SAAA,SAAA,UAAA,GAAX,mBASM,OAAA;;KATgB,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,iBAAA,CAAA;IAChC,GAAA,EAAA,UAAA,IAAA,GAAA,mBAOE,UAAA,MAAA,WANc,QAAA,WAAP,QAAG;KADZ,OAAA,UAAA,GAAA,YAOE,aAAA;MALC,KAAK,IAAI;MACT,SAAS;MACT,WAAS;MACT,kBAAgB,QAAA;MAChB,WAAO,OAAA,OAAA,OAAA,MAAG,IAAI,aAAa,KAAI,WAAY,IAAI,QAAQ"}
|
|
@@ -36,7 +36,8 @@ const REACTIONS = {
|
|
|
36
36
|
gaze: {
|
|
37
37
|
x: 0,
|
|
38
38
|
y: -.05
|
|
39
|
-
}
|
|
39
|
+
},
|
|
40
|
+
earTilt: .14
|
|
40
41
|
},
|
|
41
42
|
shock: {
|
|
42
43
|
duration: 1.2,
|
|
@@ -46,7 +47,8 @@ const REACTIONS = {
|
|
|
46
47
|
gaze: {
|
|
47
48
|
x: 0,
|
|
48
49
|
y: -.02
|
|
49
|
-
}
|
|
50
|
+
},
|
|
51
|
+
earTilt: .06
|
|
50
52
|
},
|
|
51
53
|
frustrated: {
|
|
52
54
|
duration: 1.5,
|
|
@@ -56,7 +58,8 @@ const REACTIONS = {
|
|
|
56
58
|
gaze: {
|
|
57
59
|
x: 0,
|
|
58
60
|
y: .02
|
|
59
|
-
}
|
|
61
|
+
},
|
|
62
|
+
earTilt: .62
|
|
60
63
|
}
|
|
61
64
|
};
|
|
62
65
|
/** happy 前段先睁大双眼,随后才切换弯眼(占表情时长比例) */
|
|
@@ -69,14 +72,25 @@ const COLORS = {
|
|
|
69
72
|
body: "#3d9bf0",
|
|
70
73
|
face: "#ffffff"
|
|
71
74
|
};
|
|
72
|
-
/** 球体横竖比:扁椭圆(mochi
|
|
75
|
+
/** 球体横竖比:扁椭圆(mochi 感,明显压扁),呼吸在此之上微调 */
|
|
73
76
|
const BODY_SCALE_X = 1.1;
|
|
74
|
-
const BODY_SCALE_Y = .
|
|
77
|
+
const BODY_SCALE_Y = .84;
|
|
75
78
|
/** 竖椭圆大眼睛(单位球空间):竖长才像眼睛,正圆容易读成鼻孔 */
|
|
76
79
|
const EYE_X = .27;
|
|
77
80
|
const EYE_Y = -.12;
|
|
78
81
|
const EYE_RX = .13;
|
|
79
82
|
const EYE_RY = .19;
|
|
83
|
+
/**
|
|
84
|
+
* 耳朵(单位球空间):圆角尖三角(猫耳),根部埋在球体内(先于球体路径绘制,被球遮住),
|
|
85
|
+
* 只露出耳尖;基础外倒角,表情时竖起(角小)或耷拉(角大)
|
|
86
|
+
*/
|
|
87
|
+
const EAR_X = .44;
|
|
88
|
+
const EAR_Y = -.8;
|
|
89
|
+
/** 根部半宽 */
|
|
90
|
+
const EAR_W = .17;
|
|
91
|
+
/** 耳高(根部到耳尖) */
|
|
92
|
+
const EAR_H = .46;
|
|
93
|
+
const EAR_TILT = .38;
|
|
80
94
|
/** 组合圆路径的点数(越大越平滑,36 在小尺寸下已足够) */
|
|
81
95
|
const SEGMENTS = 36;
|
|
82
96
|
function clamp(value, min, max) {
|
|
@@ -99,6 +113,7 @@ function reactionEnvelope(p) {
|
|
|
99
113
|
* 灵动感的核心在眼部状态机:
|
|
100
114
|
* - 白色大眼睛随机眨眼(约 18% 概率双眨),闭起为白色线条
|
|
101
115
|
* - 视线游移 / 转头(面部整体平移 + 轻微倾斜,idle 随机、thinking 缓慢扫视)
|
|
116
|
+
* - thinking 时在球体右上角绘制与身体同色的「?」,轻晃
|
|
102
117
|
* - 瞬时表情({@link AiOrbReaction}):睁大眼、弯眼笑、紧闭眼 ><,配合点头 / 摇头 / 后仰
|
|
103
118
|
*
|
|
104
119
|
* 性能设计:
|
|
@@ -140,6 +155,42 @@ function createOrbRenderer(canvas, options) {
|
|
|
140
155
|
let pointerPos = null;
|
|
141
156
|
/** Q 弹起始时间,-1 表示未在回弹 */
|
|
142
157
|
let pokeStart = -1;
|
|
158
|
+
/** thinking 右上角「?」显隐(0-1),随状态平滑过渡 */
|
|
159
|
+
let thinkingMark = statusLabel === "thinking" ? 1 : 0;
|
|
160
|
+
/** 猫耳轮廓:两侧微鼓、耳尖带小圆角、根部微弧朝下埋进球体 */
|
|
161
|
+
function earPath(w, h) {
|
|
162
|
+
if (!ctx) return;
|
|
163
|
+
ctx.beginPath();
|
|
164
|
+
ctx.moveTo(-w, 0);
|
|
165
|
+
ctx.quadraticCurveTo(-w * .92, -h * .68, -.028, -h * .92);
|
|
166
|
+
ctx.quadraticCurveTo(0, -h, .028, -h * .92);
|
|
167
|
+
ctx.quadraticCurveTo(w * .92, -h * .68, w, 0);
|
|
168
|
+
ctx.quadraticCurveTo(0, .09, -w, 0);
|
|
169
|
+
ctx.closePath();
|
|
170
|
+
}
|
|
171
|
+
/** 耳朵:身体色尖耳 + 白色内耳,平时随时间轻微错相摆动 */
|
|
172
|
+
function drawEars(tilt) {
|
|
173
|
+
if (!ctx) return;
|
|
174
|
+
for (const side of [-1, 1]) {
|
|
175
|
+
const swing = .05 * Math.sin(t * 1.9 + side * 1.2);
|
|
176
|
+
ctx.save();
|
|
177
|
+
ctx.translate(side * EAR_X, EAR_Y);
|
|
178
|
+
ctx.rotate(side * (tilt + swing));
|
|
179
|
+
ctx.fillStyle = COLORS.body;
|
|
180
|
+
earPath(EAR_W, EAR_H);
|
|
181
|
+
ctx.fill();
|
|
182
|
+
ctx.fillStyle = COLORS.face;
|
|
183
|
+
const iw = EAR_W * .48;
|
|
184
|
+
ctx.beginPath();
|
|
185
|
+
ctx.moveTo(-.0816, -.46 * .1);
|
|
186
|
+
ctx.quadraticCurveTo(-.07344, -.2024, 0, -.276);
|
|
187
|
+
ctx.quadraticCurveTo(iw * .9, -.2024, iw, -.46 * .1);
|
|
188
|
+
ctx.quadraticCurveTo(0, -.0092, -.0816, -.46 * .1);
|
|
189
|
+
ctx.closePath();
|
|
190
|
+
ctx.fill();
|
|
191
|
+
ctx.restore();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
143
194
|
/** 面部:白色大眼睛(圆眼 / 弯眼 / 紧闭线条)+ 嘴 */
|
|
144
195
|
function drawFace(eyeMode, eyeOpen, eyeScale, mouth) {
|
|
145
196
|
if (!ctx) return;
|
|
@@ -175,6 +226,26 @@ function createOrbRenderer(canvas, options) {
|
|
|
175
226
|
}
|
|
176
227
|
drawMouth(mouth);
|
|
177
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* thinking 吉祥物右上角的「?」:球体同色,轻晃更像在想问题。
|
|
231
|
+
* 画在单位球变换之外,避免被扁椭圆压扁。
|
|
232
|
+
*/
|
|
233
|
+
function drawThinkingMark(weight) {
|
|
234
|
+
if (!ctx || weight <= .01) return;
|
|
235
|
+
const fontSize = size * .3;
|
|
236
|
+
const x = size * .93;
|
|
237
|
+
const y = size * .24 + size * .02 * Math.sin(t * 3.6);
|
|
238
|
+
ctx.save();
|
|
239
|
+
ctx.globalAlpha = weight;
|
|
240
|
+
ctx.translate(x, y);
|
|
241
|
+
ctx.rotate(-.16 + .1 * Math.sin(t * 2.7));
|
|
242
|
+
ctx.fillStyle = COLORS.body;
|
|
243
|
+
ctx.font = `700 ${fontSize}px ui-rounded, system-ui, sans-serif`;
|
|
244
|
+
ctx.textAlign = "center";
|
|
245
|
+
ctx.textBaseline = "middle";
|
|
246
|
+
ctx.fillText("?", 0, 0);
|
|
247
|
+
ctx.restore();
|
|
248
|
+
}
|
|
178
249
|
function drawMouth(mouth) {
|
|
179
250
|
if (!ctx || mouth === "none") return;
|
|
180
251
|
ctx.fillStyle = COLORS.face;
|
|
@@ -281,6 +352,9 @@ function createOrbRenderer(canvas, options) {
|
|
|
281
352
|
const gazeEase = 1 - Math.exp(-frameDt * 6.5);
|
|
282
353
|
gazeCur.x += (gazeTarget.x - gazeCur.x) * gazeEase;
|
|
283
354
|
gazeCur.y += (gazeTarget.y - gazeCur.y) * gazeEase;
|
|
355
|
+
const markTarget = statusLabel === "thinking" ? 1 : 0;
|
|
356
|
+
if (isRunning) thinkingMark += (markTarget - thinkingMark) * ease;
|
|
357
|
+
else thinkingMark = markTarget;
|
|
284
358
|
let eyeOpen = current.eyeOpen * blinkOpen;
|
|
285
359
|
let eyeScale = 1;
|
|
286
360
|
let eyeMode = "round";
|
|
@@ -297,9 +371,13 @@ function createOrbRenderer(canvas, options) {
|
|
|
297
371
|
let bodyDy = 0;
|
|
298
372
|
let bodyRot = gazeCur.x * .5;
|
|
299
373
|
let bodyScale = 1;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
374
|
+
let earTilt = EAR_TILT;
|
|
375
|
+
if (reactionDef && reaction && reactionW > .01) {
|
|
376
|
+
earTilt = lerp(EAR_TILT, reactionDef.earTilt, reactionW);
|
|
377
|
+
if (reaction.type === "happy") bodyDy = Math.sin(Math.min(reactionP / .75, 1) * Math.PI) * .05;
|
|
378
|
+
else if (reaction.type === "frustrated") bodyRot += Math.sin(reactionP * reactionDef.duration * 18) * .05 * (1 - reactionP);
|
|
379
|
+
else bodyScale = 1 - .05 * Math.sin(Math.min(reactionP * 1.6, 1) * Math.PI);
|
|
380
|
+
}
|
|
303
381
|
let jellyX = 1;
|
|
304
382
|
let jellyY = 1;
|
|
305
383
|
if (pokeStart >= 0) {
|
|
@@ -323,6 +401,7 @@ function createOrbRenderer(canvas, options) {
|
|
|
323
401
|
ctx.translate(cx, cy);
|
|
324
402
|
ctx.rotate(bodyRot);
|
|
325
403
|
ctx.scale(radius * scaleX, radius * scaleY);
|
|
404
|
+
drawEars(earTilt);
|
|
326
405
|
ctx.beginPath();
|
|
327
406
|
for (let i = 0; i <= SEGMENTS; i++) {
|
|
328
407
|
const theta = i / SEGMENTS * Math.PI * 2;
|
|
@@ -339,6 +418,7 @@ function createOrbRenderer(canvas, options) {
|
|
|
339
418
|
ctx.rotate(gazeCur.x * .35);
|
|
340
419
|
drawFace(eyeMode, eyeOpen, eyeScale, mouth);
|
|
341
420
|
ctx.restore();
|
|
421
|
+
drawThinkingMark(thinkingMark);
|
|
342
422
|
}
|
|
343
423
|
function tick(now) {
|
|
344
424
|
if (!isRunning) return;
|