@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":"ai-chat.js","names":["$slots"],"sources":["../../../src/components/ai-chat/ai-chat.vue"],"sourcesContent":["<template>\n <ULayout\n ref=\"layoutRef\"\n :class=\"[cls.b, bem.is('resizing', resizing)]\"\n :cols=\"layoutCols\"\n rows=\"1fr\"\n :resizable=\"panelOpen\"\n :col-min-sizes=\"panelOpen ? [MAIN_MIN_WIDTH, PANEL_MIN_WIDTH] : undefined\"\n @resize-start=\"resizing = true\"\n @resize-end=\"resizing = false\"\n >\n <div :class=\"[cls.e('main'), bem.is('empty', isEmpty)]\">\n <MessageList\n :messages=\"messages\"\n :welcome=\"welcome\"\n :running=\"running\"\n :renderer-props=\"rendererProps\"\n @respond=\"respondToolCall\"\n @regenerate=\"regenerate\"\n @welcome-click=\"(text) => handleSend(text, [])\"\n >\n <template v-if=\"$slots.welcome\" #welcome>\n <slot name=\"welcome\" />\n </template>\n </MessageList>\n\n <QueueList\n :queue=\"queue\"\n @start-now=\"startQueued\"\n @edit=\"handleQueueEdit\"\n @remove=\"removeQueued\"\n />\n\n <ChatInput\n ref=\"chatInputRef\"\n v-model:model=\"model\"\n v-model:reasoning-level=\"reasoningLevel\"\n :running=\"running\"\n :models=\"models\"\n :placeholder=\"placeholder\"\n :accept=\"accept\"\n :max-attachment-size=\"maxAttachmentSize\"\n :clearable=\"clearable\"\n :token-usage=\"tokenUsage\"\n :token-usage-detail=\"tokenUsageDetail\"\n @send=\"handleSend\"\n @abort=\"abort\"\n @clear=\"clear\"\n />\n </div>\n\n <!-- 侧边面板(renderTo: 'panel' 的工具渲染区),与会话区宽度经 ULayout 拖拽手柄调节 -->\n <SidePanel\n v-if=\"activePanelCall\"\n ref=\"panelRef\"\n :tool-call=\"activePanelCall\"\n @close=\"closePanel\"\n />\n </ULayout>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useResizeObserver } from '@veltra/compositions'\nimport { ULayout } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, provide, ref, useSlots, useTemplateRef, watch } from 'vue'\n\nimport type { ChatAttachment, ChatToolCall } from '../../chat/types'\nimport { useChat } from '../../chat/use-chat'\nimport { createBuiltinTools } from '../../tools'\nimport type { _AiChatExposed, AiChatEmits, AiChatProps } from '../../types'\nimport ChatInput from './chat-input.vue'\nimport { AiChatDIKey } from './di'\nimport MessageList from './message-list.vue'\nimport QueueList from './queue-list.vue'\nimport SidePanel from './side-panel.vue'\n\ndefineOptions({ name: 'UAiChat' })\n\nconst props = defineProps<AiChatProps>()\n\nconst emit = defineEmits<AiChatEmits>()\n\ndefineSlots<{\n /** 空闲欢迎区插槽(输入框上方;工作中跳到列表末尾) */\n welcome(): any\n /** 按工具名自定义工具结果展示,如 tool-getWeather */\n [name: `tool-${string}`]: (scope: { toolCall: ChatToolCall }) => any\n}>()\n\nconst cls = bem('ai-chat')\n\nconst slots = useSlots()\n\n/** 内置 + 用户工具(同名内置优先),供工具卡片解析 icon/label/render/renderTo/autoCollapse */\nconst toolMap = computed(() => {\n const builtins = createBuiltinTools()\n const names = new Set(builtins.map((t) => t.name))\n const tools = [...builtins, ...(props.tools ?? []).filter((t) => !names.has(t.name))]\n return Object.fromEntries(tools.map((t) => [t.name, t]))\n})\n\nconst {\n messages,\n model,\n reasoningLevel,\n running,\n queue,\n tokenUsage,\n lastTurnUsage,\n send,\n abort,\n regenerate,\n clear,\n respondToolCall,\n enqueue,\n startQueued,\n removeQueued\n} = useChat({ props, emit })\n\n/** 空会话(无可见消息):输入区垂直居中;欢迎区钉在滚动容器外、贴于输入框上方 */\nconst isEmpty = computed(() => !messages.value.some((msg) => msg.role !== 'tool'))\n\n/** 有对话内容、队列或生成中才允许清除,避免误触空会话 */\nconst clearable = computed(\n () => messages.value.length > 0 || queue.value.length > 0 || running.value\n)\n\n/** 所有 renderTo: 'panel' 的工具调用(按消息顺序,面板内容的来源) */\nconst panelCalls = computed(() => {\n const calls: ChatToolCall[] = []\n for (const msg of messages.value) {\n if (msg.role !== 'assistant' || !msg.toolCalls) continue\n for (const call of msg.toolCalls) {\n if (toolMap.value[call.name]?.renderTo === 'panel') calls.push(call)\n }\n }\n return calls\n})\n\n/** 当前展示在右侧面板的工具调用 id;null 表示面板关闭 */\nconst activePanelCallId = ref<string | null>(null)\n\nconst activePanelCall = computed(() => {\n if (!activePanelCallId.value) return null\n return panelCalls.value.find((call) => call.id === activePanelCallId.value) ?? null\n})\n\nconst panelOpen = computed(() => activePanelCall.value != null)\n\n// 出现新的面板工具调用时自动打开并聚焦到它\nwatch(\n () => panelCalls.value[panelCalls.value.length - 1]?.id,\n (id) => {\n if (id) activePanelCallId.value = id\n }\n)\n\n// 聚焦的调用消失(清空消息 / 重新生成)时,回落到最近一次面板调用或关闭\nwatch(panelCalls, (calls) => {\n const id = activePanelCallId.value\n if (id && !calls.some((call) => call.id === id)) {\n activePanelCallId.value = calls[calls.length - 1]?.id ?? null\n }\n})\n\n/** 打开面板并聚焦到指定工具调用(工具卡片「查看面板」入口,经 DI 下发) */\nconst openPanel = (toolCallId: string) => {\n if (panelCalls.value.some((call) => call.id === toolCallId)) {\n activePanelCallId.value = toolCallId\n }\n}\n\nconst closePanel = () => {\n activePanelCallId.value = null\n}\n\nprovide(AiChatDIKey, { cls, slots, tools: toolMap, openPanel })\n\n/**\n * 面板宽度:经 ULayout 列轨施加(cols = 1fr + 面板宽度),\n * 拖拽手柄由 ULayout 提供(colMinSizes 约束会话区 / 面板最小宽度)。\n */\nconst PANEL_MIN_WIDTH = 320\nconst MAIN_MIN_WIDTH = 360\n/** 面板打开时默认给会话区留的宽度(即面板默认尽可能大) */\nconst MAIN_DEFAULT_WIDTH = 860\n\nconst layoutRef = useTemplateRef<InstanceType<typeof ULayout>>('layoutRef')\nconst panelRef = useTemplateRef<InstanceType<typeof SidePanel>>('panelRef')\nconst panelWidth = ref(PANEL_MIN_WIDTH)\nconst resizing = ref(false)\n\nconst layoutCols = computed(() => {\n return panelOpen.value ? ['1fr', `${panelWidth.value}px`] : ['1fr']\n})\n\nconst containerWidth = () => (layoutRef.value?.$el as HTMLElement | undefined)?.clientWidth ?? 0\n\n/** 面板默认宽度:容器宽减去会话区默认保留宽度(缺省给会话区留 MAIN_DEFAULT_WIDTH) */\nconst defaultPanelWidth = () => {\n const width = containerWidth()\n return width ? width - MAIN_DEFAULT_WIDTH : 420\n}\n\n/** 钳位面板宽度在 [PANEL_MIN_WIDTH, 容器宽 - MAIN_MIN_WIDTH] 区间 */\nconst clampPanelWidth = (width: number) => {\n const width0 = containerWidth()\n const maxWidth = width0\n ? Math.max(PANEL_MIN_WIDTH, width0 - MAIN_MIN_WIDTH)\n : Number.MAX_SAFE_INTEGER\n return Math.min(Math.max(width, PANEL_MIN_WIDTH), maxWidth)\n}\n\n// 聚焦到指定了 panelWidth 的工具调用时应用其宽度;面板从关闭到打开且工具未指定宽度时\n// 应用默认宽(会话区留 860px);面板已打开时切换聚焦保持当前宽度(含用户拖拽结果)\nwatch(activePanelCall, (call, prevCall) => {\n if (!call) return\n const toolWidth = toolMap.value[call.name]?.panelWidth\n const width = toolWidth ?? (prevCall ? undefined : defaultPanelWidth())\n if (typeof width !== 'number') return\n panelWidth.value = clampPanelWidth(width)\n})\n\n// 跟踪面板实际宽度(含用户拖拽结果):面板关闭重开、切换到未指定宽度的工具时保持\nuseResizeObserver({\n targets: computed(() => (panelRef.value?.$el as HTMLElement | undefined) ?? undefined),\n onResize(entries) {\n // borderBoxSize 随通知携带(含边框、零布局开销),不读 offsetWidth/clientWidth\n const inlineSize = entries[0]?.borderBoxSize?.[0]?.inlineSize\n if (!inlineSize) return\n const width = Math.round(inlineSize)\n // 滞回:忽略亚像素/边框级的微小偏差(< 2px),阻断任何「观测→回写→再观测」的微缩反馈回路\n if (Math.abs(width - panelWidth.value) < 2) return\n panelWidth.value = width\n }\n})\n\nconst chatInputRef = useTemplateRef<InstanceType<typeof ChatInput>>('chatInputRef')\n\n/**\n * 编辑中的队列项原后继 id(编辑态标记):\n * undefined 表示无编辑;null 表示原位置在队尾;否则重新提交时插回该后继之前,\n * 使前后项顺序不受编辑影响(前项已执行则该项自然成为最前待执行项)。\n */\nconst editingSuccessor = ref<string | null | undefined>(undefined)\n\n/** 编辑队列项:取回输入框重新编辑;已有未完成编辑时先按锚点还原,避免内容丢失 */\nconst handleQueueEdit = (id: string) => {\n restoreEditing()\n\n const index = queue.value.findIndex((item) => item.id === id)\n if (index === -1) return\n editingSuccessor.value = queue.value[index + 1]?.id ?? null\n\n const item = removeQueued(id)\n if (item) chatInputRef.value?.setContent(item.content)\n}\n\n/** 将输入框中未提交的编辑内容按原锚点插回队列 */\nconst restoreEditing = () => {\n if (editingSuccessor.value === undefined) return\n const content = chatInputRef.value?.getContent().trim()\n if (content) enqueue(content, undefined, editingSuccessor.value ?? undefined)\n editingSuccessor.value = undefined\n}\n\nconst handleSend = (content: string, attachments: ChatAttachment[]) => {\n // 编辑态提交:插回原锚点位置(会话进行中则留在队列,空闲时由 enqueue 自动消耗队首)\n if (editingSuccessor.value !== undefined) {\n enqueue(content, attachments, editingSuccessor.value ?? undefined)\n editingSuccessor.value = undefined\n return\n }\n send(content, attachments)\n}\n\ndefineExpose<_AiChatExposed>({\n send,\n abort,\n regenerate,\n clear,\n tokenUsage,\n lastTurnUsage,\n queue,\n startQueued,\n removeQueued,\n enqueue\n})\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"ai-chat.js","names":["$slots"],"sources":["../../../src/components/ai-chat/ai-chat.vue"],"sourcesContent":["<template>\n <ULayout\n ref=\"layoutRef\"\n :class=\"[cls.b, bem.is('resizing', resizing)]\"\n :cols=\"layoutCols\"\n rows=\"1fr\"\n :resizable=\"panelOpen\"\n :col-min-sizes=\"panelOpen ? [MAIN_MIN_WIDTH, PANEL_MIN_WIDTH] : undefined\"\n @resize-start=\"resizing = true\"\n @resize-end=\"resizing = false\"\n >\n <div :class=\"[cls.e('main'), bem.is('empty', isEmpty)]\">\n <MessageList\n :messages=\"messages\"\n :welcome=\"welcome\"\n :running=\"running\"\n :readonly=\"readonly\"\n :renderer-props=\"rendererProps\"\n @respond=\"respondToolCall\"\n @regenerate=\"regenerate\"\n @welcome-click=\"(text) => handleSend(text, [])\"\n >\n <template v-if=\"$slots.welcome\" #welcome>\n <slot name=\"welcome\" />\n </template>\n </MessageList>\n\n <div v-if=\"pendingQuestion\" :class=\"cls.e('session-question')\">\n <AskQuestion :questions=\"pendingQuestion.questions\" @submit=\"handleQuestionSubmit\" />\n </div>\n\n <ApprovalBanner :approvals=\"pendingApprovals\" @respond=\"respondSession\" />\n\n <QueueList\n :queue=\"queue\"\n :readonly=\"readonly\"\n @start-now=\"startQueued\"\n @edit=\"handleQueueEdit\"\n @remove=\"removeQueued\"\n />\n\n <JobBar :jobs=\"jobs\" />\n\n <ChatInput\n v-if=\"!readonly\"\n ref=\"chatInputRef\"\n v-model:model=\"model\"\n v-model:reasoning-level=\"reasoningLevel\"\n :running=\"running\"\n :models=\"models\"\n :placeholder=\"placeholder\"\n :accept=\"accept\"\n :max-attachment-size=\"maxAttachmentSize\"\n :clearable=\"clearable\"\n :token-usage=\"tokenUsage\"\n :token-usage-detail=\"tokenUsageDetail\"\n @send=\"handleSend\"\n @abort=\"abort\"\n @clear=\"clear\"\n />\n </div>\n\n <!-- 侧边面板(renderTo: 'panel' 的工具渲染区),与会话区宽度经 ULayout 拖拽手柄调节 -->\n <SidePanel\n v-if=\"activePanelCall\"\n ref=\"panelRef\"\n :tool-call=\"activePanelCall\"\n @close=\"closePanel\"\n />\n </ULayout>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useResizeObserver } from '@veltra/compositions'\nimport { ULayout } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, provide, ref, useSlots, useTemplateRef, watch } from 'vue'\n\nimport { isServerTransport } from '../../chat/session'\nimport type { ChatAttachment, ChatTool, ChatToolCall, ChatToolMeta } from '../../chat/types'\nimport { useChat } from '../../chat/use-chat'\nimport { createBuiltinTools } from '../../tools'\nimport type { AskQuestionAnswer, AskQuestionResult } from '../../tools/ask-question/ask-question'\nimport type { _AiChatExposed, AiChatEmits, AiChatProps } from '../../types'\nimport ApprovalBanner from './approval-banner.vue'\nimport AskQuestion from './ask-question.vue'\nimport ChatInput from './chat-input.vue'\nimport { AiChatDIKey } from './di'\nimport JobBar from './job-bar.vue'\nimport MessageList from './message-list.vue'\nimport QueueList from './queue-list.vue'\nimport SidePanel from './side-panel.vue'\n\ndefineOptions({ name: 'UAiChat' })\n\nconst props = defineProps<AiChatProps>()\n\nconst emit = defineEmits<AiChatEmits>()\n\ndefineSlots<{\n /** 空闲欢迎区插槽(输入框上方;工作中跳到列表末尾) */\n welcome(): any\n /** 按工具名自定义工具结果展示,如 tool-getWeather */\n [name: `tool-${string}`]: (scope: { toolCall: ChatToolCall }) => any\n}>()\n\nconst cls = bem('ai-chat')\n\nconst slots = useSlots()\n\n/**\n * 按 name 查表:session 下 tools 仅为渲染元信息(不注入内置工具,忽略 execute 等)。\n * 函数 transport 仍注入 createBuiltinTools(同名内置优先)。\n */\nconst toolMap = computed((): Record<string, ChatTool | ChatToolMeta | undefined> => {\n if (isServerTransport(props.transport)) {\n return Object.fromEntries((props.tools ?? []).map((t) => [t.name, t]))\n }\n const builtins = createBuiltinTools()\n const names = new Set(builtins.map((t) => t.name))\n const tools = [...builtins, ...(props.tools ?? []).filter((t) => !names.has(t.name))]\n return Object.fromEntries(tools.map((t) => [t.name, t]))\n})\n\nconst toolIcons = computed(() => props.toolIcons)\n\nconst {\n messages,\n model,\n reasoningLevel,\n running,\n queue,\n jobs,\n tokenUsage,\n lastTurnUsage,\n pendingApprovals,\n pendingQuestion,\n send,\n abort,\n regenerate,\n clear,\n respondToolCall,\n respondSession,\n enqueue,\n startQueued,\n removeQueued\n} = useChat({ props, emit })\n\n/** 空会话(无可见消息):输入区垂直居中;欢迎区钉在滚动容器外、贴于输入框上方 */\nconst isEmpty = computed(() => !messages.value.some((msg) => msg.role !== 'tool'))\n\n/** 有对话内容、队列或生成中才允许清除,避免误触空会话 */\nconst clearable = computed(\n () => messages.value.length > 0 || queue.value.length > 0 || running.value\n)\n\n/** 所有 renderTo: 'panel' 的工具调用(按消息顺序,面板内容的来源) */\nconst panelCalls = computed(() => {\n const calls: ChatToolCall[] = []\n for (const msg of messages.value) {\n if (msg.role !== 'assistant' || !msg.toolCalls) continue\n for (const call of msg.toolCalls) {\n if (toolMap.value[call.name]?.renderTo === 'panel') calls.push(call)\n }\n }\n return calls\n})\n\n/** 当前展示在右侧面板的工具调用 id;null 表示面板关闭 */\nconst activePanelCallId = ref<string | null>(null)\n\nconst activePanelCall = computed(() => {\n if (!activePanelCallId.value) return null\n return panelCalls.value.find((call) => call.id === activePanelCallId.value) ?? null\n})\n\nconst panelOpen = computed(() => activePanelCall.value != null)\n\n// 出现新的面板工具调用时自动打开并聚焦到它\nwatch(\n () => panelCalls.value[panelCalls.value.length - 1]?.id,\n (id) => {\n if (id) activePanelCallId.value = id\n }\n)\n\n// 聚焦的调用消失(清空消息 / 重新生成)时,回落到最近一次面板调用或关闭\nwatch(panelCalls, (calls) => {\n const id = activePanelCallId.value\n if (id && !calls.some((call) => call.id === id)) {\n activePanelCallId.value = calls[calls.length - 1]?.id ?? null\n }\n})\n\n/** 打开面板并聚焦到指定工具调用(工具卡片「查看面板」入口,经 DI 下发) */\nconst openPanel = (toolCallId: string) => {\n if (panelCalls.value.some((call) => call.id === toolCallId)) {\n activePanelCallId.value = toolCallId\n }\n}\n\nconst closePanel = () => {\n activePanelCallId.value = null\n}\n\nprovide(AiChatDIKey, { cls, slots, tools: toolMap, toolIcons, openPanel })\n\n/**\n * 面板宽度:经 ULayout 列轨施加(cols = 1fr + 面板宽度),\n * 拖拽手柄由 ULayout 提供(colMinSizes 约束会话区 / 面板最小宽度)。\n */\nconst PANEL_MIN_WIDTH = 320\nconst MAIN_MIN_WIDTH = 360\n/** 面板打开时默认给会话区留的宽度(即面板默认尽可能大) */\nconst MAIN_DEFAULT_WIDTH = 860\n\nconst layoutRef = useTemplateRef<InstanceType<typeof ULayout>>('layoutRef')\nconst panelRef = useTemplateRef<InstanceType<typeof SidePanel>>('panelRef')\nconst panelWidth = ref(PANEL_MIN_WIDTH)\nconst resizing = ref(false)\n\nconst layoutCols = computed(() => {\n return panelOpen.value ? ['1fr', `${panelWidth.value}px`] : ['1fr']\n})\n\nconst containerWidth = () => (layoutRef.value?.$el as HTMLElement | undefined)?.clientWidth ?? 0\n\n/** 面板默认宽度:容器宽减去会话区默认保留宽度(缺省给会话区留 MAIN_DEFAULT_WIDTH) */\nconst defaultPanelWidth = () => {\n const width = containerWidth()\n return width ? width - MAIN_DEFAULT_WIDTH : 420\n}\n\n/** 钳位面板宽度在 [PANEL_MIN_WIDTH, 容器宽 - MAIN_MIN_WIDTH] 区间 */\nconst clampPanelWidth = (width: number) => {\n const width0 = containerWidth()\n const maxWidth = width0\n ? Math.max(PANEL_MIN_WIDTH, width0 - MAIN_MIN_WIDTH)\n : Number.MAX_SAFE_INTEGER\n return Math.min(Math.max(width, PANEL_MIN_WIDTH), maxWidth)\n}\n\n// 聚焦到指定了 panelWidth 的工具调用时应用其宽度;面板从关闭到打开且工具未指定宽度时\n// 应用默认宽(会话区留 860px);面板已打开时切换聚焦保持当前宽度(含用户拖拽结果)\nwatch(activePanelCall, (call, prevCall) => {\n if (!call) return\n const toolWidth = toolMap.value[call.name]?.panelWidth\n const width = toolWidth ?? (prevCall ? undefined : defaultPanelWidth())\n if (typeof width !== 'number') return\n panelWidth.value = clampPanelWidth(width)\n})\n\n// 跟踪面板实际宽度(含用户拖拽结果):面板关闭重开、切换到未指定宽度的工具时保持\nuseResizeObserver({\n targets: computed(() => (panelRef.value?.$el as HTMLElement | undefined) ?? undefined),\n onResize(entries) {\n // borderBoxSize 随通知携带(含边框、零布局开销),不读 offsetWidth/clientWidth\n const inlineSize = entries[0]?.borderBoxSize?.[0]?.inlineSize\n if (!inlineSize) return\n const width = Math.round(inlineSize)\n // 滞回:忽略亚像素/边框级的微小偏差(< 2px),阻断任何「观测→回写→再观测」的微缩反馈回路\n if (Math.abs(width - panelWidth.value) < 2) return\n panelWidth.value = width\n }\n})\n\nconst chatInputRef = useTemplateRef<InstanceType<typeof ChatInput>>('chatInputRef')\n\n/**\n * 编辑中的队列项原后继 id(编辑态标记):\n * undefined 表示无编辑;null 表示原位置在队尾;否则重新提交时插回该后继之前,\n * 使前后项顺序不受编辑影响(前项已执行则该项自然成为最前待执行项)。\n */\nconst editingSuccessor = ref<string | null | undefined>(undefined)\n\n/** 编辑队列项:取回输入框重新编辑;已有未完成编辑时先按锚点还原,避免内容丢失 */\nconst handleQueueEdit = (id: string) => {\n restoreEditing()\n\n const index = queue.value.findIndex((item) => item.id === id)\n if (index === -1) return\n editingSuccessor.value = queue.value[index + 1]?.id ?? null\n\n const item = removeQueued(id)\n if (item) chatInputRef.value?.setContent(item.content)\n}\n\n/** 将输入框中未提交的编辑内容按原锚点插回队列 */\nconst restoreEditing = () => {\n if (editingSuccessor.value === undefined) return\n const content = chatInputRef.value?.getContent().trim()\n if (content) enqueue(content, undefined, editingSuccessor.value ?? undefined)\n editingSuccessor.value = undefined\n}\n\nconst handleSend = (content: string, attachments: ChatAttachment[]) => {\n if (props.readonly) return\n // 编辑态提交:插回原锚点位置(会话进行中则留在队列,空闲时由 enqueue 自动消耗队首)\n if (editingSuccessor.value !== undefined) {\n enqueue(content, attachments, editingSuccessor.value ?? undefined)\n editingSuccessor.value = undefined\n return\n }\n send(content, attachments)\n}\n\nconst handleQuestionSubmit = (value: AskQuestionAnswer[] | AskQuestionResult) => {\n const pending = pendingQuestion.value\n if (!pending) return\n respondSession(pending.rpcId, true, Array.isArray(value) ? value : value.answers)\n}\n\ndefineExpose<_AiChatExposed>({\n send,\n abort,\n regenerate,\n clear,\n tokenUsage,\n lastTurnUsage,\n queue,\n startQueued,\n removeQueued,\n enqueue\n})\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;AAmNA,MAAM,kBAAkB;AACxB,MAAM,iBAAiB;;AAEvB,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAvH3B,MAAM,QAAQ;EAEd,MAAM,OAAO;EASb,MAAM,MAAM,IAAI,SAAS;EAEzB,MAAM,QAAQ,SAAS;;;;;EAMvB,MAAM,UAAU,eAAoE;GAClF,IAAI,kBAAkB,MAAM,SAAS,GACnC,OAAO,OAAO,aAAa,MAAM,SAAS,CAAC,EAAA,CAAG,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;GAEvE,MAAM,WAAW,mBAAmB;GACpC,MAAM,QAAQ,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,IAAI,CAAC;GACjD,MAAM,QAAQ,CAAC,GAAG,UAAU,IAAI,MAAM,SAAS,CAAC,EAAA,CAAG,QAAQ,MAAM,CAAC,MAAM,IAAI,EAAE,IAAI,CAAC,CAAC;GACpF,OAAO,OAAO,YAAY,MAAM,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;EACzD,CAAC;EAED,MAAM,YAAY,eAAe,MAAM,SAAS;EAEhD,MAAM,EACJ,UACA,OACA,gBACA,SACA,OACA,MACA,YACA,eACA,kBACA,iBACA,MACA,OACA,YACA,OACA,iBACA,gBACA,SACA,aACA,iBACE,QAAQ;GAAE;GAAO;EAAK,CAAC;;EAG3B,MAAM,UAAU,eAAe,CAAC,SAAS,MAAM,MAAM,QAAQ,IAAI,SAAS,MAAM,CAAC;;EAGjF,MAAM,YAAY,eACV,SAAS,MAAM,SAAS,KAAK,MAAM,MAAM,SAAS,KAAK,QAAQ,KACvE;;EAGA,MAAM,aAAa,eAAe;GAChC,MAAM,QAAwB,CAAC;GAC/B,KAAK,MAAM,OAAO,SAAS,OAAO;IAChC,IAAI,IAAI,SAAS,eAAe,CAAC,IAAI,WAAW;IAChD,KAAK,MAAM,QAAQ,IAAI,WACrB,IAAI,QAAQ,MAAM,KAAK,KAAK,EAAE,aAAa,SAAS,MAAM,KAAK,IAAI;GAEvE;GACA,OAAO;EACT,CAAC;;EAGD,MAAM,oBAAoB,IAAmB,IAAI;EAEjD,MAAM,kBAAkB,eAAe;GACrC,IAAI,CAAC,kBAAkB,OAAO,OAAO;GACrC,OAAO,WAAW,MAAM,MAAM,SAAS,KAAK,OAAO,kBAAkB,KAAK,KAAK;EACjF,CAAC;EAED,MAAM,YAAY,eAAe,gBAAgB,SAAS,IAAI;EAG9D,YACQ,WAAW,MAAM,WAAW,MAAM,SAAS,EAAE,EAAE,KACpD,OAAO;GACN,IAAI,IAAI,kBAAkB,QAAQ;EACpC,CACF;EAGA,MAAM,aAAa,UAAU;GAC3B,MAAM,KAAK,kBAAkB;GAC7B,IAAI,MAAM,CAAC,MAAM,MAAM,SAAS,KAAK,OAAO,EAAE,GAC5C,kBAAkB,QAAQ,MAAM,MAAM,SAAS,EAAE,EAAE,MAAM;EAE7D,CAAC;;EAGD,MAAM,aAAa,eAAuB;GACxC,IAAI,WAAW,MAAM,MAAM,SAAS,KAAK,OAAO,UAAU,GACxD,kBAAkB,QAAQ;EAE9B;EAEA,MAAM,mBAAmB;GACvB,kBAAkB,QAAQ;EAC5B;EAEA,QAAQ,aAAa;GAAE;GAAK;GAAO,OAAO;GAAS;GAAW;EAAU,CAAC;;;;;EAWzE,MAAM,YAAY,eAA6C,WAAW;EAC1E,MAAM,WAAW,eAA+C,UAAU;EAC1E,MAAM,aAAa,IAAI,eAAe;EACtC,MAAM,WAAW,IAAI,KAAK;EAE1B,MAAM,aAAa,eAAe;GAChC,OAAO,UAAU,QAAQ,CAAC,OAAO,GAAG,WAAW,MAAM,GAAG,IAAI,CAAC,KAAK;EACpE,CAAC;EAED,MAAM,wBAAwB,UAAU,OAAO,IAAA,EAAiC,eAAe;;EAG/F,MAAM,0BAA0B;GAC9B,MAAM,QAAQ,eAAe;GAC7B,OAAO,QAAQ,QAAQ,qBAAqB;EAC9C;;EAGA,MAAM,mBAAmB,UAAkB;GACzC,MAAM,SAAS,eAAe;GAC9B,MAAM,WAAW,SACb,KAAK,IAAI,iBAAiB,SAAS,cAAc,IACjD,OAAO;GACX,OAAO,KAAK,IAAI,KAAK,IAAI,OAAO,eAAe,GAAG,QAAQ;EAC5D;EAIA,MAAM,kBAAkB,MAAM,aAAa;GACzC,IAAI,CAAC,MAAM;GAEX,MAAM,QADY,QAAQ,MAAM,KAAK,KAAK,EAAE,eAChB,WAAW,KAAA,IAAY,kBAAkB;GACrE,IAAI,OAAO,UAAU,UAAU;GAC/B,WAAW,QAAQ,gBAAgB,KAAK;EAC1C,CAAC;EAGD,kBAAkB;GAChB,SAAS,eAAgB,SAAS,OAAO,OAAmC,KAAA,CAAS;GACrF,SAAS,SAAS;IAEhB,MAAM,aAAa,QAAQ,EAAE,EAAE,gBAAgB,EAAE,EAAE;IACnD,IAAI,CAAC,YAAY;IACjB,MAAM,QAAQ,KAAK,MAAM,UAAU;IAEnC,IAAI,KAAK,IAAI,QAAQ,WAAW,KAAK,IAAI,GAAG;IAC5C,WAAW,QAAQ;GACrB;EACF,CAAC;EAED,MAAM,eAAe,eAA+C,cAAc;;;;;;EAOlF,MAAM,mBAAmB,IAA+B,KAAA,CAAS;;EAGjE,MAAM,mBAAmB,OAAe;GACtC,eAAe;GAEf,MAAM,QAAQ,MAAM,MAAM,WAAW,SAAS,KAAK,OAAO,EAAE;GAC5D,IAAI,UAAU,IAAI;GAClB,iBAAiB,QAAQ,MAAM,MAAM,QAAQ,EAAE,EAAE,MAAM;GAEvD,MAAM,OAAO,aAAa,EAAE;GAC5B,IAAI,MAAM,aAAa,OAAO,WAAW,KAAK,OAAO;EACvD;;EAGA,MAAM,uBAAuB;GAC3B,IAAI,iBAAiB,UAAU,KAAA,GAAW;GAC1C,MAAM,UAAU,aAAa,OAAO,WAAW,CAAC,CAAC,KAAK;GACtD,IAAI,SAAS,QAAQ,SAAS,KAAA,GAAW,iBAAiB,SAAS,KAAA,CAAS;GAC5E,iBAAiB,QAAQ,KAAA;EAC3B;EAEA,MAAM,cAAc,SAAiB,gBAAkC;GACrE,IAAI,MAAM,UAAU;GAEpB,IAAI,iBAAiB,UAAU,KAAA,GAAW;IACxC,QAAQ,SAAS,aAAa,iBAAiB,SAAS,KAAA,CAAS;IACjE,iBAAiB,QAAQ,KAAA;IACzB;GACF;GACA,KAAK,SAAS,WAAW;EAC3B;EAEA,MAAM,wBAAwB,UAAmD;GAC/E,MAAM,UAAU,gBAAgB;GAChC,IAAI,CAAC,SAAS;GACd,eAAe,QAAQ,OAAO,MAAM,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM,OAAO;EAClF;EAEA,SAA6B;GAC3B;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF,CAAC;;GAlUC,OAAA,UAAA,GAAA,YAoEU,MAAA,OAAA,GAAA;IAnEJ,SAAA;IAAJ,KAAI;IACH,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,GAAG,MAAA,GAAA,CAAG,CAAC,GAAE,YAAa,SAAA,KAAQ,CAAA,CAAA;IAC1C,MAAM,WAAA;IACP,MAAK;IACJ,WAAW,UAAA;IACX,iBAAe,UAAA,QAAS,CAAI,gBAAgB,eAAe,IAAI,KAAA;IAC/D,eAAY,OAAA,OAAA,OAAA,MAAA,WAAE,SAAA,QAAQ;IACtB,aAAU,OAAA,OAAA,OAAA,MAAA,WAAE,SAAA,QAAQ;;IAErB,SAAA,cAiDM;KAjDN,mBAiDM,OAAA,EAjDA,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,MAAA,GAAU,MAAA,GAAA,CAAG,CAAC,GAAE,SAAU,QAAA,KAAO,CAAA,CAAA,EAAA,GAAA;MAClD,YAac,aAAA;OAZX,UAAU,MAAA,QAAA;OACV,SAAS,QAAA;OACT,SAAS,MAAA,OAAA;OACT,UAAU,QAAA;OACV,kBAAgB,QAAA;OAChB,WAAS,MAAA,eAAA;OACT,cAAY,MAAA,UAAA;OACZ,gBAAa,OAAA,OAAA,OAAA,MAAG,SAAS,WAAW,MAAI,CAAA,CAAA;MAEzBA,GAAAA,YAAAA,EAAAA,GAAAA,EAAAA,GAAAA,CAAAA,KAAAA,OAAO,UAAA;OAAU,MAAA;OAC/B,IAAA,cAAuB,CAAvB,WAAuB,KAAA,QAAA,SAAA,CAAA,CAAA;;;;;;;;;;;MAIhB,MAAA,eAAA,KAAX,UAAA,GAAA,mBAEM,OAAA;;OAFuB,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,kBAAA,CAAA;MACvC,GAAA,CAAA,YAAqF,aAAA;OAAvE,WAAW,MAAA,eAAA,CAAe,CAAC;OAAY,UAAQ;;MAG/D,YAA0E,aAAA;OAAzD,WAAW,MAAA,gBAAA;OAAmB,WAAS,MAAA,cAAA;;MAExD,YAME,aAAA;OALC,OAAO,MAAA,KAAA;OACP,UAAU,QAAA;OACV,YAAW,MAAA,WAAA;OACX,QAAM;OACN,UAAQ,MAAA,YAAA;;;;;;;MAGX,YAAuB,aAAA,EAAd,MAAM,MAAA,IAAA,EAAI,GAAA,MAAA,GAAA,CAAA,MAAA,CAAA;MAGV,CAAA,QAAA,YADT,UAAA,GAAA,YAgBE,aAAA;;OAdI,SAAA;OAAJ,KAAI;OACI,OAAO,MAAA,KAAA;OAAA,kBAAA,OAAA,OAAA,OAAA,MAAA,WAAA,MAAA,KAAA,IAAA,MAAK,QAAA,SAAA;OACZ,mBAAiB,MAAA,cAAA;OAAA,2BAAA,OAAA,OAAA,OAAA,MAAA,WAAA,MAAA,cAAA,IAAA,eAAc,QAAA,SAAA;OACtC,SAAS,MAAA,OAAA;OACT,QAAQ,QAAA;OACR,aAAa,QAAA;OACb,QAAQ,QAAA;OACR,uBAAqB,QAAA;OACrB,WAAW,UAAA;OACX,eAAa,MAAA,UAAA;OACb,sBAAoB,QAAA;OACpB,QAAM;OACN,SAAO,MAAA,KAAA;OACP,SAAO,MAAA,KAAA;;;;;;;;;;;;;;;;KAIZ,mBAAA,yDAAA;KAEQ,gBAAA,SADR,UAAA,GAAA,YAKE,aAAA;;MAHI,SAAA;MAAJ,KAAI;MACH,aAAW,gBAAA;MACX,SAAO"}
@@ -0,0 +1,54 @@
1
+ import { AiChatDIKey } from "./di.js";
2
+ import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, renderList, toDisplayString, unref, withCtx } from "vue";
3
+ import { UButton } from "@veltra/desktop";
4
+ import { bem } from "@veltra/utils";
5
+ //#region src/components/ai-chat/approval-banner.vue
6
+ const _sfc_main = /*@__PURE__*/ defineComponent({
7
+ name: "UAiChatApprovalBanner",
8
+ __name: "approval-banner",
9
+ props: { approvals: {} },
10
+ emits: ["respond"],
11
+ setup(__props, { emit: __emit }) {
12
+ const props = __props;
13
+ const emit = __emit;
14
+ const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
15
+ /** 仅无 callId 的审批走横幅;有 callId 的走工具卡确认 */
16
+ const items = computed(() => props.approvals.filter((item) => !item.callId));
17
+ return (_ctx, _cache) => {
18
+ return items.value.length ? (openBlock(), createElementBlock("div", {
19
+ key: 0,
20
+ class: normalizeClass(unref(cls).e("approval-banner"))
21
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(items.value, (item) => {
22
+ return openBlock(), createElementBlock("div", {
23
+ key: item.approvalId,
24
+ class: normalizeClass(unref(cls).e("approval-banner-card"))
25
+ }, [
26
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("approval-banner-title")) }, "需要确认", 2),
27
+ item.reason || item.toolName ? (openBlock(), createElementBlock("div", {
28
+ key: 0,
29
+ class: normalizeClass(unref(cls).e("approval-banner-reason"))
30
+ }, toDisplayString(item.reason ?? item.toolName), 3)) : createCommentVNode("v-if", true),
31
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("approval-banner-actions")) }, [createVNode(unref(UButton), {
32
+ size: "small",
33
+ type: "primary",
34
+ onClick: ($event) => emit("respond", item.rpcId, true)
35
+ }, {
36
+ default: withCtx(() => [..._cache[0] || (_cache[0] = [createTextVNode(" 允许 ", -1)])]),
37
+ _: 1
38
+ }, 8, ["onClick"]), createVNode(unref(UButton), {
39
+ size: "small",
40
+ text: "",
41
+ onClick: ($event) => emit("respond", item.rpcId, false)
42
+ }, {
43
+ default: withCtx(() => [..._cache[1] || (_cache[1] = [createTextVNode("拒绝", -1)])]),
44
+ _: 1
45
+ }, 8, ["onClick"])], 2)
46
+ ], 2);
47
+ }), 128))], 2)) : createCommentVNode("v-if", true);
48
+ };
49
+ }
50
+ });
51
+ //#endregion
52
+ export { _sfc_main as default };
53
+
54
+ //# sourceMappingURL=approval-banner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-banner.js","names":[],"sources":["../../../src/components/ai-chat/approval-banner.vue"],"sourcesContent":["<template>\n <div v-if=\"items.length\" :class=\"cls.e('approval-banner')\">\n <div v-for=\"item in items\" :key=\"item.approvalId\" :class=\"cls.e('approval-banner-card')\">\n <div :class=\"cls.e('approval-banner-title')\">需要确认</div>\n <div v-if=\"item.reason || item.toolName\" :class=\"cls.e('approval-banner-reason')\">\n {{ item.reason ?? item.toolName }}\n </div>\n <div :class=\"cls.e('approval-banner-actions')\">\n <UButton size=\"small\" type=\"primary\" @click=\"emit('respond', item.rpcId, true)\">\n 允许\n </UButton>\n <UButton size=\"small\" text @click=\"emit('respond', item.rpcId, false)\">拒绝</UButton>\n </div>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, inject } from 'vue'\n\nimport type { ChatPendingApproval } from '../../chat/fold'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatApprovalBanner' })\n\nconst props = defineProps<{ approvals: ChatPendingApproval[] }>()\n\nconst emit = defineEmits<{ (e: 'respond', rpcId: string, ok: boolean): void }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 仅无 callId 的审批走横幅;有 callId 的走工具卡确认 */\nconst items = computed(() => props.approvals.filter((item) => !item.callId))\n</script>\n"],"mappings":";;;;;;;;;;;EA2BA,MAAM,QAAQ;EAEd,MAAM,OAAO;EAGb,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;;EAGpC,MAAM,QAAQ,eAAe,MAAM,UAAU,QAAQ,SAAS,CAAC,KAAK,MAAM,CAAC;;GAlC9D,OAAA,MAAA,MAAM,UAAjB,UAAA,GAAA,mBAaM,OAAA;;IAboB,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,iBAAA,CAAA;GACpC,GAAA,EAAA,UAAA,IAAA,GAAA,mBAWM,UAAA,MAAA,WAXc,MAAA,QAAR,SAAI;IAAhB,OAAA,UAAA,GAAA,mBAWM,OAAA;KAXsB,KAAK,KAAK;KAAa,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,sBAAA,CAAA;;KAC7D,mBAAuD,OAAA,EAAjD,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,uBAAA,CAAA,EAA2B,GAAA,QAAI,CAAA;KACtC,KAAK,UAAU,KAAK,YAA/B,UAAA,GAAA,mBAEM,OAAA;;MAFoC,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,wBAAA,CAAA;KACjD,GAAA,gBAAA,KAAK,UAAU,KAAK,QAAQ,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA;KAEjC,mBAKM,OAAA,EALA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,yBAAA,CAAA,EAAA,GAChB,CAAA,YAEU,MAAA,OAAA,GAAA;MAFD,MAAK;MAAQ,MAAK;MAAW,UAAK,WAAE,KAAI,WAAY,KAAK,OAAK,IAAA;;MAAS,SAAA,cAEhF,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAFgF,gBAAA,QAEhF,EAAA,CAAA,EAAA,CAAA;;KACA,GAAA,GAAA,CAAA,SAAA,CAAA,GAAA,YAAmF,MAAA,OAAA,GAAA;MAA1E,MAAK;MAAQ,MAAA;MAAM,UAAK,WAAE,KAAI,WAAY,KAAK,OAAK,KAAA;;MAAU,SAAA,cAAE,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAAF,gBAAA,MAAE,EAAA,CAAA,EAAA,CAAA"}
@@ -1,4 +1,3 @@
1
- import { resolveAskQuestion } from "../../tools/ask-question/deferred.js";
2
1
  import { AiChatDIKey } from "./di.js";
3
2
  import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, ref, renderList, toDisplayString, unref, watch, withCtx } from "vue";
4
3
  import { UButton, UInput } from "@veltra/desktop";
@@ -9,44 +8,39 @@ const _hoisted_2 = ["aria-checked", "onClick"];
9
8
  const _sfc_main = /*@__PURE__*/ defineComponent({
10
9
  name: "UAiChatAskQuestion",
11
10
  __name: "ask-question",
12
- props: { toolCall: {} },
13
- setup(__props) {
11
+ props: {
12
+ questions: {},
13
+ answers: {},
14
+ error: {}
15
+ },
16
+ emits: ["submit"],
17
+ setup(__props, { emit: __emit }) {
14
18
  const props = __props;
19
+ const emit = __emit;
15
20
  const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
16
- /** 模型输出的提问参数 */
17
- const questions = computed(() => {
18
- try {
19
- const args = JSON.parse(props.toolCall.arguments || "{}");
20
- return Array.isArray(args.questions) ? args.questions : [];
21
- } catch {
22
- return [];
23
- }
24
- });
25
- /** 作答中:execute 挂起等待用户提交 */
26
- const answering = computed(() => {
27
- return props.toolCall.result == null && (props.toolCall.status === "running" || props.toolCall.status === "pending");
28
- });
21
+ /** 作答中:尚未提交、也没有失败文案 */
22
+ const answering = computed(() => props.answers == null && !props.error);
29
23
  /** 当前题索引 */
30
24
  const current = ref(0);
31
25
  /** 各题选中的预设选项 */
32
26
  const selections = ref([]);
33
27
  /** 各题的自定义输入 */
34
28
  const customs = ref([]);
35
- watch(questions, (list) => {
36
- if (props.toolCall.result != null) return;
29
+ watch(() => props.questions, (list) => {
30
+ if (props.answers != null) return;
37
31
  selections.value = list.map(() => null);
38
32
  customs.value = list.map(() => "");
39
33
  }, { immediate: true });
40
- const currentQuestion = computed(() => questions.value[current.value]);
34
+ const currentQuestion = computed(() => props.questions[current.value]);
41
35
  /** 某题是否已作答:自定义输入非空优先,否则看选中项 */
42
36
  const isAnswered = (index) => {
43
37
  return Boolean(customs.value[index]?.trim() || selections.value[index]);
44
38
  };
45
39
  const allAnswered = computed(() => {
46
- return questions.value.length > 0 && questions.value.every((_, i) => isAnswered(i));
40
+ return props.questions.length > 0 && props.questions.every((_, i) => isAnswered(i));
47
41
  });
48
42
  const goTo = (index) => {
49
- if (index < 0 || index >= questions.value.length) return;
43
+ if (index < 0 || index >= props.questions.length) return;
50
44
  current.value = index;
51
45
  };
52
46
  /** 点选选项:再次点击取消选择;选中时清空自定义输入(两者互斥) */
@@ -62,45 +56,32 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
62
56
  if (value) selections.value[current.value] = null;
63
57
  };
64
58
  const handleSubmit = () => {
65
- const firstUnanswered = questions.value.findIndex((_, i) => !isAnswered(i));
59
+ const firstUnanswered = props.questions.findIndex((_, i) => !isAnswered(i));
66
60
  if (firstUnanswered >= 0) {
67
61
  current.value = firstUnanswered;
68
62
  return;
69
63
  }
70
- const answers = questions.value.map((item, i) => ({
64
+ emit("submit", props.questions.map((item, i) => ({
71
65
  question: item.question,
72
66
  answer: customs.value[i]?.trim() || selections.value[i] || ""
73
- }));
74
- resolveAskQuestion(props.toolCall.id, { answers });
67
+ })));
75
68
  };
76
- /** 提交后的问答摘要(含历史恢复:从序列化结果解析) */
69
+ /** 提交后的问答摘要:命中预设选项的回答渲染为 chip */
77
70
  const doneAnswers = computed(() => {
78
- if (props.toolCall.status !== "success" || !props.toolCall.result) return [];
79
- let result;
80
- try {
81
- result = JSON.parse(props.toolCall.result);
82
- } catch {
83
- return [];
84
- }
85
- if (!Array.isArray(result?.answers)) return [];
86
- return result.answers.map((item) => ({
87
- ...item,
88
- /** 命中预设选项的回答渲染为 chip */
89
- isOption: questions.value.some((q) => q.question === item.question && q.options?.includes(item.answer))
71
+ const list = props.answers;
72
+ if (!list?.length) return [];
73
+ return list.map((item) => ({
74
+ question: item.question,
75
+ answer: item.answer,
76
+ isOption: props.questions.some((q) => q.question === item.question && q.options?.includes(item.answer))
90
77
  }));
91
78
  });
92
- const statusText = computed(() => {
93
- if (props.toolCall.status !== "error") return "";
94
- const err = props.toolCall.error ?? "";
95
- if (err.includes("Abort") || err.includes("取消")) return "已取消";
96
- return err || "提问失败";
97
- });
98
79
  return (_ctx, _cache) => {
99
80
  return openBlock(), createElementBlock("div", { class: normalizeClass(unref(cls).e("ask-question")) }, [createCommentVNode(" 作答中:分页向导 "), answering.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
100
- questions.value.length > 1 ? (openBlock(), createElementBlock("div", {
81
+ __props.questions.length > 1 ? (openBlock(), createElementBlock("div", {
101
82
  key: 0,
102
83
  class: normalizeClass(unref(cls).e("ask-question-progress"))
103
- }, [createElementVNode("span", { class: normalizeClass(unref(cls).e("ask-question-count")) }, toDisplayString(current.value + 1) + " / " + toDisplayString(questions.value.length), 3), createElementVNode("span", { class: normalizeClass(unref(cls).e("ask-question-dots")) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(questions.value, (_, i) => {
84
+ }, [createElementVNode("span", { class: normalizeClass(unref(cls).e("ask-question-count")) }, toDisplayString(current.value + 1) + " / " + toDisplayString(__props.questions.length), 3), createElementVNode("span", { class: normalizeClass(unref(cls).e("ask-question-dots")) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.questions, (_, i) => {
104
85
  return openBlock(), createElementBlock("span", {
105
86
  key: i,
106
87
  class: normalizeClass([
@@ -111,7 +92,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
111
92
  onClick: ($event) => goTo(i)
112
93
  }, null, 10, _hoisted_1);
113
94
  }), 128))], 2)], 2)) : createCommentVNode("v-if", true),
114
- createElementVNode("div", { class: normalizeClass(unref(cls).e("ask-question-q")) }, [questions.value.length > 1 ? (openBlock(), createElementBlock("span", {
95
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("ask-question-q")) }, [__props.questions.length > 1 ? (openBlock(), createElementBlock("span", {
115
96
  key: 0,
116
97
  class: normalizeClass(unref(cls).e("ask-question-no")),
117
98
  "aria-hidden": "true"
@@ -138,7 +119,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
138
119
  placeholder: currentQuestion.value?.placeholder ?? "自定义回答…",
139
120
  "onUpdate:modelValue": onCustomInput
140
121
  }, null, 8, ["model-value", "placeholder"]),
141
- createElementVNode("footer", { class: normalizeClass(unref(cls).e("ask-question-actions")) }, [questions.value.length > 1 ? (openBlock(), createBlock(unref(UButton), {
122
+ createElementVNode("footer", { class: normalizeClass(unref(cls).e("ask-question-actions")) }, [__props.questions.length > 1 ? (openBlock(), createBlock(unref(UButton), {
142
123
  key: 0,
143
124
  size: "small",
144
125
  text: "",
@@ -147,7 +128,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
147
128
  }, {
148
129
  default: withCtx(() => [..._cache[2] || (_cache[2] = [createTextVNode(" 上一个 ", -1)])]),
149
130
  _: 1
150
- }, 8, ["disabled"])) : createCommentVNode("v-if", true), current.value < questions.value.length - 1 ? (openBlock(), createBlock(unref(UButton), {
131
+ }, 8, ["disabled"])) : createCommentVNode("v-if", true), current.value < __props.questions.length - 1 ? (openBlock(), createBlock(unref(UButton), {
151
132
  key: 1,
152
133
  size: "small",
153
134
  type: "primary",
@@ -165,7 +146,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
165
146
  default: withCtx(() => [..._cache[4] || (_cache[4] = [createTextVNode(" 提交 ", -1)])]),
166
147
  _: 1
167
148
  }, 8, ["disabled"]))], 2)
168
- ], 64)) : __props.toolCall.status === "success" ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createCommentVNode(" 提交后:问答摘要 "), (openBlock(true), createElementBlock(Fragment, null, renderList(doneAnswers.value, (item, i) => {
149
+ ], 64)) : __props.answers != null ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createCommentVNode(" 提交后:问答摘要 "), (openBlock(true), createElementBlock(Fragment, null, renderList(doneAnswers.value, (item, i) => {
169
150
  return openBlock(), createElementBlock("div", {
170
151
  key: i,
171
152
  class: normalizeClass(unref(cls).e("ask-question-result"))
@@ -177,7 +158,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
177
158
  key: 0,
178
159
  class: normalizeClass(unref(cls).e("ask-question-chip"))
179
160
  }, toDisplayString(item.answer), 3)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(item.answer), 1)], 64))], 2)], 2);
180
- }), 128))], 64)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [createCommentVNode(" 异常 / 取消 "), createElementVNode("div", { class: normalizeClass(unref(cls).e("ask-question-status")) }, toDisplayString(statusText.value), 3)], 2112))], 2);
161
+ }), 128))], 64)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [createCommentVNode(" 异常 / 取消 "), createElementVNode("div", { class: normalizeClass(unref(cls).e("ask-question-status")) }, toDisplayString(__props.error), 3)], 2112))], 2);
181
162
  };
182
163
  }
183
164
  });
@@ -1 +1 @@
1
- {"version":3,"file":"ask-question.js","names":[],"sources":["../../../src/components/ai-chat/ask-question.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('ask-question')\">\n <!-- 作答中:分页向导 -->\n <template v-if=\"answering\">\n <div v-if=\"questions.length > 1\" :class=\"cls.e('ask-question-progress')\">\n <span :class=\"cls.e('ask-question-count')\">{{ current + 1 }} / {{ questions.length }}</span>\n <span :class=\"cls.e('ask-question-dots')\">\n <span\n v-for=\"(_, i) in questions\"\n :key=\"i\"\n :class=\"[\n cls.e('ask-question-dot'),\n bem.is('current', i === current),\n bem.is('answered', isAnswered(i))\n ]\"\n @click=\"goTo(i)\"\n />\n </span>\n </div>\n\n <div :class=\"cls.e('ask-question-q')\">\n <span v-if=\"questions.length > 1\" :class=\"cls.e('ask-question-no')\" aria-hidden=\"true\">\n {{ current + 1 }}\n </span>\n {{ currentQuestion?.question }}\n </div>\n\n <div\n v-if=\"currentQuestion?.options?.length\"\n :class=\"cls.e('ask-question-options')\"\n role=\"radiogroup\"\n >\n <button\n v-for=\"option in currentQuestion.options\"\n :key=\"option\"\n type=\"button\"\n role=\"radio\"\n :aria-checked=\"selections[current] === option\"\n :class=\"[cls.e('ask-question-opt'), bem.is('selected', selections[current] === option)]\"\n @click=\"pickOption(option)\"\n >\n <span\n :class=\"[\n cls.e('ask-question-glyph'),\n bem.is('selected', selections[current] === option)\n ]\"\n aria-hidden=\"true\"\n />\n <span :class=\"cls.e('ask-question-opt-text')\">{{ option }}</span>\n </button>\n </div>\n\n <UInput\n :model-value=\"customs[current]\"\n :placeholder=\"currentQuestion?.placeholder ?? '自定义回答…'\"\n @update:model-value=\"onCustomInput\"\n />\n\n <footer :class=\"cls.e('ask-question-actions')\">\n <UButton\n v-if=\"questions.length > 1\"\n size=\"small\"\n text\n :disabled=\"current === 0\"\n @click=\"goTo(current - 1)\"\n >\n 上一个\n </UButton>\n <UButton\n v-if=\"current < questions.length - 1\"\n size=\"small\"\n type=\"primary\"\n @click=\"goTo(current + 1)\"\n >\n 下一个\n </UButton>\n <UButton v-else size=\"small\" type=\"primary\" :disabled=\"!allAnswered\" @click=\"handleSubmit\">\n 提交\n </UButton>\n </footer>\n </template>\n\n <!-- 提交后:问答摘要 -->\n <template v-else-if=\"toolCall.status === 'success'\">\n <div v-for=\"(item, i) in doneAnswers\" :key=\"i\" :class=\"cls.e('ask-question-result')\">\n <div :class=\"cls.e('ask-question-result-q')\">\n <span\n v-if=\"doneAnswers.length > 1\"\n :class=\"cls.e('ask-question-result-no')\"\n aria-hidden=\"true\"\n >\n {{ i + 1 }}\n </span>\n {{ item.question }}\n </div>\n <div :class=\"cls.e('ask-question-result-a')\">\n <span v-if=\"item.isOption\" :class=\"cls.e('ask-question-chip')\">{{ item.answer }}</span>\n <template v-else>{{ item.answer }}</template>\n </div>\n </div>\n </template>\n\n <!-- 异常 / 取消 -->\n <div v-else :class=\"cls.e('ask-question-status')\">{{ statusText }}</div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UInput } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, watch } from 'vue'\n\nimport type { ChatToolCall } from '../../chat/types'\nimport type {\n AskQuestionArgs,\n AskQuestionItem,\n AskQuestionResult\n} from '../../tools/ask-question/ask-question'\nimport { resolveAskQuestion } from '../../tools/ask-question/deferred'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatAskQuestion' })\n\nconst props = defineProps<{ toolCall: ChatToolCall }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 模型输出的提问参数 */\nconst questions = computed<AskQuestionItem[]>(() => {\n try {\n const args = JSON.parse(props.toolCall.arguments || '{}') as Partial<AskQuestionArgs>\n return Array.isArray(args.questions) ? args.questions : []\n } catch {\n return []\n }\n})\n\n/** 作答中:execute 挂起等待用户提交 */\nconst answering = computed(() => {\n return (\n props.toolCall.result == null &&\n (props.toolCall.status === 'running' || props.toolCall.status === 'pending')\n )\n})\n\n/** 当前题索引 */\nconst current = ref(0)\n/** 各题选中的预设选项 */\nconst selections = ref<(string | null)[]>([])\n/** 各题的自定义输入 */\nconst customs = ref<string[]>([])\n\nwatch(\n questions,\n (list) => {\n if (props.toolCall.result != null) return\n selections.value = list.map(() => null)\n customs.value = list.map(() => '')\n },\n { immediate: true }\n)\n\nconst currentQuestion = computed(() => questions.value[current.value])\n\n/** 某题是否已作答:自定义输入非空优先,否则看选中项 */\nconst isAnswered = (index: number) => {\n return Boolean(customs.value[index]?.trim() || selections.value[index])\n}\n\nconst allAnswered = computed(() => {\n return questions.value.length > 0 && questions.value.every((_, i) => isAnswered(i))\n})\n\nconst goTo = (index: number) => {\n if (index < 0 || index >= questions.value.length) return\n current.value = index\n}\n\n/** 点选选项:再次点击取消选择;选中时清空自定义输入(两者互斥) */\nconst pickOption = (option: string) => {\n const i = current.value\n const selected = selections.value[i] === option ? null : option\n selections.value[i] = selected\n if (selected) customs.value[i] = ''\n}\n\n/** 自定义输入:有内容时清除已选选项(两者互斥) */\nconst onCustomInput = (value: string) => {\n customs.value[current.value] = value\n if (value) selections.value[current.value] = null\n}\n\nconst handleSubmit = () => {\n const firstUnanswered = questions.value.findIndex((_, i) => !isAnswered(i))\n if (firstUnanswered >= 0) {\n current.value = firstUnanswered\n return\n }\n const answers = questions.value.map((item, i) => ({\n question: item.question,\n answer: customs.value[i]?.trim() || selections.value[i] || ''\n }))\n resolveAskQuestion(props.toolCall.id, { answers })\n}\n\n/** 提交后的问答摘要(含历史恢复:从序列化结果解析) */\nconst doneAnswers = computed(() => {\n if (props.toolCall.status !== 'success' || !props.toolCall.result) return []\n let result: AskQuestionResult\n try {\n result = JSON.parse(props.toolCall.result)\n } catch {\n return []\n }\n if (!Array.isArray(result?.answers)) return []\n return result.answers.map((item) => ({\n ...item,\n /** 命中预设选项的回答渲染为 chip */\n isOption: questions.value.some(\n (q) => q.question === item.question && q.options?.includes(item.answer)\n )\n }))\n})\n\nconst statusText = computed(() => {\n if (props.toolCall.status !== 'error') return ''\n const err = props.toolCall.error ?? ''\n if (err.includes('Abort') || err.includes('取消')) return '已取消'\n return err || '提问失败'\n})\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"ask-question.js","names":[],"sources":["../../../src/components/ai-chat/ask-question.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('ask-question')\">\n <!-- 作答中:分页向导 -->\n <template v-if=\"answering\">\n <div v-if=\"questions.length > 1\" :class=\"cls.e('ask-question-progress')\">\n <span :class=\"cls.e('ask-question-count')\">{{ current + 1 }} / {{ questions.length }}</span>\n <span :class=\"cls.e('ask-question-dots')\">\n <span\n v-for=\"(_, i) in questions\"\n :key=\"i\"\n :class=\"[\n cls.e('ask-question-dot'),\n bem.is('current', i === current),\n bem.is('answered', isAnswered(i))\n ]\"\n @click=\"goTo(i)\"\n />\n </span>\n </div>\n\n <div :class=\"cls.e('ask-question-q')\">\n <span v-if=\"questions.length > 1\" :class=\"cls.e('ask-question-no')\" aria-hidden=\"true\">\n {{ current + 1 }}\n </span>\n {{ currentQuestion?.question }}\n </div>\n\n <div\n v-if=\"currentQuestion?.options?.length\"\n :class=\"cls.e('ask-question-options')\"\n role=\"radiogroup\"\n >\n <button\n v-for=\"option in currentQuestion.options\"\n :key=\"option\"\n type=\"button\"\n role=\"radio\"\n :aria-checked=\"selections[current] === option\"\n :class=\"[cls.e('ask-question-opt'), bem.is('selected', selections[current] === option)]\"\n @click=\"pickOption(option)\"\n >\n <span\n :class=\"[\n cls.e('ask-question-glyph'),\n bem.is('selected', selections[current] === option)\n ]\"\n aria-hidden=\"true\"\n />\n <span :class=\"cls.e('ask-question-opt-text')\">{{ option }}</span>\n </button>\n </div>\n\n <UInput\n :model-value=\"customs[current]\"\n :placeholder=\"currentQuestion?.placeholder ?? '自定义回答…'\"\n @update:model-value=\"onCustomInput\"\n />\n\n <footer :class=\"cls.e('ask-question-actions')\">\n <UButton\n v-if=\"questions.length > 1\"\n size=\"small\"\n text\n :disabled=\"current === 0\"\n @click=\"goTo(current - 1)\"\n >\n 上一个\n </UButton>\n <UButton\n v-if=\"current < questions.length - 1\"\n size=\"small\"\n type=\"primary\"\n @click=\"goTo(current + 1)\"\n >\n 下一个\n </UButton>\n <UButton v-else size=\"small\" type=\"primary\" :disabled=\"!allAnswered\" @click=\"handleSubmit\">\n 提交\n </UButton>\n </footer>\n </template>\n\n <!-- 提交后:问答摘要 -->\n <template v-else-if=\"answers != null\">\n <div v-for=\"(item, i) in doneAnswers\" :key=\"i\" :class=\"cls.e('ask-question-result')\">\n <div :class=\"cls.e('ask-question-result-q')\">\n <span\n v-if=\"doneAnswers.length > 1\"\n :class=\"cls.e('ask-question-result-no')\"\n aria-hidden=\"true\"\n >\n {{ i + 1 }}\n </span>\n {{ item.question }}\n </div>\n <div :class=\"cls.e('ask-question-result-a')\">\n <span v-if=\"item.isOption\" :class=\"cls.e('ask-question-chip')\">{{ item.answer }}</span>\n <template v-else>{{ item.answer }}</template>\n </div>\n </div>\n </template>\n\n <!-- 异常 / 取消 -->\n <div v-else :class=\"cls.e('ask-question-status')\">{{ error }}</div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UInput } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, watch } from 'vue'\n\nimport type {\n AskQuestionAnswer,\n AskQuestionItem,\n AskQuestionResult\n} from '../../tools/ask-question/ask-question'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatAskQuestion' })\n\nconst props = defineProps<{\n /** 问题列表;组件不再从 toolCall 解析参数 */\n questions: AskQuestionItem[]\n /** 已提交摘要;有值则展示结果而非表单 */\n answers?: AskQuestionAnswer[]\n /** 失败 / 取消文案 */\n error?: string\n}>()\n\nconst emit = defineEmits<{\n /** 提交问答;值为答案数组(与 AskQuestionResult 兼容) */\n (e: 'submit', value: AskQuestionAnswer[] | AskQuestionResult): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 作答中:尚未提交、也没有失败文案 */\nconst answering = computed(() => props.answers == null && !props.error)\n\n/** 当前题索引 */\nconst current = ref(0)\n/** 各题选中的预设选项 */\nconst selections = ref<(string | null)[]>([])\n/** 各题的自定义输入 */\nconst customs = ref<string[]>([])\n\nwatch(\n () => props.questions,\n (list) => {\n if (props.answers != null) return\n selections.value = list.map(() => null)\n customs.value = list.map(() => '')\n },\n { immediate: true }\n)\n\nconst currentQuestion = computed(() => props.questions[current.value])\n\n/** 某题是否已作答:自定义输入非空优先,否则看选中项 */\nconst isAnswered = (index: number) => {\n return Boolean(customs.value[index]?.trim() || selections.value[index])\n}\n\nconst allAnswered = computed(() => {\n return props.questions.length > 0 && props.questions.every((_, i) => isAnswered(i))\n})\n\nconst goTo = (index: number) => {\n if (index < 0 || index >= props.questions.length) return\n current.value = index\n}\n\n/** 点选选项:再次点击取消选择;选中时清空自定义输入(两者互斥) */\nconst pickOption = (option: string) => {\n const i = current.value\n const selected = selections.value[i] === option ? null : option\n selections.value[i] = selected\n if (selected) customs.value[i] = ''\n}\n\n/** 自定义输入:有内容时清除已选选项(两者互斥) */\nconst onCustomInput = (value: string) => {\n customs.value[current.value] = value\n if (value) selections.value[current.value] = null\n}\n\nconst handleSubmit = () => {\n const firstUnanswered = props.questions.findIndex((_, i) => !isAnswered(i))\n if (firstUnanswered >= 0) {\n current.value = firstUnanswered\n return\n }\n emit(\n 'submit',\n props.questions.map((item, i) => ({\n question: item.question,\n answer: customs.value[i]?.trim() || selections.value[i] || ''\n }))\n )\n}\n\n/** 提交后的问答摘要:命中预设选项的回答渲染为 chip */\nconst doneAnswers = computed(() => {\n const list = props.answers\n if (!list?.length) return []\n return list.map((item) => ({\n question: item.question,\n answer: item.answer,\n isOption: props.questions.some(\n (q) => q.question === item.question && q.options?.includes(item.answer)\n )\n }))\n})\n</script>\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { AiChatDIKey } from "./di.js";
2
2
  import _sfc_main$1 from "./model-picker.js";
3
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeModels, nextTick, normalizeClass, openBlock, ref, renderList, shallowRef, toDisplayString, unref, useModel, vModelText, withCtx, withDirectives, withKeys, withModifiers } from "vue";
3
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeModels, nextTick, normalizeClass, openBlock, ref, renderList, shallowRef, toDisplayString, unref, useModel, vModelText, watch, withCtx, withDirectives, withKeys, withModifiers } from "vue";
4
4
  import { Attach, Clear, Close, Up } from "@veltra/icons/normal";
5
5
  import { UButton, UFilePicker, UIcon, UPopConfirm } from "@veltra/desktop";
6
6
  import { bem } from "@veltra/utils";
@@ -8,6 +8,8 @@ import { bem } from "@veltra/utils";
8
8
  const _hoisted_1 = ["src", "alt"];
9
9
  const _hoisted_2 = ["placeholder", "onKeydown"];
10
10
  const _hoisted_3 = ["title"];
11
+ const MAX_INPUT_HEIGHT = 160;
12
+ /** 多行自适应高度,上限 160px */
11
13
  const _sfc_main = /*@__PURE__*/ defineComponent({
12
14
  name: "UAiChatInput",
13
15
  __name: "chat-input",
@@ -72,13 +74,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
72
74
  if (props.running) return "会话进行中,发送的消息将进入待发送队列";
73
75
  return props.placeholder ?? "输入消息,Enter 发送,Shift + Enter 换行";
74
76
  });
75
- /** 多行自适应高度,上限 160px */
76
77
  const autoResize = () => {
77
78
  const el = textareaRef.value;
78
79
  if (!el) return;
79
80
  el.style.height = "auto";
80
- el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
81
+ const nextHeight = Math.min(el.scrollHeight, MAX_INPUT_HEIGHT);
82
+ el.style.height = `${nextHeight}px`;
83
+ el.style.overflowY = el.scrollHeight > MAX_INPUT_HEIGHT ? "auto" : "hidden";
81
84
  };
85
+ watch(placeholderText, () => {
86
+ nextTick(autoResize);
87
+ });
82
88
  const handleSend = () => {
83
89
  if (!hasContent.value) return;
84
90
  emit("send", text.value.trim(), attachments.value);
@@ -1 +1 @@
1
- {"version":3,"file":"chat-input.js","names":[],"sources":["../../../src/components/ai-chat/chat-input.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('input-area')\">\n <div v-if=\"attachments.length\" :class=\"cls.e('input-attachments')\">\n <div\n v-for=\"(att, index) in attachments\"\n :key=\"att.dataUrl\"\n :class=\"cls.e('input-attachment')\"\n >\n <img :src=\"att.dataUrl\" :alt=\"att.name\" />\n <UIcon :class=\"cls.e('input-attachment-remove')\" @click=\"attachments.splice(index, 1)\">\n <Close />\n </UIcon>\n </div>\n </div>\n\n <textarea\n ref=\"textareaRef\"\n v-model=\"text\"\n :class=\"cls.e('input')\"\n :placeholder=\"placeholderText\"\n rows=\"1\"\n @keydown.enter.exact.prevent=\"handleSend\"\n @input=\"autoResize\"\n />\n\n <div :class=\"cls.e('input-toolbar')\">\n <div :class=\"cls.e('input-toolbar-left')\">\n <UFilePicker multiple :accept=\"accept ?? 'image/*'\" @pick=\"handlePick\">\n <UIcon :class=\"cls.e('input-attach')\" title=\"添加图片\">\n <Attach />\n </UIcon>\n </UFilePicker>\n\n <span :class=\"[cls.e('input-clear-wrap'), bem.is('disabled', !clearable)]\">\n <UPopConfirm\n title=\"清空当前对话?进行中的生成将被中止。\"\n confirm-text=\"清空\"\n direction=\"top\"\n alignment=\"start\"\n @confirm=\"emit('clear')\"\n >\n <template #reference>\n <UIcon :class=\"cls.e('input-clear')\" title=\"清除会话\">\n <Clear />\n </UIcon>\n </template>\n </UPopConfirm>\n </span>\n\n <span v-if=\"tokenUsage\" :class=\"cls.e('input-usage')\" :title=\"usageText\">\n {{ usageText }}\n </span>\n </div>\n\n <!-- 右簇:模型/推理选择 → 发送或停止(互斥) -->\n <div :class=\"cls.e('input-toolbar-right')\">\n <ModelPicker\n v-if=\"models?.length\"\n v-model:model=\"model\"\n v-model:reasoning-level=\"reasoningLevel\"\n :models=\"models\"\n />\n\n <UButton\n v-if=\"showStop\"\n key=\"stop\"\n size=\"small\"\n type=\"danger\"\n circle\n title=\"停止生成\"\n @click=\"emit('abort')\"\n >\n <span :class=\"cls.e('input-stop')\" />\n </UButton>\n <UButton\n v-else\n key=\"send\"\n size=\"small\"\n type=\"primary\"\n circle\n :disabled=\"!hasContent\"\n :icon=\"Up\"\n :title=\"running ? '加入待发送队列' : undefined\"\n @click=\"handleSend\"\n />\n </div>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UFilePicker, UIcon, UPopConfirm } from '@veltra/desktop'\nimport { Attach, Clear, Close, Up } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, ref, shallowRef } from 'vue'\n\nimport type { ChatAttachment, ChatTokenUsage } from '../../chat/types'\nimport type { ChatModelOption } from '../../providers'\nimport { AiChatDIKey } from './di'\nimport ModelPicker from './model-picker.vue'\n\ndefineOptions({ name: 'UAiChatInput' })\n\nconst props = defineProps<{\n /** 是否生成中(空输入显示停止,有内容则发送入队) */\n running: boolean\n /** 可选模型列表;有值则显示选择器 */\n models?: ChatModelOption[]\n /** 输入框占位文本 */\n placeholder?: string\n /** 附件 accept 类型 */\n accept?: string\n /** 单个附件最大字节数 */\n maxAttachmentSize?: number\n /** 是否可清除(有消息 / 队列 / 生成中) */\n clearable?: boolean\n /** 会话累计 token;无 usage 时为 null,不展示 */\n tokenUsage?: ChatTokenUsage | null\n /** 是否展示缓存命中 / 未命中(默认只显示总 token) */\n tokenUsageDetail?: boolean\n}>()\n\nconst model = defineModel<string>('model')\nconst reasoningLevel = defineModel<string>('reasoningLevel')\n\nconst emit = defineEmits<{\n (e: 'send', content: string, attachments: ChatAttachment[]): void\n (e: 'abort'): void\n (e: 'clear'): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst text = ref('')\nconst attachments = ref<ChatAttachment[]>([])\nconst textareaRef = shallowRef<HTMLTextAreaElement>()\n\nconst hasContent = computed(() => !!text.value.trim() || attachments.value.length > 0)\n\n/** 生成中且输入为空时显示停止;有内容则显示发送(入队) */\nconst showStop = computed(() => props.running && !hasContent.value)\n\n/**\n * 用量数字:不到 1000 原样;≥1000 用 K;≥100 万用 M。\n * 最多 1 位小数,整数不写 `.0`(1000 → 1K,1500 → 1.5K)。\n */\nconst formatTokenCount = (n: number) => {\n const scaled = (value: number, unit: 'K' | 'M') => {\n const rounded = Math.round(value * 10) / 10\n return `${Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)}${unit}`\n }\n if (n < 1000) return String(n)\n if (n < 1_000_000) {\n const k = Math.round((n / 1000) * 10) / 10\n // 四舍五入后会变成 1000K 时改用 M\n return k >= 1000 ? scaled(n / 1_000_000, 'M') : scaled(n / 1000, 'K')\n }\n return scaled(n / 1_000_000, 'M')\n}\n\n/** 会话累计:总 token;明细再拼有数据的缓存项,缺字段不写 0 */\nconst usageText = computed(() => {\n const usage = props.tokenUsage\n if (!usage) return ''\n const parts = [`总 token ${formatTokenCount(usage.totalTokens)}`]\n if (props.tokenUsageDetail) {\n if (usage.cacheHitTokens != null) {\n parts.push(`缓存命中 ${formatTokenCount(usage.cacheHitTokens)}`)\n }\n if (usage.cacheMissTokens != null) {\n parts.push(`缓存未命中 ${formatTokenCount(usage.cacheMissTokens)}`)\n }\n }\n return parts.join(' · ')\n})\n\n/** 生成中提示用户消息将进入队列 */\nconst placeholderText = computed(() => {\n if (props.running) return '会话进行中,发送的消息将进入待发送队列'\n return props.placeholder ?? '输入消息,Enter 发送,Shift + Enter 换行'\n})\n\n/** 多行自适应高度,上限 160px */\nconst autoResize = () => {\n const el = textareaRef.value\n if (!el) return\n el.style.height = 'auto'\n el.style.height = `${Math.min(el.scrollHeight, 160)}px`\n}\n\nconst handleSend = () => {\n if (!hasContent.value) return\n emit('send', text.value.trim(), attachments.value)\n text.value = ''\n attachments.value = []\n autoResize()\n}\n\n/** 取回内容到输入框(队列编辑场景),并聚焦 */\nconst setContent = (content: string) => {\n text.value = content\n void nextTick(() => {\n autoResize()\n textareaRef.value?.focus()\n })\n}\n\n/** 读取当前输入框内容 */\nconst getContent = () => text.value\n\ndefineExpose({ setContent, getContent })\n\n/** 将拾取的文件转为 dataUrl 附件(超限忽略) */\nconst handlePick = (files: File[]) => {\n const maxSize = props.maxAttachmentSize ?? 10 * 1024 * 1024\n\n for (const file of files) {\n if (file.size > maxSize) {\n console.warn(`[UAiChat] 附件 ${file.name} 超过大小限制,已忽略`)\n continue\n }\n const reader = new FileReader()\n reader.onload = () => {\n attachments.value.push({\n name: file.name,\n mimeType: file.type,\n size: file.size,\n dataUrl: String(reader.result)\n })\n }\n reader.readAsDataURL(file)\n }\n}\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"chat-input.js","names":[],"sources":["../../../src/components/ai-chat/chat-input.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('input-area')\">\n <div v-if=\"attachments.length\" :class=\"cls.e('input-attachments')\">\n <div\n v-for=\"(att, index) in attachments\"\n :key=\"att.dataUrl\"\n :class=\"cls.e('input-attachment')\"\n >\n <img :src=\"att.dataUrl\" :alt=\"att.name\" />\n <UIcon :class=\"cls.e('input-attachment-remove')\" @click=\"attachments.splice(index, 1)\">\n <Close />\n </UIcon>\n </div>\n </div>\n\n <textarea\n ref=\"textareaRef\"\n v-model=\"text\"\n :class=\"cls.e('input')\"\n :placeholder=\"placeholderText\"\n rows=\"1\"\n @keydown.enter.exact.prevent=\"handleSend\"\n @input=\"autoResize\"\n />\n\n <div :class=\"cls.e('input-toolbar')\">\n <div :class=\"cls.e('input-toolbar-left')\">\n <UFilePicker multiple :accept=\"accept ?? 'image/*'\" @pick=\"handlePick\">\n <UIcon :class=\"cls.e('input-attach')\" title=\"添加图片\">\n <Attach />\n </UIcon>\n </UFilePicker>\n\n <span :class=\"[cls.e('input-clear-wrap'), bem.is('disabled', !clearable)]\">\n <UPopConfirm\n title=\"清空当前对话?进行中的生成将被中止。\"\n confirm-text=\"清空\"\n direction=\"top\"\n alignment=\"start\"\n @confirm=\"emit('clear')\"\n >\n <template #reference>\n <UIcon :class=\"cls.e('input-clear')\" title=\"清除会话\">\n <Clear />\n </UIcon>\n </template>\n </UPopConfirm>\n </span>\n\n <span v-if=\"tokenUsage\" :class=\"cls.e('input-usage')\" :title=\"usageText\">\n {{ usageText }}\n </span>\n </div>\n\n <!-- 右簇:模型/推理选择 → 发送或停止(互斥) -->\n <div :class=\"cls.e('input-toolbar-right')\">\n <ModelPicker\n v-if=\"models?.length\"\n v-model:model=\"model\"\n v-model:reasoning-level=\"reasoningLevel\"\n :models=\"models\"\n />\n\n <UButton\n v-if=\"showStop\"\n key=\"stop\"\n size=\"small\"\n type=\"danger\"\n circle\n title=\"停止生成\"\n @click=\"emit('abort')\"\n >\n <span :class=\"cls.e('input-stop')\" />\n </UButton>\n <UButton\n v-else\n key=\"send\"\n size=\"small\"\n type=\"primary\"\n circle\n :disabled=\"!hasContent\"\n :icon=\"Up\"\n :title=\"running ? '加入待发送队列' : undefined\"\n @click=\"handleSend\"\n />\n </div>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UFilePicker, UIcon, UPopConfirm } from '@veltra/desktop'\nimport { Attach, Clear, Close, Up } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, ref, shallowRef, watch } from 'vue'\n\nimport type { ChatAttachment, ChatTokenUsage } from '../../chat/types'\nimport type { ChatModelOption } from '../../providers'\nimport { AiChatDIKey } from './di'\nimport ModelPicker from './model-picker.vue'\n\ndefineOptions({ name: 'UAiChatInput' })\n\nconst props = defineProps<{\n /** 是否生成中(空输入显示停止,有内容则发送入队) */\n running: boolean\n /** 可选模型列表;有值则显示选择器 */\n models?: ChatModelOption[]\n /** 输入框占位文本 */\n placeholder?: string\n /** 附件 accept 类型 */\n accept?: string\n /** 单个附件最大字节数 */\n maxAttachmentSize?: number\n /** 是否可清除(有消息 / 队列 / 生成中) */\n clearable?: boolean\n /** 会话累计 token;无 usage 时为 null,不展示 */\n tokenUsage?: ChatTokenUsage | null\n /** 是否展示缓存命中 / 未命中(默认只显示总 token) */\n tokenUsageDetail?: boolean\n}>()\n\nconst model = defineModel<string>('model')\nconst reasoningLevel = defineModel<string>('reasoningLevel')\n\nconst emit = defineEmits<{\n (e: 'send', content: string, attachments: ChatAttachment[]): void\n (e: 'abort'): void\n (e: 'clear'): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst text = ref('')\nconst attachments = ref<ChatAttachment[]>([])\nconst textareaRef = shallowRef<HTMLTextAreaElement>()\n\nconst hasContent = computed(() => !!text.value.trim() || attachments.value.length > 0)\n\n/** 生成中且输入为空时显示停止;有内容则显示发送(入队) */\nconst showStop = computed(() => props.running && !hasContent.value)\n\n/**\n * 用量数字:不到 1000 原样;≥1000 用 K;≥100 万用 M。\n * 最多 1 位小数,整数不写 `.0`(1000 → 1K,1500 → 1.5K)。\n */\nconst formatTokenCount = (n: number) => {\n const scaled = (value: number, unit: 'K' | 'M') => {\n const rounded = Math.round(value * 10) / 10\n return `${Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)}${unit}`\n }\n if (n < 1000) return String(n)\n if (n < 1_000_000) {\n const k = Math.round((n / 1000) * 10) / 10\n // 四舍五入后会变成 1000K 时改用 M\n return k >= 1000 ? scaled(n / 1_000_000, 'M') : scaled(n / 1000, 'K')\n }\n return scaled(n / 1_000_000, 'M')\n}\n\n/** 会话累计:总 token;明细再拼有数据的缓存项,缺字段不写 0 */\nconst usageText = computed(() => {\n const usage = props.tokenUsage\n if (!usage) return ''\n const parts = [`总 token ${formatTokenCount(usage.totalTokens)}`]\n if (props.tokenUsageDetail) {\n if (usage.cacheHitTokens != null) {\n parts.push(`缓存命中 ${formatTokenCount(usage.cacheHitTokens)}`)\n }\n if (usage.cacheMissTokens != null) {\n parts.push(`缓存未命中 ${formatTokenCount(usage.cacheMissTokens)}`)\n }\n }\n return parts.join(' · ')\n})\n\n/** 生成中提示用户消息将进入队列 */\nconst placeholderText = computed(() => {\n if (props.running) return '会话进行中,发送的消息将进入待发送队列'\n return props.placeholder ?? '输入消息,Enter 发送,Shift + Enter 换行'\n})\n\nconst MAX_INPUT_HEIGHT = 160\n\n/** 多行自适应高度,上限 160px */\nconst autoResize = () => {\n const el = textareaRef.value\n if (!el) return\n el.style.height = 'auto'\n const nextHeight = Math.min(el.scrollHeight, MAX_INPUT_HEIGHT)\n el.style.height = `${nextHeight}px`\n el.style.overflowY = el.scrollHeight > MAX_INPUT_HEIGHT ? 'auto' : 'hidden'\n}\n\nwatch(placeholderText, () => {\n void nextTick(autoResize)\n})\n\nconst handleSend = () => {\n if (!hasContent.value) return\n emit('send', text.value.trim(), attachments.value)\n text.value = ''\n attachments.value = []\n autoResize()\n}\n\n/** 取回内容到输入框(队列编辑场景),并聚焦 */\nconst setContent = (content: string) => {\n text.value = content\n void nextTick(() => {\n autoResize()\n textareaRef.value?.focus()\n })\n}\n\n/** 读取当前输入框内容 */\nconst getContent = () => text.value\n\ndefineExpose({ setContent, getContent })\n\n/** 将拾取的文件转为 dataUrl 附件(超限忽略) */\nconst handlePick = (files: File[]) => {\n const maxSize = props.maxAttachmentSize ?? 10 * 1024 * 1024\n\n for (const file of files) {\n if (file.size > maxSize) {\n console.warn(`[UAiChat] 附件 ${file.name} 超过大小限制,已忽略`)\n continue\n }\n const reader = new FileReader()\n reader.onload = () => {\n attachments.value.push({\n name: file.name,\n mimeType: file.type,\n size: file.size,\n dataUrl: String(reader.result)\n })\n }\n reader.readAsDataURL(file)\n }\n}\n</script>\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"di.js","names":[],"sources":["../../../src/components/ai-chat/di.ts"],"sourcesContent":["import type { BEM } from '@veltra/utils'\nimport type { ComputedRef, InjectionKey, Slots } from 'vue'\n\nimport type { ChatTool } from '../../chat/types'\n\nexport interface AiChatContext {\n /** ai-chat 组件的 BEM 类生成器 */\n cls: BEM<'ai-chat'>\n /** ai-chat 使用方提供的插槽(用于 tool-<name> 动态插槽) */\n slots: Slots\n /** 按 name 索引的工具定义(解析 icon/label/render/autoCollapse) */\n tools: ComputedRef<Record<string, ChatTool | undefined>>\n /** 打开右侧面板并聚焦到指定工具调用(renderTo: 'panel' 工具的「查看面板」入口) */\n openPanel: (toolCallId: string) => void\n}\n\nexport const AiChatDIKey: InjectionKey<AiChatContext> = Symbol('AiChatDIKey')\n"],"mappings":";AAgBA,MAAa,cAA2C,OAAO,aAAa"}
1
+ {"version":3,"file":"di.js","names":[],"sources":["../../../src/components/ai-chat/di.ts"],"sourcesContent":["import type { BEM } from '@veltra/utils'\nimport type { Component, ComputedRef, InjectionKey, Slots } from 'vue'\n\nimport type { ChatTool, ChatToolMeta } from '../../chat/types'\n\nexport interface AiChatContext {\n /** ai-chat 组件的 BEM 类生成器 */\n cls: BEM<'ai-chat'>\n /** ai-chat 使用方提供的插槽(用于 tool-<name> 动态插槽) */\n slots: Slots\n /** 按 name 索引的工具定义或渲染元信息(解析 icon/label/render/autoCollapse) */\n tools: ComputedRef<Record<string, ChatTool | ChatToolMeta | undefined>>\n /** 宿主覆盖的 toolName → icon;缺省走包内名称规则 */\n toolIcons: ComputedRef<Record<string, Component> | undefined>\n /** 打开右侧面板并聚焦到指定工具调用(renderTo: 'panel' 工具的「查看面板」入口) */\n openPanel: (toolCallId: string) => void\n}\n\nexport const AiChatDIKey: InjectionKey<AiChatContext> = Symbol('AiChatDIKey')\n"],"mappings":";AAkBA,MAAa,cAA2C,OAAO,aAAa"}
@@ -0,0 +1,48 @@
1
+ import { AiChatDIKey } from "./di.js";
2
+ import { resolveToolIcon } from "./tool-icons.js";
3
+ import { Fragment, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, ref, renderList, resolveDynamicComponent, toDisplayString, unref, withCtx } from "vue";
4
+ import { ArrowRight } from "@veltra/icons/normal";
5
+ import { UIcon } from "@veltra/desktop";
6
+ import { bem } from "@veltra/utils";
7
+ //#region src/components/ai-chat/job-bar.vue
8
+ const _sfc_main = /*@__PURE__*/ defineComponent({
9
+ name: "UAiChatJobBar",
10
+ __name: "job-bar",
11
+ props: { jobs: {} },
12
+ setup(__props) {
13
+ const expanded = ref(true);
14
+ const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
15
+ return (_ctx, _cache) => {
16
+ return __props.jobs.length ? (openBlock(), createElementBlock("div", {
17
+ key: 0,
18
+ class: normalizeClass(unref(cls).e("job-bar"))
19
+ }, [createElementVNode("button", {
20
+ type: "button",
21
+ class: normalizeClass(unref(cls).e("job-bar-head")),
22
+ onClick: _cache[0] || (_cache[0] = ($event) => expanded.value = !expanded.value)
23
+ }, [createElementVNode("span", null, "作业 · " + toDisplayString(__props.jobs.length), 1), createVNode(unref(UIcon), { class: normalizeClass([unref(cls).e("job-bar-chevron"), unref(bem).is("expanded", expanded.value)]) }, {
24
+ default: withCtx(() => [createVNode(unref(ArrowRight))]),
25
+ _: 1
26
+ }, 8, ["class"])], 2), expanded.value ? (openBlock(), createElementBlock("div", {
27
+ key: 0,
28
+ class: normalizeClass(unref(cls).e("job-bar-list"))
29
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.jobs, (job) => {
30
+ return openBlock(), createElementBlock("div", {
31
+ key: job.id,
32
+ class: normalizeClass(unref(cls).e("job-bar-item"))
33
+ }, [
34
+ createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("job-bar-icon")) }, {
35
+ default: withCtx(() => [(openBlock(), createBlock(resolveDynamicComponent(unref(resolveToolIcon)(job.kind))))]),
36
+ _: 2
37
+ }, 1032, ["class"]),
38
+ createElementVNode("span", { class: normalizeClass([unref(cls).e("job-bar-label"), job.status === "running" && "u-shine"]) }, toDisplayString(job.label), 3),
39
+ createElementVNode("span", { class: normalizeClass([unref(cls).e("job-bar-dot"), unref(bem).is(job.status)]) }, null, 2)
40
+ ], 2);
41
+ }), 128))], 2)) : createCommentVNode("v-if", true)], 2)) : createCommentVNode("v-if", true);
42
+ };
43
+ }
44
+ });
45
+ //#endregion
46
+ export { _sfc_main as default };
47
+
48
+ //# sourceMappingURL=job-bar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-bar.js","names":[],"sources":["../../../src/components/ai-chat/job-bar.vue"],"sourcesContent":["<template>\n <div v-if=\"jobs.length\" :class=\"cls.e('job-bar')\">\n <button type=\"button\" :class=\"cls.e('job-bar-head')\" @click=\"expanded = !expanded\">\n <span>作业 · {{ jobs.length }}</span>\n <UIcon :class=\"[cls.e('job-bar-chevron'), bem.is('expanded', expanded)]\">\n <ArrowRight />\n </UIcon>\n </button>\n <div v-if=\"expanded\" :class=\"cls.e('job-bar-list')\">\n <div v-for=\"job in jobs\" :key=\"job.id\" :class=\"cls.e('job-bar-item')\">\n <UIcon :class=\"cls.e('job-bar-icon')\">\n <component :is=\"resolveToolIcon(job.kind)\" />\n </UIcon>\n <span :class=\"[cls.e('job-bar-label'), job.status === 'running' && 'u-shine']\">\n {{ job.label }}\n </span>\n <span :class=\"[cls.e('job-bar-dot'), bem.is(job.status)]\" />\n </div>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon } from '@veltra/desktop'\nimport { ArrowRight } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { inject, ref } from 'vue'\n\nimport type { ChatJob } from '../../chat/types'\nimport { AiChatDIKey } from './di'\nimport { resolveToolIcon } from './tool-icons'\n\ndefineOptions({ name: 'UAiChatJobBar' })\n\ndefineProps<{ jobs: ChatJob[] }>()\n\nconst expanded = ref(true)\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n</script>\n"],"mappings":";;;;;;;;;;;;EAoCA,MAAM,WAAW,IAAI,IAAI;EAGzB,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;;GAtCvB,OAAA,QAAA,KAAK,UAAhB,UAAA,GAAA,mBAkBM,OAAA;;IAlBmB,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,SAAA,CAAA;GACnC,GAAA,CAAA,mBAKS,UAAA;IALD,MAAK;IAAU,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;IAAmB,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,SAAA,QAAQ,CAAI,SAAA;GACvE,GAAA,CAAA,mBAAmC,QAAA,MAA7B,UAAK,gBAAG,QAAA,KAAK,MAAM,GAAA,CAAA,GACzB,YAEQ,MAAA,KAAA,GAAA,EAFA,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,iBAAA,GAAqB,MAAA,GAAA,CAAG,CAAC,GAAE,YAAa,SAAA,KAAQ,CAAA,CAAA,EAAA,GAAA;IACnE,SAAA,cAAc,CAAd,YAAc,MAAA,UAAA,CAAA,CAAA,CAAA;;GAGP,GAAA,GAAA,CAAA,OAAA,CAAA,CAAA,GAAA,CAAA,GAAA,SAAA,SAAX,UAAA,GAAA,mBAUM,OAAA;;IAVgB,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;GAChC,GAAA,EAAA,UAAA,IAAA,GAAA,mBAQM,UAAA,MAAA,WARa,QAAA,OAAP,QAAG;IAAf,OAAA,UAAA,GAAA,mBAQM,OAAA;KARoB,KAAK,IAAI;KAAK,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;;KAClD,YAEQ,MAAA,KAAA,GAAA,EAFA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA,EAAA,GAAA;MAClB,SAAA,cAA6C,EAA7C,UAAA,GAAA,YAA6C,wBAA7B,MAAA,eAAA,CAAe,CAAC,IAAI,IAAI,CAAA,CAAA,EAAA,CAAA;;;KAE1C,mBAEO,QAAA,EAFA,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,eAAA,GAAmB,IAAI,WAAM,aAAA,SAAA,CAAA,EAC5C,GAAA,gBAAA,IAAI,KAAK,GAAA,CAAA;KAEd,mBAA4D,QAAA,EAArD,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,aAAA,GAAiB,MAAA,GAAA,CAAG,CAAC,GAAG,IAAI,MAAM,CAAA,CAAA,EAAA,GAAA,MAAA,CAAA"}
@@ -7,6 +7,7 @@ import { ArrowDown } from "@veltra/icons/normal";
7
7
  import { UIcon, UScroll } from "@veltra/desktop";
8
8
  import { bem } from "@veltra/utils";
9
9
  //#region src/components/ai-chat/message-list.vue
10
+ const _hoisted_1 = ["disabled"];
10
11
  const ORB_SIZE = 48;
11
12
  /** 生成结束 / 失败后工作球停留再跳回输入框上方 */
12
13
  const WORKING_LINGER_MS = 2500;
@@ -19,6 +20,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
19
20
  messages: {},
20
21
  welcome: {},
21
22
  running: { type: Boolean },
23
+ readonly: { type: Boolean },
22
24
  rendererProps: {}
23
25
  },
24
26
  emits: [
@@ -346,8 +348,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
346
348
  key: welcomeIndex.value,
347
349
  type: "button",
348
350
  class: normalizeClass(unref(cls).e("welcome-item")),
349
- onClick: _cache[7] || (_cache[7] = ($event) => emit("welcome-click", currentWelcomeItem.value))
350
- }, toDisplayString(currentWelcomeItem.value), 3)) : createCommentVNode("v-if", true)]),
351
+ disabled: __props.readonly,
352
+ onClick: _cache[7] || (_cache[7] = ($event) => !__props.readonly && emit("welcome-click", currentWelcomeItem.value))
353
+ }, toDisplayString(currentWelcomeItem.value), 11, _hoisted_1)) : createCommentVNode("v-if", true)]),
351
354
  _: 1
352
355
  }, 8, ["name"])], 2)) : createCommentVNode("v-if", true)
353
356
  ], 2)])], 2)) : createCommentVNode("v-if", true),