@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.
- package/README.md +42 -0
- package/dist/chat/fold.d.ts +41 -0
- package/dist/chat/fold.js +208 -0
- package/dist/chat/fold.js.map +1 -0
- package/dist/chat/index.d.ts +4 -2
- package/dist/chat/session.d.ts +123 -0
- package/dist/chat/session.js +79 -0
- package/dist/chat/session.js.map +1 -0
- package/dist/chat/types.d.ts +38 -1
- package/dist/chat/use-chat-helpers.js +39 -0
- package/dist/chat/use-chat-helpers.js.map +1 -0
- package/dist/chat/use-chat-session.js +68 -0
- package/dist/chat/use-chat-session.js.map +1 -0
- package/dist/chat/use-chat.d.ts +50 -2
- package/dist/chat/use-chat.js +73 -39
- package/dist/chat/use-chat.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js +49 -14
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/approval-banner.js +54 -0
- package/dist/components/ai-chat/approval-banner.js.map +1 -0
- package/dist/components/ai-chat/ask-question.js +32 -51
- package/dist/components/ai-chat/ask-question.js.map +1 -1
- package/dist/components/ai-chat/chat-input.js +9 -3
- package/dist/components/ai-chat/chat-input.js.map +1 -1
- package/dist/components/ai-chat/di.js.map +1 -1
- package/dist/components/ai-chat/job-bar.js +48 -0
- package/dist/components/ai-chat/job-bar.js.map +1 -0
- package/dist/components/ai-chat/message-list.js +5 -2
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/queue-list.js +9 -3
- package/dist/components/ai-chat/queue-list.js.map +1 -1
- package/dist/components/ai-chat/side-panel.js +12 -30
- package/dist/components/ai-chat/side-panel.js.map +1 -1
- package/dist/components/ai-chat/style2.css +150 -8
- package/dist/components/ai-chat/tool-call.js +51 -40
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/components/ai-chat/tool-icons.js +31 -0
- package/dist/components/ai-chat/tool-icons.js.map +1 -0
- package/dist/components/ai-chat/tool-json.js +40 -0
- package/dist/components/ai-chat/tool-json.js.map +1 -0
- package/dist/components/ai-orb/style2.css +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -1
- package/dist/tools/ask-question/ask-question.js +48 -2
- package/dist/tools/ask-question/ask-question.js.map +1 -1
- package/dist/types/ai-chat.d.ts +17 -6
- package/package.json +7 -7
- package/src/chat/__test__/fold.test.ts +136 -0
- package/src/chat/__test__/session.test.ts +191 -0
- package/src/chat/__test__/use-chat.test.ts +268 -1
- package/src/chat/fold.ts +260 -0
- package/src/chat/index.ts +2 -0
- package/src/chat/session.ts +168 -0
- package/src/chat/types.ts +40 -0
- package/src/chat/use-chat-helpers.ts +39 -0
- package/src/chat/use-chat-session.ts +87 -0
- package/src/chat/use-chat.ts +90 -44
- package/src/components/ai-chat/__test__/ai-chat.test.ts +405 -7
- package/src/components/ai-chat/__test__/tool-icons.test.ts +33 -0
- package/src/components/ai-chat/ai-chat.vue +39 -4
- package/src/components/ai-chat/approval-banner.vue +37 -0
- package/src/components/ai-chat/ask-question.vue +38 -54
- package/src/components/ai-chat/chat-input.vue +10 -2
- package/src/components/ai-chat/di.ts +6 -4
- package/src/components/ai-chat/job-bar.vue +41 -0
- package/src/components/ai-chat/message-list.vue +4 -1
- package/src/components/ai-chat/queue-list.vue +3 -1
- package/src/components/ai-chat/side-panel.vue +15 -13
- package/src/components/ai-chat/style.scss +162 -6
- package/src/components/ai-chat/tool-call.vue +46 -34
- package/src/components/ai-chat/tool-icons.ts +33 -0
- package/src/components/ai-chat/tool-json.vue +52 -0
- package/src/tools/ask-question/ask-question.ts +53 -4
- 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":"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"}
|
|
@@ -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: {
|
|
13
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
350
|
-
|
|
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),
|