@veltra/ai 2.1.5 → 2.1.7

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 (74) hide show
  1. package/README.md +42 -0
  2. package/dist/chat/fold.d.ts +41 -0
  3. package/dist/chat/fold.js +208 -0
  4. package/dist/chat/fold.js.map +1 -0
  5. package/dist/chat/index.d.ts +4 -2
  6. package/dist/chat/session.d.ts +123 -0
  7. package/dist/chat/session.js +79 -0
  8. package/dist/chat/session.js.map +1 -0
  9. package/dist/chat/types.d.ts +38 -1
  10. package/dist/chat/use-chat-helpers.js +39 -0
  11. package/dist/chat/use-chat-helpers.js.map +1 -0
  12. package/dist/chat/use-chat-session.js +68 -0
  13. package/dist/chat/use-chat-session.js.map +1 -0
  14. package/dist/chat/use-chat.d.ts +50 -2
  15. package/dist/chat/use-chat.js +73 -39
  16. package/dist/chat/use-chat.js.map +1 -1
  17. package/dist/components/ai-chat/ai-chat.js +49 -14
  18. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  19. package/dist/components/ai-chat/approval-banner.js +54 -0
  20. package/dist/components/ai-chat/approval-banner.js.map +1 -0
  21. package/dist/components/ai-chat/ask-question.js +32 -51
  22. package/dist/components/ai-chat/ask-question.js.map +1 -1
  23. package/dist/components/ai-chat/chat-input.js +9 -3
  24. package/dist/components/ai-chat/chat-input.js.map +1 -1
  25. package/dist/components/ai-chat/di.js.map +1 -1
  26. package/dist/components/ai-chat/job-bar.js +48 -0
  27. package/dist/components/ai-chat/job-bar.js.map +1 -0
  28. package/dist/components/ai-chat/message-list.js +5 -2
  29. package/dist/components/ai-chat/message-list.js.map +1 -1
  30. package/dist/components/ai-chat/queue-list.js +9 -3
  31. package/dist/components/ai-chat/queue-list.js.map +1 -1
  32. package/dist/components/ai-chat/side-panel.js +12 -30
  33. package/dist/components/ai-chat/side-panel.js.map +1 -1
  34. package/dist/components/ai-chat/style2.css +150 -8
  35. package/dist/components/ai-chat/tool-call.js +51 -40
  36. package/dist/components/ai-chat/tool-call.js.map +1 -1
  37. package/dist/components/ai-chat/tool-icons.js +31 -0
  38. package/dist/components/ai-chat/tool-icons.js.map +1 -0
  39. package/dist/components/ai-chat/tool-json.js +40 -0
  40. package/dist/components/ai-chat/tool-json.js.map +1 -0
  41. package/dist/components/ai-orb/style2.css +1 -1
  42. package/dist/index.d.ts +6 -4
  43. package/dist/index.js +3 -1
  44. package/dist/tools/ask-question/ask-question.js +48 -2
  45. package/dist/tools/ask-question/ask-question.js.map +1 -1
  46. package/dist/types/ai-chat.d.ts +17 -6
  47. package/package.json +7 -7
  48. package/src/chat/__test__/fold.test.ts +136 -0
  49. package/src/chat/__test__/session.test.ts +191 -0
  50. package/src/chat/__test__/use-chat.test.ts +268 -1
  51. package/src/chat/fold.ts +260 -0
  52. package/src/chat/index.ts +2 -0
  53. package/src/chat/session.ts +168 -0
  54. package/src/chat/types.ts +40 -0
  55. package/src/chat/use-chat-helpers.ts +39 -0
  56. package/src/chat/use-chat-session.ts +87 -0
  57. package/src/chat/use-chat.ts +90 -44
  58. package/src/components/ai-chat/__test__/ai-chat.test.ts +405 -7
  59. package/src/components/ai-chat/__test__/tool-icons.test.ts +33 -0
  60. package/src/components/ai-chat/ai-chat.vue +39 -4
  61. package/src/components/ai-chat/approval-banner.vue +37 -0
  62. package/src/components/ai-chat/ask-question.vue +38 -54
  63. package/src/components/ai-chat/chat-input.vue +10 -2
  64. package/src/components/ai-chat/di.ts +6 -4
  65. package/src/components/ai-chat/job-bar.vue +41 -0
  66. package/src/components/ai-chat/message-list.vue +4 -1
  67. package/src/components/ai-chat/queue-list.vue +3 -1
  68. package/src/components/ai-chat/side-panel.vue +15 -13
  69. package/src/components/ai-chat/style.scss +162 -6
  70. package/src/components/ai-chat/tool-call.vue +46 -34
  71. package/src/components/ai-chat/tool-icons.ts +33 -0
  72. package/src/components/ai-chat/tool-json.vue +52 -0
  73. package/src/tools/ask-question/ask-question.ts +53 -4
  74. package/src/types/ai-chat.ts +17 -5
@@ -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 <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 // 清除会话后列表已空:跳过结束停留,立刻回到欢迎区\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":";;;;;;;;;AAsHA,MAAM,WAAW;;AAEjB,MAAM,oBAAoB;;AAE1B,MAAM,UAAU;;;;;;;;;;;;;;;;;EAEhB,MAAM,QAAQ;EAUd,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,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;IAChB,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;GAGX,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,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;EAClB,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;;GA5crD,OAAA,UAAA,GAAA,mBAwFM,OAAA,EAxFA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,WAAA,CAAA,EAAA,GAAA;IAChB,YAmDU,MAAA,OAAA,GAAA;KAlDP,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,MAAA,CAAA;KACT,SAAA;KAAJ,KAAI;KACJ,QAAA;KACC,UAAQ;KACO,gBAAA;;KAEN,SAAA,cAAqB;OAA/B,UAAA,IAAA,GAAA,mBAqCW,UAAA,MAAA,WArCc,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,YAOE,aAAA;;SALC,SAAS,KAAK;SACd,WAAS,KAAK;SACd,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,mBAGM,OAAA;;OAHmB,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;MACvC,GAAA,MAAA,GAAA,CAAA,QAAA,CAAA,GAAA,QAAA,WAAZ,UAAA,GAAA,mBAA4E,QAAA;;OAAtD,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,GAAA,SAAA,CAAA;MAA8B,GAAA,QAAI,CAAA,KAAA,mBAAA,QAAA,IAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA;;;;IAIzE,mBAAA,wCAAA;IAEQ,gBAAA,SADR,UAAA,GAAA,mBAwBM,OAAA;;KAtBH,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,WAmBO,KAAA,QAAA,WAAA,CAAA,SAAA,CAlBL,mBAiBM,OAAA,EAjBA,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,mBAYM,OAAA;;MAZ2B,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA;KAC3C,GAAA,CAAA,YAUa,YAAA;MAVA,MAAM,MAAA,GAAA,CAAG,CAAC,GAAE,WAAA,MAAA;MAAqB,MAAK;;MACjD,SAAA,cAQS,CAPD,mBAAA,SADR,UAAA,GAAA,mBAQS,UAAA;OANN,KAAK,aAAA;OACN,MAAK;OACJ,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;OACZ,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,iBAAkB,mBAAA,KAAkB;MAE7C,GAAA,gBAAA,mBAAA,KAAkB,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,CAAA,CAAA;;;;IAQjC,mBAAA,wBAAA;IACA,YAKa,YAAA,EALA,MAAM,MAAA,GAAA,CAAG,CAAC,GAAE,aAAA,MAAA,EAAA,GAAA;KACvB,SAAA,cAGS,CAHK,aAAA,SAAd,UAAA,GAAA,mBAGS,UAAA;;MAHmB,MAAK;MAAU,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,WAAA,CAAA;MAAgB,SAAO;KAC5E,GAAA,CAAA,YAA4B,MAAA,KAAA,GAAA,MAAA;MAArB,SAAA,cAAa,CAAb,YAAa,MAAA,SAAA,CAAA,CAAA,CAAA;;KACpB,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,mBAAiB,QAAA,MAAX,QAAI,EAAA,EAAA,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,CAAA,CAAA"}
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 :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 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 /** 只读时欢迎语不可点击发送 */\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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"}
@@ -11,7 +11,10 @@ const _hoisted_4 = ["onClick"];
11
11
  const _sfc_main = /*@__PURE__*/ defineComponent({
12
12
  name: "UAiChatQueueList",
13
13
  __name: "queue-list",
14
- props: { queue: {} },
14
+ props: {
15
+ queue: {},
16
+ readonly: { type: Boolean }
17
+ },
15
18
  emits: [
16
19
  "start-now",
17
20
  "edit",
@@ -31,7 +34,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
31
34
  }, [createElementVNode("span", {
32
35
  class: normalizeClass(unref(cls).e("queue-text")),
33
36
  title: item.content
34
- }, toDisplayString(item.content), 11, _hoisted_1), createElementVNode("span", { class: normalizeClass(unref(cls).e("queue-actions")) }, [
37
+ }, toDisplayString(item.content), 11, _hoisted_1), !__props.readonly ? (openBlock(), createElementBlock("span", {
38
+ key: 0,
39
+ class: normalizeClass(unref(cls).e("queue-actions"))
40
+ }, [
35
41
  createElementVNode("button", {
36
42
  type: "button",
37
43
  class: normalizeClass([unref(cls).e("queue-action"), unref(cls).em("queue-action", "primary")]),
@@ -59,7 +65,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
59
65
  default: withCtx(() => [createVNode(unref(Close))]),
60
66
  _: 1
61
67
  })], 10, _hoisted_4)
62
- ], 2)], 2);
68
+ ], 2)) : createCommentVNode("v-if", true)], 2);
63
69
  }), 128))], 2)) : createCommentVNode("v-if", true);
64
70
  };
65
71
  }
@@ -1 +1 @@
1
- {"version":3,"file":"queue-list.js","names":[],"sources":["../../../src/components/ai-chat/queue-list.vue"],"sourcesContent":["<template>\n <div v-if=\"queue.length\" :class=\"cls.e('queue')\">\n <div :class=\"cls.e('queue-head')\">待发送队列 · {{ queue.length }}</div>\n <div v-for=\"item in queue\" :key=\"item.id\" :class=\"cls.e('queue-item')\">\n <span :class=\"cls.e('queue-text')\" :title=\"item.content\">{{ item.content }}</span>\n <span :class=\"cls.e('queue-actions')\">\n <button\n type=\"button\"\n :class=\"[cls.e('queue-action'), cls.em('queue-action', 'primary')]\"\n title=\"中断当前会话,立即执行该条\"\n @click=\"emit('start-now', item.id)\"\n >\n <UIcon><CaretRight /></UIcon>立即开始\n </button>\n <button\n type=\"button\"\n :class=\"cls.e('queue-action')\"\n title=\"取回输入框编辑\"\n @click=\"emit('edit', item.id)\"\n >\n <UIcon><EditPen /></UIcon>\n </button>\n <button\n type=\"button\"\n :class=\"cls.e('queue-action')\"\n title=\"移出队列\"\n @click=\"emit('remove', item.id)\"\n >\n <UIcon><Close /></UIcon>\n </button>\n </span>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon } from '@veltra/desktop'\nimport { CaretRight, Close, EditPen } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { inject } from 'vue'\n\nimport type { ChatQueuedMessage } from '../../chat/types'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatQueueList' })\n\ndefineProps<{\n /** 待发送队列(按执行顺序排列) */\n queue: ChatQueuedMessage[]\n}>()\n\nconst emit = defineEmits<{\n /** 立即开始:中断当前会话并插队执行 */\n (e: 'start-now', id: string): void\n /** 编辑:取回输入框重新编辑 */\n (e: 'edit', id: string): void\n /** 移出队列 */\n (e: 'remove', id: string): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;EAmDA,MAAM,OAAO;EAUb,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;;GA5DvB,OAAA,QAAA,MAAM,UAAjB,UAAA,GAAA,mBA+BM,OAAA;;IA/BoB,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,OAAA,CAAA;GACpC,GAAA,CAAA,mBAAkE,OAAA,EAA5D,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA,EAAgB,GAAA,aAAQ,gBAAG,QAAA,MAAM,MAAM,GAAA,CAAA,IACzD,UAAA,IAAA,GAAA,mBA4BM,UAAA,MAAA,WA5Bc,QAAA,QAAR,SAAI;IAAhB,OAAA,UAAA,GAAA,mBA4BM,OAAA;KA5BsB,KAAK,KAAK;KAAK,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA;IACrD,GAAA,CAAA,mBAAkF,QAAA;KAA3E,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA;KAAiB,OAAO,KAAK;IAAY,GAAA,gBAAA,KAAK,OAAO,GAAA,IAAA,UAAA,GACxE,mBAyBO,QAAA,EAzBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,eAAA,CAAA,EAAA,GAAA;KACjB,mBAOS,UAAA;MANP,MAAK;MACJ,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,GAAkB,MAAA,GAAA,CAAG,CAAC,GAAE,gBAAA,SAAA,CAAA,CAAA;MACtC,OAAM;MACL,UAAK,WAAE,KAAI,aAAc,KAAK,EAAE;KAEjC,GAAA,CAAA,YAA6B,MAAA,KAAA,GAAA,MAAA;MAAtB,SAAA,cAAc,CAAd,YAAc,MAAA,UAAA,CAAA,CAAA,CAAA;;KAAQ,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,gBAAA,SAC/B,EAAA,EAAA,GAAA,IAAA,UAAA;KACA,mBAOS,UAAA;MANP,MAAK;MACJ,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;MACb,OAAM;MACL,UAAK,WAAE,KAAI,QAAS,KAAK,EAAE;KAE5B,GAAA,CAAA,YAA0B,MAAA,KAAA,GAAA,MAAA;MAAnB,SAAA,cAAW,CAAX,YAAW,MAAA,OAAA,CAAA,CAAA,CAAA;;;KAEpB,mBAOS,UAAA;MANP,MAAK;MACJ,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;MACb,OAAM;MACL,UAAK,WAAE,KAAI,UAAW,KAAK,EAAE;KAE9B,GAAA,CAAA,YAAwB,MAAA,KAAA,GAAA,MAAA;MAAjB,SAAA,cAAS,CAAT,YAAS,MAAA,KAAA,CAAA,CAAA,CAAA"}
1
+ {"version":3,"file":"queue-list.js","names":[],"sources":["../../../src/components/ai-chat/queue-list.vue"],"sourcesContent":["<template>\n <div v-if=\"queue.length\" :class=\"cls.e('queue')\">\n <div :class=\"cls.e('queue-head')\">待发送队列 · {{ queue.length }}</div>\n <div v-for=\"item in queue\" :key=\"item.id\" :class=\"cls.e('queue-item')\">\n <span :class=\"cls.e('queue-text')\" :title=\"item.content\">{{ item.content }}</span>\n <span v-if=\"!readonly\" :class=\"cls.e('queue-actions')\">\n <button\n type=\"button\"\n :class=\"[cls.e('queue-action'), cls.em('queue-action', 'primary')]\"\n title=\"中断当前会话,立即执行该条\"\n @click=\"emit('start-now', item.id)\"\n >\n <UIcon><CaretRight /></UIcon>立即开始\n </button>\n <button\n type=\"button\"\n :class=\"cls.e('queue-action')\"\n title=\"取回输入框编辑\"\n @click=\"emit('edit', item.id)\"\n >\n <UIcon><EditPen /></UIcon>\n </button>\n <button\n type=\"button\"\n :class=\"cls.e('queue-action')\"\n title=\"移出队列\"\n @click=\"emit('remove', item.id)\"\n >\n <UIcon><Close /></UIcon>\n </button>\n </span>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon } from '@veltra/desktop'\nimport { CaretRight, Close, EditPen } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { inject } from 'vue'\n\nimport type { ChatQueuedMessage } from '../../chat/types'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatQueueList' })\n\ndefineProps<{\n /** 待发送队列(按执行顺序排列) */\n queue: ChatQueuedMessage[]\n /** 只读时不展示插队 / 编辑 / 移除 */\n readonly?: boolean\n}>()\n\nconst emit = defineEmits<{\n /** 立即开始:中断当前会话并插队执行 */\n (e: 'start-now', id: string): void\n /** 编辑:取回输入框重新编辑 */\n (e: 'edit', id: string): void\n /** 移出队列 */\n (e: 'remove', id: string): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;EAqDA,MAAM,OAAO;EAUb,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;;GA9DvB,OAAA,QAAA,MAAM,UAAjB,UAAA,GAAA,mBA+BM,OAAA;;IA/BoB,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,OAAA,CAAA;GACpC,GAAA,CAAA,mBAAkE,OAAA,EAA5D,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA,EAAgB,GAAA,aAAQ,gBAAG,QAAA,MAAM,MAAM,GAAA,CAAA,IACzD,UAAA,IAAA,GAAA,mBA4BM,UAAA,MAAA,WA5Bc,QAAA,QAAR,SAAI;IAAhB,OAAA,UAAA,GAAA,mBA4BM,OAAA;KA5BsB,KAAK,KAAK;KAAK,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA;IACrD,GAAA,CAAA,mBAAkF,QAAA;KAA3E,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA;KAAiB,OAAO,KAAK;IAAY,GAAA,gBAAA,KAAK,OAAO,GAAA,IAAA,UAAA,GAC3D,CAAA,QAAA,YAAb,UAAA,GAAA,mBAyBO,QAAA;;KAzBiB,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,eAAA,CAAA;;KAClC,mBAOS,UAAA;MANP,MAAK;MACJ,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,GAAkB,MAAA,GAAA,CAAG,CAAC,GAAE,gBAAA,SAAA,CAAA,CAAA;MACtC,OAAM;MACL,UAAK,WAAE,KAAI,aAAc,KAAK,EAAE;KAEjC,GAAA,CAAA,YAA6B,MAAA,KAAA,GAAA,MAAA;MAAtB,SAAA,cAAc,CAAd,YAAc,MAAA,UAAA,CAAA,CAAA,CAAA;;KAAQ,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,gBAAA,SAC/B,EAAA,EAAA,GAAA,IAAA,UAAA;KACA,mBAOS,UAAA;MANP,MAAK;MACJ,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;MACb,OAAM;MACL,UAAK,WAAE,KAAI,QAAS,KAAK,EAAE;KAE5B,GAAA,CAAA,YAA0B,MAAA,KAAA,GAAA,MAAA;MAAnB,SAAA,cAAW,CAAX,YAAW,MAAA,OAAA,CAAA,CAAA,CAAA;;;KAEpB,mBAOS,UAAA;MANP,MAAK;MACJ,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;MACb,OAAM;MACL,UAAK,WAAE,KAAI,UAAW,KAAK,EAAE;KAE9B,GAAA,CAAA,YAAwB,MAAA,KAAA,GAAA,MAAA;MAAjB,SAAA,cAAS,CAAT,YAAS,MAAA,KAAA,CAAA,CAAA,CAAA"}
@@ -1,6 +1,7 @@
1
1
  import { AiChatDIKey } from "./di.js";
2
+ import { resolveToolIcon } from "./tool-icons.js";
2
3
  import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, resolveDynamicComponent, toDisplayString, unref, withCtx } from "vue";
3
- import { CircleCheckFilled, CircleClose, Close, Loading, WarningFilled } from "@veltra/icons/normal";
4
+ import { Close } from "@veltra/icons/normal";
4
5
  import { UIcon, UScroll } from "@veltra/desktop";
5
6
  import { bem } from "@veltra/utils";
6
7
  //#region src/components/ai-chat/side-panel.vue
@@ -16,36 +17,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
16
17
  const cls = di?.cls ?? bem("ai-chat");
17
18
  /** 当前调用对应的工具定义(解析 icon/label/render) */
18
19
  const tool = computed(() => di?.tools.value[props.toolCall.name]);
19
- const STATUS_META = {
20
- pending: {
21
- icon: Loading,
22
- class: "is-active"
23
- },
24
- running: {
25
- icon: Loading,
26
- class: "is-active"
27
- },
28
- "awaiting-confirm": {
29
- icon: WarningFilled,
30
- class: "is-warning"
31
- },
32
- success: {
33
- icon: CircleCheckFilled,
34
- class: "is-success"
35
- },
36
- error: {
37
- icon: CircleClose,
38
- class: "is-danger"
39
- },
40
- rejected: {
41
- icon: Close,
42
- class: "is-danger"
43
- }
20
+ const STATUS_CLASS = {
21
+ pending: "is-active",
22
+ running: "is-active",
23
+ "awaiting-confirm": "is-warning",
24
+ success: "is-success",
25
+ error: "is-danger",
26
+ rejected: "is-danger"
44
27
  };
45
- const statusIcon = computed(() => STATUS_META[props.toolCall.status]?.icon ?? Loading);
46
- const statusClass = computed(() => STATUS_META[props.toolCall.status]?.class ?? "");
47
- /** 头部图标:工具自定义图标优先,缺省用状态图标(与工具卡片一致) */
48
- const headerIcon = computed(() => tool.value?.icon ?? statusIcon.value);
28
+ const statusClass = computed(() => STATUS_CLASS[props.toolCall.status] ?? "");
29
+ /** 头部图标:meta.icon > 宿主覆盖 > 内置名称规则 > 兜底;状态点走 status class */
30
+ const headerIcon = computed(() => tool.value?.icon ?? resolveToolIcon(props.toolCall.name, di?.toolIcons?.value));
49
31
  /** 面板标题:优先工具定义的 panelTitle(业务对象 + 动作),缺省回落到 label ?? name */
50
32
  const panelTitle = computed(() => {
51
33
  const t = tool.value;
@@ -1 +1 @@
1
- {"version":3,"file":"side-panel.js","names":[],"sources":["../../../src/components/ai-chat/side-panel.vue"],"sourcesContent":["<template>\n <aside :class=\"cls.e('panel')\">\n <div :class=\"cls.e('panel-card')\">\n <div :class=\"cls.e('panel-header')\">\n <UIcon :class=\"[cls.e('panel-icon'), statusClass]\">\n <component :is=\"headerIcon\" />\n </UIcon>\n <span :class=\"cls.e('panel-title')\">{{ panelTitle }}</span>\n <UIcon :class=\"cls.e('panel-close')\" title=\"关闭面板\" @click=\"emit('close')\">\n <Close />\n </UIcon>\n </div>\n\n <UScroll :class=\"cls.e('panel-body')\">\n <!-- render 组件与工具卡片同一契约(ChatToolRenderProps),可随 toolCall 状态实时更新 -->\n <component :is=\"tool.render\" v-if=\"tool?.render\" :tool-call=\"toolCall\" />\n <pre v-else :class=\"cls.e('panel-fallback')\">{{ fallbackResult }}</pre>\n </UScroll>\n </div>\n </aside>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon, UScroll } from '@veltra/desktop'\nimport { CircleCheckFilled, CircleClose, Close, Loading, WarningFilled } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, type Component } from 'vue'\n\nimport type { ChatToolCall } from '../../chat/types'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatSidePanel' })\n\nconst props = defineProps<{ toolCall: ChatToolCall }>()\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 当前调用对应的工具定义(解析 icon/label/render) */\nconst tool = computed(() => di?.tools.value[props.toolCall.name])\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/** 头部图标:工具自定义图标优先,缺省用状态图标(与工具卡片一致) */\nconst headerIcon = computed(() => tool.value?.icon ?? statusIcon.value)\n\n/** 面板标题:优先工具定义的 panelTitle(业务对象 + 动作),缺省回落到 label ?? name */\nconst panelTitle = computed(() => {\n const t = tool.value\n const pt = t?.panelTitle\n if (typeof pt === 'function') return pt(props.toolCall)\n return pt ?? t?.label ?? props.toolCall.name\n})\n\n/** 未提供 render 时的兜底展示(面板工具通常都应提供 render) */\nconst fallbackResult = 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</script>\n"],"mappings":";;;;;;;;;;;;EAiCA,MAAM,QAAQ;EAEd,MAAM,OAAO;EAEb,MAAM,KAAK,OAAO,WAAW;EAC7B,MAAM,MAAM,IAAI,OAAO,IAAI,SAAS;;EAGpC,MAAM,OAAO,eAAe,IAAI,MAAM,MAAM,MAAM,SAAS,KAAK;EAEhE,MAAM,cAAkE;GACtE,SAAS;IAAE,MAAM;IAAS,OAAO;GAAY;GAC7C,SAAS;IAAE,MAAM;IAAS,OAAO;GAAY;GAC7C,oBAAoB;IAAE,MAAM;IAAe,OAAO;GAAa;GAC/D,SAAS;IAAE,MAAM;IAAmB,OAAO;GAAa;GACxD,OAAO;IAAE,MAAM;IAAa,OAAO;GAAY;GAC/C,UAAU;IAAE,MAAM;IAAO,OAAO;GAAY;EAC9C;EAEA,MAAM,aAAa,eAAe,YAAY,MAAM,SAAS,OAAO,EAAE,QAAQ,OAAO;EACrF,MAAM,cAAc,eAAe,YAAY,MAAM,SAAS,OAAO,EAAE,SAAS,EAAE;;EAGlF,MAAM,aAAa,eAAe,KAAK,OAAO,QAAQ,WAAW,KAAK;;EAGtE,MAAM,aAAa,eAAe;GAChC,MAAM,IAAI,KAAK;GACf,MAAM,KAAK,GAAG;GACd,IAAI,OAAO,OAAO,YAAY,OAAO,GAAG,MAAM,QAAQ;GACtD,OAAO,MAAM,GAAG,SAAS,MAAM,SAAS;EAC1C,CAAC;;EAGD,MAAM,iBAAiB,eAAe;GACpC,MAAM,MAAM,MAAM,SAAS,SAAS,MAAM,SAAS,UAAU;GAC7D,IAAI;IACF,OAAO,KAAK,UAAU,KAAK,MAAM,GAAG,GAAG,MAAM,CAAC;GAChD,QAAQ;IACN,OAAO;GACT;EACF,CAAC;;GAzEC,OAAA,UAAA,GAAA,mBAkBQ,SAAA,EAlBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,OAAA,CAAA,EAAA,GAClB,CAAA,mBAgBM,OAAA,EAhBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA,EAAA,GAChB,CAAA,mBAQM,OAAA,EARA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA,EAAA,GAAA;IAChB,YAEQ,MAAA,KAAA,GAAA,EAFA,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,GAAgB,YAAA,KAAW,CAAA,EAAA,GAAA;KAC9C,SAAA,cAA8B,EAA9B,UAAA,GAAA,YAA8B,wBAAd,WAAA,KAAU,CAAA,EAAA,CAAA;;;IAE5B,mBAA2D,QAAA,EAApD,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,aAAA,CAAA,EAAoB,GAAA,gBAAA,WAAA,KAAU,GAAA,CAAA;IACjD,YAEQ,MAAA,KAAA,GAAA;KAFA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,aAAA,CAAA;KAAiB,OAAM;KAAQ,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,OAAA;;KAC5D,SAAA,cAAS,CAAT,YAAS,MAAA,KAAA,CAAA,CAAA,CAAA;;;GAIb,GAAA,CAAA,GAAA,YAIU,MAAA,OAAA,GAAA,EAJA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA,EAAA,GAAA;IACpB,SAAA,cAAmE,CAAnE,mBAAA,8DAAA,GACmC,KAAA,OAAM,UAAzC,UAAA,GAAA,YAAyE,wBAAzD,KAAA,MAAK,MAAM,GAAA;;KAAuB,aAAW,QAAA;IAC7D,GAAA,MAAA,GAAA,CAAA,WAAA,CAAA,MAAA,UAAA,GAAA,mBAAuE,OAAA;;KAA1D,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,gBAAA,CAAA;IAAuB,GAAA,gBAAA,eAAA,KAAc,GAAA,CAAA,EAAA,CAAA"}
1
+ {"version":3,"file":"side-panel.js","names":[],"sources":["../../../src/components/ai-chat/side-panel.vue"],"sourcesContent":["<template>\n <aside :class=\"cls.e('panel')\">\n <div :class=\"cls.e('panel-card')\">\n <div :class=\"cls.e('panel-header')\">\n <UIcon :class=\"[cls.e('panel-icon'), statusClass]\">\n <component :is=\"headerIcon\" />\n </UIcon>\n <span :class=\"cls.e('panel-title')\">{{ panelTitle }}</span>\n <UIcon :class=\"cls.e('panel-close')\" title=\"关闭面板\" @click=\"emit('close')\">\n <Close />\n </UIcon>\n </div>\n\n <UScroll :class=\"cls.e('panel-body')\">\n <!-- render 组件与工具卡片同一契约(ChatToolRenderProps),可随 toolCall 状态实时更新 -->\n <component :is=\"tool.render\" v-if=\"tool?.render\" :tool-call=\"toolCall\" />\n <pre v-else :class=\"cls.e('panel-fallback')\">{{ fallbackResult }}</pre>\n </UScroll>\n </div>\n </aside>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon, UScroll } from '@veltra/desktop'\nimport { Close } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject } from 'vue'\n\nimport type { ChatToolCall } from '../../chat/types'\nimport { AiChatDIKey } from './di'\nimport { resolveToolIcon } from './tool-icons'\n\ndefineOptions({ name: 'UAiChatSidePanel' })\n\nconst props = defineProps<{ toolCall: ChatToolCall }>()\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 当前调用对应的工具定义(解析 icon/label/render) */\nconst tool = computed(() => di?.tools.value[props.toolCall.name])\n\nconst STATUS_CLASS: Record<string, string> = {\n pending: 'is-active',\n running: 'is-active',\n 'awaiting-confirm': 'is-warning',\n success: 'is-success',\n error: 'is-danger',\n rejected: 'is-danger'\n}\n\nconst statusClass = computed(() => STATUS_CLASS[props.toolCall.status] ?? '')\n\n/** 头部图标:meta.icon > 宿主覆盖 > 内置名称规则 > 兜底;状态点走 status class */\nconst headerIcon = computed(\n () => tool.value?.icon ?? resolveToolIcon(props.toolCall.name, di?.toolIcons?.value)\n)\n\n/** 面板标题:优先工具定义的 panelTitle(业务对象 + 动作),缺省回落到 label ?? name */\nconst panelTitle = computed(() => {\n const t = tool.value\n const pt = t?.panelTitle\n if (typeof pt === 'function') return pt(props.toolCall)\n return pt ?? t?.label ?? props.toolCall.name\n})\n\n/** 未提供 render 时的兜底展示(面板工具通常都应提供 render) */\nconst fallbackResult = 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</script>\n"],"mappings":";;;;;;;;;;;;;EAkCA,MAAM,QAAQ;EAEd,MAAM,OAAO;EAEb,MAAM,KAAK,OAAO,WAAW;EAC7B,MAAM,MAAM,IAAI,OAAO,IAAI,SAAS;;EAGpC,MAAM,OAAO,eAAe,IAAI,MAAM,MAAM,MAAM,SAAS,KAAK;EAEhE,MAAM,eAAuC;GAC3C,SAAS;GACT,SAAS;GACT,oBAAoB;GACpB,SAAS;GACT,OAAO;GACP,UAAU;EACZ;EAEA,MAAM,cAAc,eAAe,aAAa,MAAM,SAAS,WAAW,EAAE;;EAG5E,MAAM,aAAa,eACX,KAAK,OAAO,QAAQ,gBAAgB,MAAM,SAAS,MAAM,IAAI,WAAW,KAAK,CACrF;;EAGA,MAAM,aAAa,eAAe;GAChC,MAAM,IAAI,KAAK;GACf,MAAM,KAAK,GAAG;GACd,IAAI,OAAO,OAAO,YAAY,OAAO,GAAG,MAAM,QAAQ;GACtD,OAAO,MAAM,GAAG,SAAS,MAAM,SAAS;EAC1C,CAAC;;EAGD,MAAM,iBAAiB,eAAe;GACpC,MAAM,MAAM,MAAM,SAAS,SAAS,MAAM,SAAS,UAAU;GAC7D,IAAI;IACF,OAAO,KAAK,UAAU,KAAK,MAAM,GAAG,GAAG,MAAM,CAAC;GAChD,QAAQ;IACN,OAAO;GACT;EACF,CAAC;;GA3EC,OAAA,UAAA,GAAA,mBAkBQ,SAAA,EAlBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,OAAA,CAAA,EAAA,GAClB,CAAA,mBAgBM,OAAA,EAhBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA,EAAA,GAChB,CAAA,mBAQM,OAAA,EARA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA,EAAA,GAAA;IAChB,YAEQ,MAAA,KAAA,GAAA,EAFA,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,GAAgB,YAAA,KAAW,CAAA,EAAA,GAAA;KAC9C,SAAA,cAA8B,EAA9B,UAAA,GAAA,YAA8B,wBAAd,WAAA,KAAU,CAAA,EAAA,CAAA;;;IAE5B,mBAA2D,QAAA,EAApD,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,aAAA,CAAA,EAAoB,GAAA,gBAAA,WAAA,KAAU,GAAA,CAAA;IACjD,YAEQ,MAAA,KAAA,GAAA;KAFA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,aAAA,CAAA;KAAiB,OAAM;KAAQ,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,OAAA;;KAC5D,SAAA,cAAS,CAAT,YAAS,MAAA,KAAA,CAAA,CAAA,CAAA;;;GAIb,GAAA,CAAA,GAAA,YAIU,MAAA,OAAA,GAAA,EAJA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA,EAAA,GAAA;IACpB,SAAA,cAAmE,CAAnE,mBAAA,8DAAA,GACmC,KAAA,OAAM,UAAzC,UAAA,GAAA,YAAyE,wBAAzD,KAAA,MAAK,MAAM,GAAA;;KAAuB,aAAW,QAAA;IAC7D,GAAA,MAAA,GAAA,CAAA,WAAA,CAAA,MAAA,UAAA,GAAA,mBAAuE,OAAA;;KAA1D,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,gBAAA,CAAA;IAAuB,GAAA,gBAAA,eAAA,KAAc,GAAA,CAAA,EAAA,CAAA"}
@@ -14,7 +14,7 @@
14
14
  * --u-height-default: 32px;
15
15
  * --u-height-small: 24px;
16
16
  */
17
- /** 暗色:data-theme 与系统偏好,与 UITheme.injectBuiltInThemes 选择器策略一致 */
17
+ /** 暗色:`UITheme.render()` 按主题系列写入 html[data-theme] */
18
18
  .u-ai-chat {
19
19
  height: 100%;
20
20
  min-height: 0;
@@ -149,7 +149,7 @@
149
149
  .u-ai-chat__panel-fallback {
150
150
  margin: 0;
151
151
  padding: var(--u-gap-default);
152
- font-family: var(--u-font-family-code);
152
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
153
153
  font-size: var(--u-font-size-main-small);
154
154
  line-height: 1.5;
155
155
  white-space: pre-wrap;
@@ -241,6 +241,14 @@
241
241
  color: var(--u-color-primary);
242
242
  background-color: var(--u-color-primary-light-9);
243
243
  }
244
+ .u-ai-chat__welcome-item:disabled {
245
+ cursor: default;
246
+ color: var(--u-text-color-second);
247
+ }
248
+ .u-ai-chat__welcome-item:disabled:hover {
249
+ color: var(--u-text-color-second);
250
+ background-color: var(--u-bg-color-top);
251
+ }
244
252
  .u-ai-chat__welcome--swap-enter-active {
245
253
  transition: opacity 0.18s ease, transform 0.18s ease;
246
254
  }
@@ -528,17 +536,28 @@
528
536
  }
529
537
  .u-ai-chat__tool-call-section {
530
538
  color: var(--u-text-color-assist);
531
- margin-bottom: 4px;
532
539
  }
533
- .u-ai-chat__tool-call-section:not(:first-child) {
540
+ .u-ai-chat__tool-call-json:not(:first-child) {
534
541
  margin-top: var(--u-gap-small);
535
542
  }
543
+ .u-ai-chat__tool-call-json-head {
544
+ display: flex;
545
+ justify-content: flex-start;
546
+ align-items: center;
547
+ flex-wrap: nowrap;
548
+ gap: var(--u-gap-small);
549
+ margin-bottom: 4px;
550
+ }
551
+ .u-ai-chat__tool-call-json-toggle {
552
+ flex-shrink: 0;
553
+ margin-left: auto;
554
+ }
536
555
  .u-ai-chat__tool-call-code {
537
556
  margin: 0;
538
557
  padding: 4px var(--u-gap-small);
539
558
  border-radius: var(--u-radius-small);
540
559
  background-color: var(--u-bg-color-bottom);
541
- font-family: var(--u-font-family-code);
560
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
542
561
  font-size: var(--u-font-size-main-small);
543
562
  line-height: 1.5;
544
563
  white-space: pre-wrap;
@@ -766,6 +785,125 @@
766
785
  .u-ai-chat__queue-action--primary {
767
786
  color: var(--u-color-primary);
768
787
  }
788
+ .u-ai-chat__job-bar {
789
+ flex-shrink: 0;
790
+ width: 100%;
791
+ max-width: 800px;
792
+ box-sizing: border-box;
793
+ margin-bottom: 4px;
794
+ border-radius: var(--u-radius-default);
795
+ background-color: var(--u-bg-color-middle);
796
+ padding: 4px;
797
+ font-size: var(--u-font-size-main-small);
798
+ }
799
+ .u-ai-chat__job-bar-head {
800
+ display: flex;
801
+ justify-content: space-between;
802
+ align-items: center;
803
+ flex-wrap: nowrap;
804
+ width: 100%;
805
+ border: none;
806
+ background: none;
807
+ padding: 2px 6px;
808
+ color: var(--u-text-color-assist);
809
+ font-size: inherit;
810
+ font-family: inherit;
811
+ cursor: pointer;
812
+ }
813
+ .u-ai-chat__job-bar-chevron {
814
+ font-size: 12px;
815
+ transition: transform 0.2s;
816
+ }
817
+ .u-ai-chat__job-bar-chevron.is-expanded {
818
+ transform: rotate(90deg);
819
+ }
820
+ .u-ai-chat__job-bar-list {
821
+ display: flex;
822
+ flex-direction: column;
823
+ }
824
+ .u-ai-chat__job-bar-item {
825
+ display: flex;
826
+ justify-content: flex-start;
827
+ align-items: center;
828
+ flex-wrap: nowrap;
829
+ gap: 6px;
830
+ padding: 3px 6px;
831
+ border-radius: var(--u-radius-small);
832
+ }
833
+ .u-ai-chat__job-bar-icon {
834
+ flex-shrink: 0;
835
+ font-size: 13px;
836
+ color: var(--u-text-color-second);
837
+ }
838
+ .u-ai-chat__job-bar-label {
839
+ flex: 1;
840
+ min-width: 0;
841
+ overflow: hidden;
842
+ text-overflow: ellipsis;
843
+ white-space: nowrap;
844
+ color: var(--u-text-color-main);
845
+ }
846
+ .u-ai-chat__job-bar-dot {
847
+ flex-shrink: 0;
848
+ width: 6px;
849
+ height: 6px;
850
+ border-radius: 50%;
851
+ background-color: var(--u-text-color-placeholder);
852
+ }
853
+ .u-ai-chat__job-bar-dot.is-running {
854
+ background-color: var(--u-color-primary);
855
+ }
856
+ .u-ai-chat__job-bar-dot.is-done {
857
+ background-color: var(--u-color-success);
858
+ }
859
+ .u-ai-chat__job-bar-dot.is-error {
860
+ background-color: var(--u-color-danger);
861
+ }
862
+ .u-ai-chat__session-question {
863
+ flex-shrink: 0;
864
+ width: 100%;
865
+ max-width: 800px;
866
+ box-sizing: border-box;
867
+ margin-bottom: 4px;
868
+ border-radius: var(--u-radius-default);
869
+ background-color: var(--u-bg-color-top);
870
+ box-shadow: var(--u-shadow-sm);
871
+ padding: var(--u-gap-small);
872
+ }
873
+ .u-ai-chat__approval-banner {
874
+ flex-shrink: 0;
875
+ width: 100%;
876
+ max-width: 800px;
877
+ box-sizing: border-box;
878
+ margin-bottom: 4px;
879
+ }
880
+ .u-ai-chat__approval-banner-card {
881
+ padding: var(--u-gap-small);
882
+ border-radius: var(--u-radius-default);
883
+ background-color: var(--u-bg-color-top);
884
+ box-shadow: var(--u-shadow-sm);
885
+ }
886
+ .u-ai-chat__approval-banner-card + .u-ai-chat__approval-banner-card {
887
+ margin-top: 4px;
888
+ }
889
+ .u-ai-chat__approval-banner-title {
890
+ color: var(--u-text-color-title);
891
+ font-weight: 500;
892
+ font-size: var(--u-font-size-main-small);
893
+ }
894
+ .u-ai-chat__approval-banner-reason {
895
+ margin-top: 4px;
896
+ color: var(--u-text-color-assist);
897
+ font-size: var(--u-font-size-main-small);
898
+ }
899
+ .u-ai-chat__approval-banner-actions {
900
+ display: flex;
901
+ justify-content: flex-end;
902
+ align-items: center;
903
+ flex-wrap: nowrap;
904
+ gap: var(--u-gap-small);
905
+ margin-top: var(--u-gap-small);
906
+ }
769
907
  .u-ai-chat__input-area {
770
908
  flex-shrink: 0;
771
909
  width: 100%;
@@ -788,9 +926,13 @@
788
926
  font-family: inherit;
789
927
  line-height: 1.6;
790
928
  box-sizing: border-box;
929
+ overflow-y: hidden;
791
930
  }
792
931
  .u-ai-chat__input::placeholder {
793
932
  color: var(--u-text-color-placeholder);
933
+ white-space: nowrap;
934
+ overflow: hidden;
935
+ text-overflow: ellipsis;
794
936
  }
795
937
  .u-ai-chat__input-toolbar {
796
938
  display: flex;
@@ -835,7 +977,7 @@
835
977
  transition: background-color 0.15s ease;
836
978
  }
837
979
  .u-ai-chat__model-trigger:hover {
838
- background-color: var(--u-nav-hover-bg);
980
+ background-color: var(--u-bg-color-hover);
839
981
  }
840
982
  .u-ai-chat__model-trigger-label {
841
983
  overflow: hidden;
@@ -937,7 +1079,7 @@
937
1079
  transition: background-color 0.15s ease;
938
1080
  }
939
1081
  .u-ai-chat__model-panel .u-ai-chat__model-option-main:hover {
940
- background-color: var(--u-nav-hover-bg);
1082
+ background-color: var(--u-bg-color-hover);
941
1083
  }
942
1084
  .u-ai-chat__model-panel .u-ai-chat__model-option-head {
943
1085
  display: flex;
@@ -1026,7 +1168,7 @@
1026
1168
  transition: background-color 0.15s ease, color 0.15s ease;
1027
1169
  }
1028
1170
  .u-ai-chat__model-panel .u-ai-chat__reasoning-option:hover {
1029
- background-color: var(--u-nav-hover-bg);
1171
+ background-color: var(--u-bg-color-hover);
1030
1172
  }
1031
1173
  .u-ai-chat__model-panel .u-ai-chat__reasoning-option.is-active {
1032
1174
  color: var(--u-color-primary);
@@ -1,6 +1,7 @@
1
1
  import { AiChatDIKey } from "./di.js";
2
+ import { resolveToolIcon } from "./tool-icons.js";
3
+ import _sfc_main$1 from "./tool-json.js";
2
4
  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 { CircleCheckFilled, CircleClose, Close, Loading, WarningFilled } from "@veltra/icons/normal";
4
5
  import { UButton, UCollapseItem, UIcon } from "@veltra/desktop";
5
6
  import { bem } from "@veltra/utils";
6
7
  //#region src/components/ai-chat/tool-call.vue
@@ -14,7 +15,21 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
14
15
  const emit = __emit;
15
16
  const di = inject(AiChatDIKey);
16
17
  const cls = di?.cls ?? bem("ai-chat");
17
- /** 当前调用对应的工具定义(解析 icon/label/render/autoCollapse/terminal) */
18
+ /** 把未知值格式成可读 JSON,循环引用等异常时退回 String,不得 throw */
19
+ function formatUnknown(value) {
20
+ if (value == null) return "";
21
+ if (typeof value === "string") try {
22
+ return JSON.stringify(JSON.parse(value), null, 2);
23
+ } catch {
24
+ return value;
25
+ }
26
+ try {
27
+ return JSON.stringify(value, null, 2) ?? String(value);
28
+ } catch {
29
+ return String(value);
30
+ }
31
+ }
32
+ /** 当前调用对应的工具定义或渲染元信息(session 下忽略 execute / needsConfirm / terminal) */
18
33
  const tool = computed(() => di?.tools.value[props.toolCall.name]);
19
34
  const isExpanded = ref(false);
20
35
  /** 用户手动切换过后,不再随 status 自动展开/折叠 */
@@ -29,37 +44,18 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
29
44
  const autoCollapse = computed(() => tool.value?.autoCollapse ?? (isPanelTool.value || !tool.value?.render));
30
45
  watch(() => props.toolCall.status, (status) => {
31
46
  if (userToggled) return;
32
- if (status === "running" || status === "awaiting-confirm") isExpanded.value = true;
47
+ if (status === "pending" || status === "running" || status === "awaiting-confirm") isExpanded.value = true;
33
48
  else isExpanded.value = !autoCollapse.value;
34
49
  }, { immediate: true });
35
- const STATUS_META = {
36
- pending: {
37
- icon: Loading,
38
- class: "is-active"
39
- },
40
- running: {
41
- icon: Loading,
42
- class: "is-active"
43
- },
44
- "awaiting-confirm": {
45
- icon: WarningFilled,
46
- class: "is-warning"
47
- },
48
- success: {
49
- icon: CircleCheckFilled,
50
- class: "is-success"
51
- },
52
- error: {
53
- icon: CircleClose,
54
- class: "is-danger"
55
- },
56
- rejected: {
57
- icon: Close,
58
- class: "is-danger"
59
- }
50
+ const STATUS_CLASS = {
51
+ pending: "is-active",
52
+ running: "is-active",
53
+ "awaiting-confirm": "is-warning",
54
+ success: "is-success",
55
+ error: "is-danger",
56
+ rejected: "is-danger"
60
57
  };
61
- const statusIcon = computed(() => STATUS_META[props.toolCall.status]?.icon ?? Loading);
62
- const statusClass = computed(() => STATUS_META[props.toolCall.status]?.class ?? "");
58
+ const statusClass = computed(() => STATUS_CLASS[props.toolCall.status] ?? "");
63
59
  /** 进行中(pending/running):工具名带扫光效果 */
64
60
  const isActive = computed(() => {
65
61
  return props.toolCall.status === "pending" || props.toolCall.status === "running";
@@ -77,8 +73,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
77
73
  * 进行中/待确认要保留内容状态(确认按钮、表单),面板工具的 body 是「查看面板」入口必须可达
78
74
  */
79
75
  const destroyOnCollapse = computed(() => isSettled.value && !isPanelTool.value);
80
- /** 头部图标:工具自定义图标优先,缺省用状态图标 */
81
- const headerIcon = computed(() => tool.value?.icon ?? statusIcon.value);
76
+ /** 头部图标:meta.icon > 宿主覆盖 > 内置名称规则 > 兜底;状态点走 status class */
77
+ const headerIcon = computed(() => tool.value?.icon ?? resolveToolIcon(props.toolCall.name, di?.toolIcons?.value));
82
78
  const parsedArguments = computed(() => {
83
79
  if (!props.toolCall.arguments) return null;
84
80
  try {
@@ -102,13 +98,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
102
98
  const hasResult = computed(() => {
103
99
  return props.toolCall.result != null || props.toolCall.error != null;
104
100
  });
105
- const displayResult = computed(() => {
106
- const raw = props.toolCall.error ?? props.toolCall.result ?? "";
107
- try {
108
- return JSON.stringify(JSON.parse(raw), null, 2);
109
- } catch {
110
- return raw;
111
- }
101
+ const resultTitle = computed(() => props.toolCall.status === "error" ? "错误" : "结果");
102
+ const displayResult = computed(() => formatUnknown(props.toolCall.error ?? props.toolCall.result));
103
+ /** 服务端下发的 view,格式化为 JSON;不得丢弃、不得 throw */
104
+ const prettyView = computed(() => {
105
+ if (props.toolCall.view == null) return "";
106
+ return formatUnknown(props.toolCall.view);
112
107
  });
113
108
  /** 使用方通过 tool-<name> 插槽自定义该工具的展示(有结果时替换整个 body) */
114
109
  const slotBody = computed(() => {
@@ -165,7 +160,23 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
165
160
  }, {
166
161
  default: withCtx(() => [..._cache[9] || (_cache[9] = [createTextVNode(" 查看面板 ", -1)])]),
167
162
  _: 1
168
- })], 2)) : tool.value?.render ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createCommentVNode(" 自定义渲染:工具定义的 render 优先于 tool-<name> 插槽,均替换整个 body "), (openBlock(), createBlock(resolveDynamicComponent(tool.value.render), { "tool-call": __props.toolCall }, null, 8, ["tool-call"]))], 2112)) : slotBody.value && hasResult.value ? (openBlock(), createBlock(resolveDynamicComponent(slotBody.value), { key: 2 })) : (openBlock(), createElementBlock(Fragment, { key: 3 }, [prettyArguments.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("div", { class: normalizeClass(unref(cls).e("tool-call-section")) }, "参数", 2), createElementVNode("pre", { class: normalizeClass(unref(cls).e("tool-call-code")) }, toDisplayString(prettyArguments.value), 3)], 64)) : createCommentVNode("v-if", true), hasResult.value ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("div", { class: normalizeClass(unref(cls).e("tool-call-section")) }, toDisplayString(__props.toolCall.status === "error" ? "错误" : "结果"), 3), createElementVNode("pre", { class: normalizeClass(unref(cls).e("tool-call-code")) }, toDisplayString(displayResult.value), 3)], 64)) : createCommentVNode("v-if", true)], 64))]),
163
+ })], 2)) : tool.value?.render ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createCommentVNode(" 自定义渲染:meta.render / tool-<name> 插槽优先于通用工具视图 "), (openBlock(), createBlock(resolveDynamicComponent(tool.value.render), { "tool-call": __props.toolCall }, null, 8, ["tool-call"]))], 2112)) : slotBody.value && hasResult.value ? (openBlock(), createBlock(resolveDynamicComponent(slotBody.value), { key: 2 })) : (openBlock(), createElementBlock(Fragment, { key: 3 }, [
164
+ prettyArguments.value ? (openBlock(), createBlock(_sfc_main$1, {
165
+ key: 0,
166
+ title: "参数",
167
+ text: prettyArguments.value
168
+ }, null, 8, ["text"])) : createCommentVNode("v-if", true),
169
+ hasResult.value ? (openBlock(), createBlock(_sfc_main$1, {
170
+ key: 1,
171
+ title: resultTitle.value,
172
+ text: displayResult.value
173
+ }, null, 8, ["title", "text"])) : createCommentVNode("v-if", true),
174
+ prettyView.value ? (openBlock(), createBlock(_sfc_main$1, {
175
+ key: 2,
176
+ title: "视图",
177
+ text: prettyView.value
178
+ }, null, 8, ["text"])) : createCommentVNode("v-if", true)
179
+ ], 64))]),
169
180
  _: 1
170
181
  }, 8, [
171
182
  "modelValue",