@veltra/ai 2.1.8 → 2.1.10

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 (40) hide show
  1. package/dist/chat/fold.d.ts +4 -5
  2. package/dist/chat/session.d.ts +5 -6
  3. package/dist/chat/transports/openai.d.ts +3 -4
  4. package/dist/chat/types.d.ts +15 -16
  5. package/dist/chat/use-chat.d.ts +2 -3
  6. package/dist/components/ai-chat/message-item.js +18 -8
  7. package/dist/components/ai-chat/message-item.js.map +1 -1
  8. package/dist/components/ai-chat/message-list.js +3 -3
  9. package/dist/components/ai-chat/message-list.js.map +1 -1
  10. package/dist/components/ai-chat/style.js +1 -0
  11. package/dist/components/ai-chat/style2.css +20 -7
  12. package/dist/components/ai-chat/tool-call.js +18 -11
  13. package/dist/components/ai-chat/tool-call.js.map +1 -1
  14. package/dist/index.d.ts +1 -3
  15. package/dist/index.js +1 -2
  16. package/dist/providers/types.d.ts +4 -5
  17. package/dist/style.js +0 -1
  18. package/dist/tools/ask-question/ask-question.d.ts +4 -5
  19. package/dist/tools/ask-question/ask-question.js +2 -1
  20. package/dist/tools/ask-question/ask-question.js.map +1 -1
  21. package/dist/types/ai-chat.d.ts +4 -5
  22. package/dist/types/index.d.ts +1 -2
  23. package/package.json +6 -6
  24. package/src/components/ai-chat/__test__/ai-chat.test.ts +74 -5
  25. package/src/components/ai-chat/message-item.vue +18 -16
  26. package/src/components/ai-chat/message-list.vue +8 -4
  27. package/src/components/ai-chat/style.scss +22 -8
  28. package/src/components/ai-chat/style.ts +2 -0
  29. package/src/components/ai-chat/tool-call.vue +27 -9
  30. package/src/components/index.ts +0 -1
  31. package/src/style.ts +0 -1
  32. package/src/tools/ask-question/__test__/ask-question.test.ts +20 -2
  33. package/src/tools/ask-question/ask-question.ts +2 -1
  34. package/src/types/index.ts +0 -1
  35. package/dist/components/ai-orb/style.d.ts +0 -1
  36. package/dist/components/ai-orb/style.js +0 -1
  37. package/dist/types/ai-orb.d.ts +0 -27
  38. package/src/components/ai-orb/index.ts +0 -1
  39. package/src/components/ai-orb/style.ts +0 -1
  40. /package/dist/components/ai-orb/{style2.css → style.css} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"tool-call.js","names":[],"sources":["../../../src/components/ai-chat/tool-call.vue"],"sourcesContent":["<template>\n <UCollapseItem\n v-model=\"isExpanded\"\n :class=\"[cls.e('tool-call'), bem.is(toolCall.status)]\"\n :destroy-on-collapse=\"destroyOnCollapse\"\n @change=\"markUserToggled\"\n >\n <template #header>\n <UIcon :class=\"[cls.e('tool-call-status'), statusClass]\">\n <component :is=\"headerIcon\" />\n </UIcon>\n <span :class=\"[cls.e('tool-call-name'), isActive && 'u-shine']\">\n {{ tool?.label ?? toolCall.name }}\n </span>\n <span v-if=\"summary\" :class=\"cls.e('tool-call-summary')\">{{ summary }}</span>\n\n <span\n v-if=\"toolCall.status === 'awaiting-confirm'\"\n :class=\"cls.e('tool-call-confirm')\"\n @click.stop\n @keydown.enter.stop\n @keydown.space.stop\n >\n <UButton size=\"small\" type=\"primary\" @click=\"emit('respond', true)\">允许</UButton>\n <UButton size=\"small\" text @click=\"emit('respond', false)\">拒绝</UButton>\n </span>\n </template>\n\n <!-- 面板工具:render 展示在右侧侧边面板,卡片 body 仅提供查看入口 -->\n <div v-if=\"isPanelTool\" :class=\"cls.e('tool-call-panel-entry')\">\n <span :class=\"cls.e('tool-call-panel-hint')\">内容展示在右侧面板</span>\n <UButton size=\"small\" text type=\"primary\" @click=\"di?.openPanel(toolCall.id)\">\n 查看面板\n </UButton>\n </div>\n <!-- 自定义渲染:meta.render / tool-<name> 插槽优先于通用工具视图 -->\n <component :is=\"tool.render\" v-else-if=\"tool?.render\" :tool-call=\"toolCall\" />\n <component :is=\"slotBody\" v-else-if=\"slotBody && hasResult\" />\n <template v-else>\n <ToolJson v-if=\"prettyArguments\" title=\"参数\" :text=\"prettyArguments\" />\n <ToolJson v-if=\"hasResult\" :title=\"resultTitle\" :text=\"displayResult\" />\n <ToolJson v-if=\"prettyView\" title=\"视图\" :text=\"prettyView\" />\n </template>\n </UCollapseItem>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UCollapseItem, UIcon } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, watch } from 'vue'\n\nimport type { ChatToolCall } from '../../chat/types'\nimport { AiChatDIKey } from './di'\nimport { resolveToolIcon } from './tool-icons'\nimport ToolJson from './tool-json.vue'\n\ndefineOptions({ name: 'UAiChatToolCall' })\n\nconst props = defineProps<{ toolCall: ChatToolCall }>()\n\nconst emit = defineEmits<{ (e: 'respond', approved: boolean): void }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 把未知值格式成可读 JSON,循环引用等异常时退回 String,不得 throw */\nfunction formatUnknown(value: unknown): string {\n if (value == null) return ''\n if (typeof value === 'string') {\n try {\n return JSON.stringify(JSON.parse(value), null, 2)\n } catch {\n return value\n }\n }\n try {\n return JSON.stringify(value, null, 2) ?? String(value)\n } catch {\n return String(value)\n }\n}\n\n/** 当前调用对应的工具定义或渲染元信息(session 下忽略 execute / needsConfirm / terminal) */\nconst tool = computed(() => di?.tools.value[props.toolCall.name])\n\nconst isExpanded = ref(false)\n/** 用户手动切换过后,不再随 status 自动展开/折叠 */\nlet userToggled: boolean = false\n\n/** UCollapseItem 的 change 仅在用户点击头部时触发(程序化 v-model 变更不触发) */\nconst markUserToggled = () => {\n userToggled = true\n}\n\n/** 面板工具:render 展示在右侧侧边面板,卡片 body 仅保留「查看面板」入口 */\nconst isPanelTool = computed(() => tool.value?.renderTo === 'panel')\n\n/** 完成后是否自动折叠:缺省面板工具折叠;有 render 时不折叠,否则折叠 */\nconst autoCollapse = computed(\n () => tool.value?.autoCollapse ?? (isPanelTool.value || !tool.value?.render)\n)\n\nwatch(\n () => props.toolCall.status,\n (status) => {\n if (userToggled) return\n if (status === 'pending' || status === 'running' || status === 'awaiting-confirm') {\n isExpanded.value = true\n } else {\n isExpanded.value = !autoCollapse.value\n }\n },\n { immediate: true }\n)\n\nconst STATUS_CLASS: Record<string, string> = {\n pending: 'is-active',\n running: 'is-active',\n 'awaiting-confirm': 'is-warning',\n success: 'is-success',\n error: 'is-danger',\n rejected: 'is-danger'\n}\n\nconst statusClass = computed(() => STATUS_CLASS[props.toolCall.status] ?? '')\n\n/** 进行中(pending/running):工具名带扫光效果 */\nconst isActive = computed(() => {\n return props.toolCall.status === 'pending' || props.toolCall.status === 'running'\n})\n\n/** 终态(成功/失败/拒绝) */\nconst isSettled = computed(() => {\n return ['success', 'error', 'rejected'].includes(props.toolCall.status)\n})\n\n/**\n * 折叠后卸载内容 DOM:仅终态且非面板工具时启用——\n * 进行中/待确认要保留内容状态(确认按钮、表单),面板工具的 body 是「查看面板」入口必须可达\n */\nconst destroyOnCollapse = computed(() => isSettled.value && !isPanelTool.value)\n\n/** 头部图标:meta.icon > 宿主覆盖 > 内置名称规则 > 兜底;状态点走 status class */\nconst headerIcon = computed(\n () => tool.value?.icon ?? resolveToolIcon(props.toolCall.name, di?.toolIcons?.value)\n)\n\nconst parsedArguments = computed<Record<string, unknown> | null>(() => {\n if (!props.toolCall.arguments) return null\n try {\n return JSON.parse(props.toolCall.arguments)\n } catch {\n return null\n }\n})\n\n/** 折叠态参数摘要:取第一个字符串参数值 */\nconst summary = computed(() => {\n const args = parsedArguments.value\n if (!args) return ''\n for (const value of Object.values(args)) {\n if (typeof value === 'string' && value) {\n return value.length > 40 ? `${value.slice(0, 40)}…` : value\n }\n }\n return ''\n})\n\nconst prettyArguments = computed(() => {\n const args = parsedArguments.value\n if (args) return JSON.stringify(args, null, 2)\n return props.toolCall.arguments || ''\n})\n\nconst hasResult = computed(() => {\n return props.toolCall.result != null || props.toolCall.error != null\n})\n\nconst resultTitle = computed(() => (props.toolCall.status === 'error' ? '错误' : '结果'))\n\nconst displayResult = computed(() => formatUnknown(props.toolCall.error ?? props.toolCall.result))\n\n/** 服务端下发的 view,格式化为 JSON;不得丢弃、不得 throw */\nconst prettyView = computed(() => {\n if (props.toolCall.view == null) return ''\n return formatUnknown(props.toolCall.view)\n})\n\n/** 使用方通过 tool-<name> 插槽自定义该工具的展示(有结果时替换整个 body) */\nconst slotBody = computed(() => {\n const slot = di?.slots[`tool-${props.toolCall.name}`]\n return slot ? () => slot({ toolCall: props.toolCall }) : undefined\n})\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"tool-call.js","names":[],"sources":["../../../src/components/ai-chat/tool-call.vue"],"sourcesContent":["<template>\n <UCollapseItem\n v-model=\"isExpanded\"\n :class=\"[cls.e('tool-call'), bem.is(toolCall.status)]\"\n :destroy-on-collapse=\"destroyOnCollapse\"\n @change=\"markUserToggled\"\n >\n <template #header>\n <UIcon :class=\"[cls.e('tool-call-status'), statusClass]\">\n <component :is=\"headerIcon\" />\n </UIcon>\n <span :class=\"[cls.e('tool-call-name'), isActive && 'u-shine']\">\n {{ tool?.label ?? toolCall.name }}\n </span>\n <span v-if=\"summary\" :class=\"cls.e('tool-call-summary')\">{{ summary }}</span>\n\n <span\n v-if=\"toolCall.status === 'awaiting-confirm'\"\n :class=\"cls.e('tool-call-confirm')\"\n @click.stop\n @keydown.enter.stop\n @keydown.space.stop\n >\n <UButton size=\"small\" type=\"primary\" @click=\"emit('respond', true)\">允许</UButton>\n <UButton size=\"small\" text @click=\"emit('respond', false)\">拒绝</UButton>\n </span>\n </template>\n\n <!-- 面板工具:render 展示在右侧侧边面板,卡片 body 仅提供查看入口 -->\n <div v-if=\"isPanelTool\" :class=\"cls.e('tool-call-panel-entry')\">\n <span :class=\"cls.e('tool-call-panel-hint')\">内容展示在右侧面板</span>\n <UButton size=\"small\" text type=\"primary\" @click=\"di?.openPanel(toolCall.id)\">\n 查看面板\n </UButton>\n </div>\n <!-- 自定义渲染:meta.render / tool-<name> 插槽优先于通用工具视图 -->\n <component :is=\"tool.render\" v-else-if=\"tool?.render\" :tool-call=\"toolCall\" />\n <component :is=\"slotBody\" v-else-if=\"slotBody && hasResult\" />\n <template v-else>\n <ToolJson v-if=\"prettyArguments\" title=\"参数\" :text=\"prettyArguments\" />\n <ToolJson v-if=\"hasResult\" :title=\"resultTitle\" :text=\"displayResult\" />\n <ToolJson v-if=\"prettyView\" title=\"视图\" :text=\"prettyView\" />\n </template>\n </UCollapseItem>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UCollapseItem, UIcon } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, watch } from 'vue'\n\nimport type { ChatToolCall } from '../../chat/types'\nimport { AiChatDIKey } from './di'\nimport { resolveToolIcon } from './tool-icons'\nimport ToolJson from './tool-json.vue'\n\ndefineOptions({ name: 'UAiChatToolCall' })\n\nconst props = defineProps<{ toolCall: ChatToolCall }>()\n\nconst emit = defineEmits<{ (e: 'respond', approved: boolean): void }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 把未知值格式成可读 JSON,循环引用等异常时退回 String,不得 throw */\nfunction formatUnknown(value: unknown): string {\n if (value == null) return ''\n if (typeof value === 'string') {\n try {\n return JSON.stringify(JSON.parse(value), null, 2)\n } catch {\n return value\n }\n }\n try {\n return JSON.stringify(value, null, 2) ?? String(value)\n } catch {\n return String(value)\n }\n}\n\n/** 当前调用对应的工具定义或渲染元信息(session 下忽略 execute / needsConfirm / terminal) */\nconst tool = computed(() => di?.tools.value[props.toolCall.name])\n\nconst isExpanded = ref(false)\n/** 用户手动切换过后,不再随 status 自动展开/折叠 */\nlet userToggled: boolean = false\n\n/** UCollapseItem 的 change 仅在用户点击头部时触发(程序化 v-model 变更不触发) */\nconst markUserToggled = () => {\n userToggled = true\n}\n\n/** 面板工具:render 展示在右侧侧边面板,卡片 body 仅保留「查看面板」入口 */\nconst isPanelTool = computed(() => tool.value?.renderTo === 'panel')\n\n/** 「UI 即答复」的终结工具(session 元信息无 terminal,恒为 false) */\nconst isAnswerTool = computed(() => {\n const t = tool.value\n return !!(t && 'terminal' in t && t.terminal)\n})\n\n/** 内联自定义 body(render / tool-<name> 插槽):进行中可能是提问表单等交互 UI,需展开可见 */\nconst hasInlineBody = computed(\n () => !isPanelTool.value && !!(tool.value?.render || di?.slots[`tool-${props.toolCall.name}`])\n)\n\n/** 完成后是否自动折叠:缺省一律折叠;「UI 即答复」的终结工具缺省保持展开 */\nconst autoCollapse = computed(() => tool.value?.autoCollapse ?? !isAnswerTool.value)\n\n/** 需要用户交互/展示的进行中状态:待确认(看参数再决策)、有内联自定义 body(提问表单等) */\nconst needsInteraction = computed(() => {\n if (props.toolCall.status === 'awaiting-confirm') return true\n const active = props.toolCall.status === 'pending' || props.toolCall.status === 'running'\n return active && hasInlineBody.value\n})\n\nconst isSettledStatus = (status: ChatToolCall['status']) =>\n status === 'success' || status === 'error' || status === 'rejected'\n\n// 默认折叠(含进行中);仅交互/答复类自动展开,用户手动切换过后不再自动变化\nwatch(\n () => props.toolCall.status,\n (status) => {\n if (userToggled) return\n if (isSettledStatus(status)) {\n isExpanded.value = !autoCollapse.value\n } else {\n isExpanded.value = needsInteraction.value\n }\n },\n { immediate: true }\n)\n\nconst STATUS_CLASS: Record<string, string> = {\n pending: 'is-active',\n running: 'is-active',\n 'awaiting-confirm': 'is-warning',\n success: 'is-success',\n error: 'is-danger',\n rejected: 'is-danger'\n}\n\nconst statusClass = computed(() => STATUS_CLASS[props.toolCall.status] ?? '')\n\n/** 进行中(pending/running):工具名带扫光效果 */\nconst isActive = computed(() => {\n return props.toolCall.status === 'pending' || props.toolCall.status === 'running'\n})\n\n/** 终态(成功/失败/拒绝) */\nconst isSettled = computed(() => isSettledStatus(props.toolCall.status))\n\n/**\n * 折叠后卸载内容 DOM:仅终态且非面板工具时启用——\n * 进行中/待确认要保留内容状态(确认按钮、表单),面板工具的 body 是「查看面板」入口必须可达\n */\nconst destroyOnCollapse = computed(() => isSettled.value && !isPanelTool.value)\n\n/** 头部图标:meta.icon > 宿主覆盖 > 内置名称规则 > 兜底;状态点走 status class */\nconst headerIcon = computed(\n () => tool.value?.icon ?? resolveToolIcon(props.toolCall.name, di?.toolIcons?.value)\n)\n\nconst parsedArguments = computed<Record<string, unknown> | null>(() => {\n if (!props.toolCall.arguments) return null\n try {\n return JSON.parse(props.toolCall.arguments)\n } catch {\n return null\n }\n})\n\n/** 折叠态参数摘要:取第一个字符串参数值 */\nconst summary = computed(() => {\n const args = parsedArguments.value\n if (!args) return ''\n for (const value of Object.values(args)) {\n if (typeof value === 'string' && value) {\n return value.length > 40 ? `${value.slice(0, 40)}…` : value\n }\n }\n return ''\n})\n\nconst prettyArguments = computed(() => {\n const args = parsedArguments.value\n if (args) return JSON.stringify(args, null, 2)\n return props.toolCall.arguments || ''\n})\n\nconst hasResult = computed(() => {\n return props.toolCall.result != null || props.toolCall.error != null\n})\n\nconst resultTitle = computed(() => (props.toolCall.status === 'error' ? '错误' : '结果'))\n\nconst displayResult = computed(() => formatUnknown(props.toolCall.error ?? props.toolCall.result))\n\n/** 服务端下发的 view,格式化为 JSON;不得丢弃、不得 throw */\nconst prettyView = computed(() => {\n if (props.toolCall.view == null) return ''\n return formatUnknown(props.toolCall.view)\n})\n\n/** 使用方通过 tool-<name> 插槽自定义该工具的展示(有结果时替换整个 body) */\nconst slotBody = computed(() => {\n const slot = di?.slots[`tool-${props.toolCall.name}`]\n return slot ? () => slot({ toolCall: props.toolCall }) : undefined\n})\n</script>\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import _sfc_main$1 from "./components/ai-orb/ai-orb.js";
2
1
  import _sfc_main from "./components/ai-chat/ai-chat.js";
3
2
  import { ChatAttachment, ChatJob, ChatJobStatus, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolMeta, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus } from "./chat/types.js";
4
3
  import { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult } from "./tools/ask-question/ask-question.js";
@@ -6,10 +5,9 @@ import "./tools/index.js";
6
5
  import { ChatSessionAdapter, ChatSessionEvent, ChatSessionTransport, createServerTransport, isServerTransport } from "./chat/session.js";
7
6
  import { ChatModel, ChatModelOption, ChatProvider, ChatReasoningLevel } from "./providers/types.js";
8
7
  import { AiChatEmits, AiChatExposed, AiChatProps, _AiChatExposed } from "./types/ai-chat.js";
9
- import { AiOrbEmits, AiOrbExposed, AiOrbProps, AiOrbReaction, AiOrbStatus } from "./types/ai-orb.js";
10
8
  import "./types/index.js";
11
9
  import { ChatFoldState, ChatPendingApproval, createFoldState, foldSessionEvent } from "./chat/fold.js";
12
10
  import { UseChatOptions, useChat } from "./chat/use-chat.js";
13
11
  import { OpenAITransport, OpenAITransportOptions, createOpenAITransport } from "./chat/transports/openai.js";
14
12
  import "./chat/index.js";
15
- export { type AiChatEmits, type AiChatExposed, type AiChatProps, type AiOrbEmits, type AiOrbExposed, type AiOrbProps, type AiOrbReaction, type AiOrbStatus, type AskQuestionAnswer, type AskQuestionArgs, type AskQuestionItem, type AskQuestionResult, ChatAttachment, ChatFoldState, ChatJob, ChatJobStatus, ChatMessage, type ChatModel, type ChatModelOption, ChatPendingApproval, type ChatProvider, ChatQueuedMessage, type ChatReasoningLevel, ChatSessionAdapter, ChatSessionEvent, ChatSessionTransport, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolMeta, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, OpenAITransport, OpenAITransportOptions, ToolCallStatus, _sfc_main as UAiChat, _sfc_main$1 as UAiOrb, UseChatOptions, type _AiChatExposed, createFoldState, createOpenAITransport, createServerTransport, foldSessionEvent, isServerTransport, useChat };
13
+ export { type AiChatEmits, type AiChatExposed, type AiChatProps, type AskQuestionAnswer, type AskQuestionArgs, type AskQuestionItem, type AskQuestionResult, ChatAttachment, ChatFoldState, ChatJob, ChatJobStatus, ChatMessage, type ChatModel, type ChatModelOption, ChatPendingApproval, type ChatProvider, ChatQueuedMessage, type ChatReasoningLevel, ChatSessionAdapter, ChatSessionEvent, ChatSessionTransport, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolMeta, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, OpenAITransport, OpenAITransportOptions, ToolCallStatus, _sfc_main as UAiChat, UseChatOptions, type _AiChatExposed, createFoldState, createOpenAITransport, createServerTransport, foldSessionEvent, isServerTransport, useChat };
package/dist/index.js CHANGED
@@ -2,6 +2,5 @@ import { createServerTransport, isServerTransport } from "./chat/session.js";
2
2
  import { createFoldState, foldSessionEvent } from "./chat/fold.js";
3
3
  import { useChat } from "./chat/use-chat.js";
4
4
  import { createOpenAITransport } from "./chat/transports/openai.js";
5
- import _sfc_main$1 from "./components/ai-orb/ai-orb.js";
6
5
  import _sfc_main from "./components/ai-chat/ai-chat.js";
7
- export { _sfc_main as UAiChat, _sfc_main$1 as UAiOrb, createFoldState, createOpenAITransport, createServerTransport, foldSessionEvent, isServerTransport, useChat };
6
+ export { _sfc_main as UAiChat, createFoldState, createOpenAITransport, createServerTransport, foldSessionEvent, isServerTransport, useChat };
@@ -1,13 +1,13 @@
1
1
  //#region src/providers/types.d.ts
2
2
  /** 推理等级选项(值不透明,由宿主/服务商约定) */
3
- interface ChatReasoningLevel {
3
+ export interface ChatReasoningLevel {
4
4
  /** 写入请求的值 */
5
5
  value: string;
6
6
  /** UI 文案 */
7
7
  label: string;
8
8
  }
9
9
  /** 单个模型配置 */
10
- interface ChatModel {
10
+ export interface ChatModel {
11
11
  /** 模型 id(跨 Provider 全局唯一) */
12
12
  id: string;
13
13
  /** UI 显示名,缺省取 id */
@@ -20,7 +20,7 @@ interface ChatModel {
20
20
  defaultReasoningLevel?: string;
21
21
  }
22
22
  /** 模型服务商配置(内嵌 models) */
23
- interface ChatProvider {
23
+ export interface ChatProvider {
24
24
  id: string;
25
25
  /** UI 显示名 */
26
26
  label?: string;
@@ -38,10 +38,9 @@ interface ChatProvider {
38
38
  models: ChatModel[];
39
39
  }
40
40
  /** UI / useChat 使用的扁平模型项(带 providerId) */
41
- interface ChatModelOption extends ChatModel {
41
+ export interface ChatModelOption extends ChatModel {
42
42
  providerId: string;
43
43
  providerLabel?: string;
44
44
  }
45
45
  //#endregion
46
- export { ChatModel, ChatModelOption, ChatProvider, ChatReasoningLevel };
47
46
  //# sourceMappingURL=types.d.ts.map
package/dist/style.js CHANGED
@@ -1,2 +1 @@
1
1
  import "./components/ai-chat/style.js";
2
- import "./components/ai-orb/style.js";
@@ -2,7 +2,7 @@ import "../../chat/types.js";
2
2
  import { Component } from "vue";
3
3
  //#region src/tools/ask-question/ask-question.d.ts
4
4
  /** 单个提问项 */
5
- interface AskQuestionItem {
5
+ export interface AskQuestionItem {
6
6
  /** 问题文案 */
7
7
  question: string;
8
8
  /** 预设选项(单选);缺省为纯文本题 */
@@ -11,20 +11,19 @@ interface AskQuestionItem {
11
11
  placeholder?: string;
12
12
  }
13
13
  /** 一条问答结果 */
14
- interface AskQuestionAnswer {
14
+ export interface AskQuestionAnswer {
15
15
  /** 问题文案 */
16
16
  question: string;
17
17
  /** 用户回答(选中的选项或自定义输入) */
18
18
  answer: string;
19
19
  }
20
20
  /** 提问工具参数(模型输出) */
21
- interface AskQuestionArgs {
21
+ export interface AskQuestionArgs {
22
22
  questions: AskQuestionItem[];
23
23
  }
24
24
  /** 提问工具结果(序列化后回灌模型,渲染层据此展示问答摘要) */
25
- interface AskQuestionResult {
25
+ export interface AskQuestionResult {
26
26
  answers: AskQuestionAnswer[];
27
27
  }
28
28
  //#endregion
29
- export { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult };
30
29
  //# sourceMappingURL=ask-question.d.ts.map
@@ -81,7 +81,8 @@ const AskQuestionRender = defineComponent({
81
81
  });
82
82
  /**
83
83
  * 创建内置提问工具:execute 挂起等待用户在 UI 中作答,提交后结果回灌模型。
84
- * 工具卡片由内联提问表单渲染(多题分页导航,提交后展示问答摘要,不自动折叠)。
84
+ * 工具卡片由内联提问表单渲染(多题分页导航,提交后展示问答摘要);
85
+ * 进行中因有内联 render 自动展开,终态默认折叠,展开后可查看问答摘要。
85
86
  */
86
87
  function createAskQuestionTool(options = {}) {
87
88
  const { name = "askQuestion", description = DEFAULT_DESCRIPTION, label = "提问", icon = QuestionFilled } = options;
@@ -1 +1 @@
1
- {"version":3,"file":"ask-question.js","names":["AskQuestion"],"sources":["../../../src/tools/ask-question/ask-question.ts"],"sourcesContent":["import { QuestionFilled } from '@veltra/icons/normal'\nimport { computed, defineComponent, h, type Component, type PropType } from 'vue'\n\nimport type { ChatTool, ChatToolCall } from '../../chat/types'\nimport AskQuestion from '../../components/ai-chat/ask-question.vue'\nimport { deferAskQuestion, resolveAskQuestion, takeAskQuestionDeferred } from './deferred'\n\n/** 单个提问项 */\nexport interface AskQuestionItem {\n /** 问题文案 */\n question: string\n /** 预设选项(单选);缺省为纯文本题 */\n options?: string[]\n /** 自定义输入占位文案 */\n placeholder?: string\n}\n\n/** 一条问答结果 */\nexport interface AskQuestionAnswer {\n /** 问题文案 */\n question: string\n /** 用户回答(选中的选项或自定义输入) */\n answer: string\n}\n\n/** 提问工具参数(模型输出) */\nexport interface AskQuestionArgs {\n questions: AskQuestionItem[]\n}\n\n/** 提问工具结果(序列化后回灌模型,渲染层据此展示问答摘要) */\nexport interface AskQuestionResult {\n answers: AskQuestionAnswer[]\n}\n\nexport interface CreateAskQuestionToolOptions {\n /** 工具名(传给模型),默认 askQuestion */\n name?: string\n /** 工具描述(传给模型) */\n description?: string\n /** 工具显示名,默认「提问」 */\n label?: string\n /** 工具图标,默认 QuestionFilled */\n icon?: Component\n}\n\nconst DEFAULT_DESCRIPTION =\n '当需求不明确或存在歧义时,向用户提问以澄清。一次可提 1-4 个关键问题;' +\n '每个问题可提供若干预设选项,用户可选择选项或自定义输入。用户作答后再继续生成。'\n\n/** 提问参数 JSON Schema(原样传给模型) */\nconst QUESTIONS_SCHEMA: Record<string, unknown> = {\n type: 'object',\n properties: {\n questions: {\n type: 'array',\n description: '问题列表',\n minItems: 1,\n maxItems: 4,\n items: {\n type: 'object',\n properties: {\n question: { type: 'string', description: '问题文案' },\n options: {\n type: 'array',\n description: '预设选项(单选);缺省为纯文本题',\n items: { type: 'string' }\n },\n placeholder: { type: 'string', description: '自定义输入占位文案' }\n },\n required: ['question']\n }\n }\n },\n required: ['questions']\n}\n\nfunction parseQuestions(toolCall: ChatToolCall): AskQuestionItem[] {\n try {\n const args = JSON.parse(toolCall.arguments || '{}') as Partial<AskQuestionArgs>\n return Array.isArray(args.questions) ? args.questions : []\n } catch {\n return []\n }\n}\n\nfunction parseAnswers(toolCall: ChatToolCall): AskQuestionAnswer[] | undefined {\n if (toolCall.status !== 'success' || !toolCall.result) return undefined\n try {\n const result = JSON.parse(toolCall.result) as Partial<AskQuestionResult>\n return Array.isArray(result.answers) ? result.answers : undefined\n } catch {\n return undefined\n }\n}\n\nfunction statusError(toolCall: ChatToolCall): string | undefined {\n if (toolCall.status !== 'error') return undefined\n const err = toolCall.error ?? ''\n if (err.includes('Abort') || err.includes('取消')) return '已取消'\n return err || '提问失败'\n}\n\n/** 把 toolCall 转成提问表单的 questions / onSubmit;提交仍走 resolveAskQuestion */\nconst AskQuestionRender = defineComponent({\n name: 'AskQuestionRender',\n props: { toolCall: { type: Object as PropType<ChatToolCall>, required: true } },\n setup(props) {\n const questions = computed(() => parseQuestions(props.toolCall))\n const answers = computed(() => parseAnswers(props.toolCall))\n const error = computed(() => statusError(props.toolCall))\n const onSubmit = (value: AskQuestionAnswer[] | AskQuestionResult) => {\n resolveAskQuestion(props.toolCall.id, {\n answers: Array.isArray(value) ? value : value.answers\n })\n }\n return () =>\n h(AskQuestion, {\n questions: questions.value,\n answers: answers.value,\n error: error.value,\n onSubmit\n })\n }\n})\n\n/**\n * 创建内置提问工具:execute 挂起等待用户在 UI 中作答,提交后结果回灌模型。\n * 工具卡片由内联提问表单渲染(多题分页导航,提交后展示问答摘要,不自动折叠)。\n */\nexport function createAskQuestionTool(\n options: CreateAskQuestionToolOptions = {}\n): ChatTool<AskQuestionArgs> {\n const {\n name = 'askQuestion',\n description = DEFAULT_DESCRIPTION,\n label = '提问',\n icon = QuestionFilled\n } = options\n\n return {\n name,\n description,\n parameters: QUESTIONS_SCHEMA,\n icon,\n label,\n render: AskQuestionRender,\n execute: (_args, { toolCall, signal }) =>\n new Promise<AskQuestionResult>((resolve, reject) => {\n deferAskQuestion(toolCall.id, { resolve, reject })\n signal.addEventListener(\n 'abort',\n () => {\n // 不依赖 signal.reason(各环境 DOMException 行为不一),统一拒绝为 AbortError\n const error = new Error('Aborted')\n error.name = 'AbortError'\n takeAskQuestionDeferred(toolCall.id)?.reject(error)\n },\n { once: true }\n )\n })\n }\n}\n"],"mappings":";;;;;AA8CA,MAAM,sBACJ;;AAIF,MAAM,mBAA4C;CAChD,MAAM;CACN,YAAY,EACV,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,UAAU;EACV,OAAO;GACL,MAAM;GACN,YAAY;IACV,UAAU;KAAE,MAAM;KAAU,aAAa;IAAO;IAChD,SAAS;KACP,MAAM;KACN,aAAa;KACb,OAAO,EAAE,MAAM,SAAS;IAC1B;IACA,aAAa;KAAE,MAAM;KAAU,aAAa;IAAY;GAC1D;GACA,UAAU,CAAC,UAAU;EACvB;CACF,EACF;CACA,UAAU,CAAC,WAAW;AACxB;AAEA,SAAS,eAAe,UAA2C;CACjE,IAAI;EACF,MAAM,OAAO,KAAK,MAAM,SAAS,aAAa,IAAI;EAClD,OAAO,MAAM,QAAQ,KAAK,SAAS,IAAI,KAAK,YAAY,CAAC;CAC3D,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAAS,aAAa,UAAyD;CAC7E,IAAI,SAAS,WAAW,aAAa,CAAC,SAAS,QAAQ,OAAO,KAAA;CAC9D,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,SAAS,MAAM;EACzC,OAAO,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO,UAAU,KAAA;CAC1D,QAAQ;EACN;CACF;AACF;AAEA,SAAS,YAAY,UAA4C;CAC/D,IAAI,SAAS,WAAW,SAAS,OAAO,KAAA;CACxC,MAAM,MAAM,SAAS,SAAS;CAC9B,IAAI,IAAI,SAAS,OAAO,KAAK,IAAI,SAAS,IAAI,GAAG,OAAO;CACxD,OAAO,OAAO;AAChB;;AAGA,MAAM,oBAAoB,gBAAgB;CACxC,MAAM;CACN,OAAO,EAAE,UAAU;EAAE,MAAM;EAAkC,UAAU;CAAK,EAAE;CAC9E,MAAM,OAAO;EACX,MAAM,YAAY,eAAe,eAAe,MAAM,QAAQ,CAAC;EAC/D,MAAM,UAAU,eAAe,aAAa,MAAM,QAAQ,CAAC;EAC3D,MAAM,QAAQ,eAAe,YAAY,MAAM,QAAQ,CAAC;EACxD,MAAM,YAAY,UAAmD;GACnE,mBAAmB,MAAM,SAAS,IAAI,EACpC,SAAS,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAChD,CAAC;EACH;EACA,aACE,EAAEA,WAAa;GACb,WAAW,UAAU;GACrB,SAAS,QAAQ;GACjB,OAAO,MAAM;GACb;EACF,CAAC;CACL;AACF,CAAC;;;;;AAMD,SAAgB,sBACd,UAAwC,CAAC,GACd;CAC3B,MAAM,EACJ,OAAO,eACP,cAAc,qBACd,QAAQ,MACR,OAAO,mBACL;CAEJ,OAAO;EACL;EACA;EACA,YAAY;EACZ;EACA;EACA,QAAQ;EACR,UAAU,OAAO,EAAE,UAAU,aAC3B,IAAI,SAA4B,SAAS,WAAW;GAClD,iBAAiB,SAAS,IAAI;IAAE;IAAS;GAAO,CAAC;GACjD,OAAO,iBACL,eACM;IAEJ,MAAM,wBAAQ,IAAI,MAAM,SAAS;IACjC,MAAM,OAAO;IACb,wBAAwB,SAAS,EAAE,CAAC,EAAE,OAAO,KAAK;GACpD,GACA,EAAE,MAAM,KAAK,CACf;EACF,CAAC;CACL;AACF"}
1
+ {"version":3,"file":"ask-question.js","names":["AskQuestion"],"sources":["../../../src/tools/ask-question/ask-question.ts"],"sourcesContent":["import { QuestionFilled } from '@veltra/icons/normal'\nimport { computed, defineComponent, h, type Component, type PropType } from 'vue'\n\nimport type { ChatTool, ChatToolCall } from '../../chat/types'\nimport AskQuestion from '../../components/ai-chat/ask-question.vue'\nimport { deferAskQuestion, resolveAskQuestion, takeAskQuestionDeferred } from './deferred'\n\n/** 单个提问项 */\nexport interface AskQuestionItem {\n /** 问题文案 */\n question: string\n /** 预设选项(单选);缺省为纯文本题 */\n options?: string[]\n /** 自定义输入占位文案 */\n placeholder?: string\n}\n\n/** 一条问答结果 */\nexport interface AskQuestionAnswer {\n /** 问题文案 */\n question: string\n /** 用户回答(选中的选项或自定义输入) */\n answer: string\n}\n\n/** 提问工具参数(模型输出) */\nexport interface AskQuestionArgs {\n questions: AskQuestionItem[]\n}\n\n/** 提问工具结果(序列化后回灌模型,渲染层据此展示问答摘要) */\nexport interface AskQuestionResult {\n answers: AskQuestionAnswer[]\n}\n\nexport interface CreateAskQuestionToolOptions {\n /** 工具名(传给模型),默认 askQuestion */\n name?: string\n /** 工具描述(传给模型) */\n description?: string\n /** 工具显示名,默认「提问」 */\n label?: string\n /** 工具图标,默认 QuestionFilled */\n icon?: Component\n}\n\nconst DEFAULT_DESCRIPTION =\n '当需求不明确或存在歧义时,向用户提问以澄清。一次可提 1-4 个关键问题;' +\n '每个问题可提供若干预设选项,用户可选择选项或自定义输入。用户作答后再继续生成。'\n\n/** 提问参数 JSON Schema(原样传给模型) */\nconst QUESTIONS_SCHEMA: Record<string, unknown> = {\n type: 'object',\n properties: {\n questions: {\n type: 'array',\n description: '问题列表',\n minItems: 1,\n maxItems: 4,\n items: {\n type: 'object',\n properties: {\n question: { type: 'string', description: '问题文案' },\n options: {\n type: 'array',\n description: '预设选项(单选);缺省为纯文本题',\n items: { type: 'string' }\n },\n placeholder: { type: 'string', description: '自定义输入占位文案' }\n },\n required: ['question']\n }\n }\n },\n required: ['questions']\n}\n\nfunction parseQuestions(toolCall: ChatToolCall): AskQuestionItem[] {\n try {\n const args = JSON.parse(toolCall.arguments || '{}') as Partial<AskQuestionArgs>\n return Array.isArray(args.questions) ? args.questions : []\n } catch {\n return []\n }\n}\n\nfunction parseAnswers(toolCall: ChatToolCall): AskQuestionAnswer[] | undefined {\n if (toolCall.status !== 'success' || !toolCall.result) return undefined\n try {\n const result = JSON.parse(toolCall.result) as Partial<AskQuestionResult>\n return Array.isArray(result.answers) ? result.answers : undefined\n } catch {\n return undefined\n }\n}\n\nfunction statusError(toolCall: ChatToolCall): string | undefined {\n if (toolCall.status !== 'error') return undefined\n const err = toolCall.error ?? ''\n if (err.includes('Abort') || err.includes('取消')) return '已取消'\n return err || '提问失败'\n}\n\n/** 把 toolCall 转成提问表单的 questions / onSubmit;提交仍走 resolveAskQuestion */\nconst AskQuestionRender = defineComponent({\n name: 'AskQuestionRender',\n props: { toolCall: { type: Object as PropType<ChatToolCall>, required: true } },\n setup(props) {\n const questions = computed(() => parseQuestions(props.toolCall))\n const answers = computed(() => parseAnswers(props.toolCall))\n const error = computed(() => statusError(props.toolCall))\n const onSubmit = (value: AskQuestionAnswer[] | AskQuestionResult) => {\n resolveAskQuestion(props.toolCall.id, {\n answers: Array.isArray(value) ? value : value.answers\n })\n }\n return () =>\n h(AskQuestion, {\n questions: questions.value,\n answers: answers.value,\n error: error.value,\n onSubmit\n })\n }\n})\n\n/**\n * 创建内置提问工具:execute 挂起等待用户在 UI 中作答,提交后结果回灌模型。\n * 工具卡片由内联提问表单渲染(多题分页导航,提交后展示问答摘要);\n * 进行中因有内联 render 自动展开,终态默认折叠,展开后可查看问答摘要。\n */\nexport function createAskQuestionTool(\n options: CreateAskQuestionToolOptions = {}\n): ChatTool<AskQuestionArgs> {\n const {\n name = 'askQuestion',\n description = DEFAULT_DESCRIPTION,\n label = '提问',\n icon = QuestionFilled\n } = options\n\n return {\n name,\n description,\n parameters: QUESTIONS_SCHEMA,\n icon,\n label,\n render: AskQuestionRender,\n execute: (_args, { toolCall, signal }) =>\n new Promise<AskQuestionResult>((resolve, reject) => {\n deferAskQuestion(toolCall.id, { resolve, reject })\n signal.addEventListener(\n 'abort',\n () => {\n // 不依赖 signal.reason(各环境 DOMException 行为不一),统一拒绝为 AbortError\n const error = new Error('Aborted')\n error.name = 'AbortError'\n takeAskQuestionDeferred(toolCall.id)?.reject(error)\n },\n { once: true }\n )\n })\n }\n}\n"],"mappings":";;;;;AA8CA,MAAM,sBACJ;;AAIF,MAAM,mBAA4C;CAChD,MAAM;CACN,YAAY,EACV,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,UAAU;EACV,OAAO;GACL,MAAM;GACN,YAAY;IACV,UAAU;KAAE,MAAM;KAAU,aAAa;IAAO;IAChD,SAAS;KACP,MAAM;KACN,aAAa;KACb,OAAO,EAAE,MAAM,SAAS;IAC1B;IACA,aAAa;KAAE,MAAM;KAAU,aAAa;IAAY;GAC1D;GACA,UAAU,CAAC,UAAU;EACvB;CACF,EACF;CACA,UAAU,CAAC,WAAW;AACxB;AAEA,SAAS,eAAe,UAA2C;CACjE,IAAI;EACF,MAAM,OAAO,KAAK,MAAM,SAAS,aAAa,IAAI;EAClD,OAAO,MAAM,QAAQ,KAAK,SAAS,IAAI,KAAK,YAAY,CAAC;CAC3D,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAAS,aAAa,UAAyD;CAC7E,IAAI,SAAS,WAAW,aAAa,CAAC,SAAS,QAAQ,OAAO,KAAA;CAC9D,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,SAAS,MAAM;EACzC,OAAO,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO,UAAU,KAAA;CAC1D,QAAQ;EACN;CACF;AACF;AAEA,SAAS,YAAY,UAA4C;CAC/D,IAAI,SAAS,WAAW,SAAS,OAAO,KAAA;CACxC,MAAM,MAAM,SAAS,SAAS;CAC9B,IAAI,IAAI,SAAS,OAAO,KAAK,IAAI,SAAS,IAAI,GAAG,OAAO;CACxD,OAAO,OAAO;AAChB;;AAGA,MAAM,oBAAoB,gBAAgB;CACxC,MAAM;CACN,OAAO,EAAE,UAAU;EAAE,MAAM;EAAkC,UAAU;CAAK,EAAE;CAC9E,MAAM,OAAO;EACX,MAAM,YAAY,eAAe,eAAe,MAAM,QAAQ,CAAC;EAC/D,MAAM,UAAU,eAAe,aAAa,MAAM,QAAQ,CAAC;EAC3D,MAAM,QAAQ,eAAe,YAAY,MAAM,QAAQ,CAAC;EACxD,MAAM,YAAY,UAAmD;GACnE,mBAAmB,MAAM,SAAS,IAAI,EACpC,SAAS,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAChD,CAAC;EACH;EACA,aACE,EAAEA,WAAa;GACb,WAAW,UAAU;GACrB,SAAS,QAAQ;GACjB,OAAO,MAAM;GACb;EACF,CAAC;CACL;AACF,CAAC;;;;;;AAOD,SAAgB,sBACd,UAAwC,CAAC,GACd;CAC3B,MAAM,EACJ,OAAO,eACP,cAAc,qBACd,QAAQ,MACR,OAAO,mBACL;CAEJ,OAAO;EACL;EACA;EACA,YAAY;EACZ;EACA;EACA,QAAQ;EACR,UAAU,OAAO,EAAE,UAAU,aAC3B,IAAI,SAA4B,SAAS,WAAW;GAClD,iBAAiB,SAAS,IAAI;IAAE;IAAS;GAAO,CAAC;GACjD,OAAO,iBACL,eACM;IAEJ,MAAM,wBAAQ,IAAI,MAAM,SAAS;IACjC,MAAM,OAAO;IACb,wBAAwB,SAAS,EAAE,CAAC,EAAE,OAAO,KAAK;GACpD,GACA,EAAE,MAAM,KAAK,CACf;EACF,CAAC;CACL;AACF"}
@@ -4,7 +4,7 @@ import { ChatModelOption } from "../providers/types.js";
4
4
  import { Component, Ref } from "vue";
5
5
  import { DeconstructValue } from "@veltra/utils";
6
6
  //#region src/types/ai-chat.d.ts
7
- interface AiChatProps {
7
+ export interface AiChatProps {
8
8
  /** 传输层(必填);函数型 ChatTransport 或 session 对象,可使用 createOpenAITransport / createServerTransport */
9
9
  transport: ChatTransport | ChatSessionTransport;
10
10
  /**
@@ -53,7 +53,7 @@ interface AiChatProps {
53
53
  /** 只读:不展示输入区、欢迎语不可发送、队列无插队/编辑/移除。默认 false */
54
54
  readonly?: boolean;
55
55
  }
56
- interface AiChatEmits {
56
+ export interface AiChatEmits {
57
57
  /** 消息列表变化 */
58
58
  (e: 'update:messages', messages: ChatMessage[]): void;
59
59
  /** 当前模型变化 */
@@ -69,7 +69,7 @@ interface AiChatEmits {
69
69
  /** 模型发起工具调用 */
70
70
  (e: 'tool-call', toolCall: ChatToolCall): void;
71
71
  }
72
- interface _AiChatExposed {
72
+ export interface _AiChatExposed {
73
73
  /** 发送一条用户消息(会话进行中时进入待发送队列) */
74
74
  send: (content: string, attachments?: ChatAttachment[]) => void;
75
75
  /** 中断当前生成(保留待发送队列) */
@@ -91,7 +91,6 @@ interface _AiChatExposed {
91
91
  /** 向队列插入一条消息(beforeId 插到某条之前,缺省追加尾部;空闲时自动消耗队首) */
92
92
  enqueue: (content: string, attachments?: ChatAttachment[], beforeId?: string) => ChatQueuedMessage;
93
93
  }
94
- type AiChatExposed = DeconstructValue<_AiChatExposed>;
94
+ export type AiChatExposed = DeconstructValue<_AiChatExposed>;
95
95
  //#endregion
96
- export { AiChatEmits, AiChatExposed, AiChatProps, _AiChatExposed };
97
96
  //# sourceMappingURL=ai-chat.d.ts.map
@@ -1,3 +1,2 @@
1
1
  import { AiChatEmits, AiChatExposed, AiChatProps, _AiChatExposed } from "./ai-chat.js";
2
- import { AiOrbEmits, AiOrbExposed, AiOrbProps, AiOrbReaction, AiOrbStatus } from "./ai-orb.js";
3
- export { AiChatEmits, AiChatExposed, AiChatProps, AiOrbEmits, AiOrbExposed, AiOrbProps, AiOrbReaction, AiOrbStatus, _AiChatExposed };
2
+ export { AiChatEmits, AiChatExposed, AiChatProps, _AiChatExposed };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/ai",
3
- "version": "2.1.8",
3
+ "version": "2.1.10",
4
4
  "homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/cabinet-fe/ultra-ui/issues"
@@ -36,14 +36,14 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "markstream-vue": "^2.0.8"
39
+ "markstream-vue": "^2.0.11"
40
40
  },
41
41
  "peerDependencies": {
42
- "@veltra/compositions": "^1.7.9",
43
- "@veltra/desktop": "^1.7.9",
42
+ "@veltra/compositions": "^1.7.14",
43
+ "@veltra/desktop": "^1.7.14",
44
44
  "@veltra/icons": "^1.5.0",
45
- "@veltra/styles": "^1.7.9",
46
- "@veltra/utils": "^1.7.9",
45
+ "@veltra/styles": "^1.7.14",
46
+ "@veltra/utils": "^1.7.14",
47
47
  "vue": ">=3.5.42"
48
48
  }
49
49
  }
@@ -207,6 +207,8 @@ describe('UAiChat', () => {
207
207
  await vi.waitFor(() => {
208
208
  expect(host.querySelector('.u-ai-chat__tool-call-confirm')).toBeTruthy()
209
209
  })
210
+ // 待确认自动展开:确认前可查看参数
211
+ expect(host.querySelector('.u-ai-chat__tool-call.is-active')).toBeTruthy()
210
212
  expect(execute).not.toHaveBeenCalled()
211
213
 
212
214
  const allowBtn = [
@@ -286,21 +288,40 @@ describe('UAiChat', () => {
286
288
  ...extra
287
289
  })
288
290
 
289
- it('render 自定义渲染替换卡片 body,完成后保持展开', async () => {
291
+ it('render 自定义渲染替换卡片 body,完成后默认折叠、展开后可见', async () => {
290
292
  const tools = [weatherTool({ render: createResultView('weather-view') })]
291
293
  const { host, chat, unmount } = mountAiChat({ transport: createWeatherTransport(), tools })
292
294
 
293
295
  chat.value?.send('北京天气')
294
296
 
295
- // 最终答案输出后工具卡片收进「已完成」过程块,展开后 render 保持展开态
297
+ // 最终答案输出后工具卡片收进「已完成」过程块,展开后断言
296
298
  await expandProcess(host)
297
299
 
298
- // 完成后保持展开,默认参数/结果区被替换
300
+ // render 工具完成后默认折叠(不再是保持展开)
301
+ expect(host.querySelector('.u-ai-chat__tool-call.is-success')).toBeTruthy()
302
+ expect(host.querySelector('.u-ai-chat__tool-call.is-active')).toBeFalsy()
303
+
304
+ // 手动展开:render 替换默认参数/结果区
305
+ const header = host.querySelector<HTMLElement>('.u-ai-chat__tool-call .u-collapse__header')!
306
+ header.dispatchEvent(new MouseEvent('click', { bubbles: true }))
307
+ await nextTick()
308
+ await nextTick()
309
+ expect(host.querySelector('.weather-view')?.textContent).toContain('{"temperature":26}')
310
+ expect(host.querySelector('.u-ai-chat__tool-call-code')).toBeFalsy()
311
+ unmount()
312
+ })
313
+
314
+ it('terminal + render 的「UI 即答复」工具完成后保持展开', async () => {
315
+ const tools = [weatherTool({ render: createResultView('weather-view'), terminal: true })]
316
+ const { host, chat, unmount } = mountAiChat({ transport: createWeatherTransport(), tools })
317
+
318
+ chat.value?.send('北京天气')
319
+
320
+ // 终结工具成功即结束对话(无第二轮文本),答复卡片保持展开
299
321
  await vi.waitFor(() => {
300
322
  expect(host.querySelector('.u-ai-chat__tool-call.is-success.is-active')).toBeTruthy()
301
323
  })
302
324
  expect(host.querySelector('.weather-view')?.textContent).toContain('{"temperature":26}')
303
- expect(host.querySelector('.u-ai-chat__tool-call-code')).toBeFalsy()
304
325
  unmount()
305
326
  })
306
327
 
@@ -389,6 +410,43 @@ describe('UAiChat', () => {
389
410
  unmount()
390
411
  })
391
412
 
413
+ it('思考中默认折叠,头部右侧滚动展示最新一行思考', async () => {
414
+ // 由测试手动推进流式节奏,避免断言竞态
415
+ const steps: (() => void)[] = []
416
+ const transport: ChatTransport = async (_req, handlers) => {
417
+ handlers.onReasoningDelta?.('第一行思考')
418
+ await new Promise<void>((resolve) => steps.push(resolve))
419
+ handlers.onReasoningDelta?.('\n\n第二行思考')
420
+ await new Promise<void>((resolve) => steps.push(resolve))
421
+ handlers.onTextDelta('答案')
422
+ }
423
+ const { host, chat, unmount } = mountAiChat({ transport })
424
+
425
+ chat.value?.send('hi')
426
+
427
+ // 思考中:折叠态不挂载内容 DOM,头部标题为「思考中…」且右侧展示最新一行
428
+ await vi.waitFor(() => {
429
+ expect(host.querySelector('.u-ai-chat__reasoning-line')?.textContent).toBe('第一行思考')
430
+ })
431
+ expect(host.querySelector('.u-ai-chat__reasoning-title')?.textContent).toContain('思考中')
432
+ expect(host.querySelector('.u-ai-chat__reasoning-content')).toBeFalsy()
433
+
434
+ // 流式推进后滚动到最新一行(跳过空行)
435
+ steps.shift()?.()
436
+ await vi.waitFor(() => {
437
+ expect(host.querySelector('.u-ai-chat__reasoning-line')?.textContent).toBe('第二行思考')
438
+ })
439
+
440
+ // 正文开始输出后:标题回到「思考过程」,不再展示行
441
+ steps.shift()?.()
442
+ await vi.waitFor(() => {
443
+ expect(host.querySelector('.md-stub')?.textContent).toContain('答案')
444
+ })
445
+ expect(host.querySelector('.u-ai-chat__reasoning-title')?.textContent).toContain('思考过程')
446
+ expect(host.querySelector('.u-ai-chat__reasoning-line')).toBeFalsy()
447
+ unmount()
448
+ })
449
+
392
450
  it('思考过程流式结束后自动折叠并卸载内容 DOM', async () => {
393
451
  const transport: ChatTransport = (_req, handlers) => {
394
452
  handlers.onReasoningDelta?.('先分析一下问题')
@@ -513,6 +571,12 @@ describe('UAiChat', () => {
513
571
  await expandProcess(host)
514
572
 
515
573
  expect(host.querySelector('.u-ai-chat__tool-call.is-success')).toBeTruthy()
574
+
575
+ // 完成后默认折叠,展开后 render 优先于插槽
576
+ const header = host.querySelector<HTMLElement>('.u-ai-chat__tool-call .u-collapse__header')!
577
+ header.dispatchEvent(new MouseEvent('click', { bubbles: true }))
578
+ await nextTick()
579
+ await nextTick()
516
580
  expect(host.querySelector('.weather-view')).toBeTruthy()
517
581
  expect(host.querySelector('.slot-view')).toBeFalsy()
518
582
  unmount()
@@ -1319,6 +1383,11 @@ describe('UAiChat', () => {
1319
1383
  expect(host.querySelector('.u-ai-chat__tool-call-name')?.classList.contains('u-shine')).toBe(
1320
1384
  true
1321
1385
  )
1386
+ // 进行中默认折叠
1387
+ expect(host.querySelector('.u-ai-chat__tool-call.is-active')).toBeFalsy()
1388
+
1389
+ // 手动展开可查看参数
1390
+ await expandToolCard(host)
1322
1391
  expect(host.textContent).toContain('"q": "hi"')
1323
1392
 
1324
1393
  call.status = 'success'
@@ -1326,7 +1395,7 @@ describe('UAiChat', () => {
1326
1395
  await nextTick()
1327
1396
  assertGenericCard(host, 'success')
1328
1397
 
1329
- await expandToolCard(host)
1398
+ // 用户已手动展开过,进入终态后保持展开
1330
1399
  expect(host.textContent).toContain('"ok": true')
1331
1400
  unmount()
1332
1401
  })
@@ -32,6 +32,10 @@
32
32
  <span :class="[cls.e('reasoning-title'), isThinking && 'u-shine']">
33
33
  {{ isThinking ? '思考中…' : '思考过程' }}
34
34
  </span>
35
+ <!-- 折叠态在头部右侧滚动展示最新一行思考 -->
36
+ <span v-if="latestReasoningLine" :class="cls.e('reasoning-line')">
37
+ {{ latestReasoningLine }}
38
+ </span>
35
39
  <UIcon :class="cls.e('reasoning-chevron')"><ArrowRight /></UIcon>
36
40
  </div>
37
41
  <!-- 折叠时完全卸载内容 DOM:思考文本可能很长,常驻挂载浪费内存与渲染成本 -->
@@ -127,12 +131,24 @@ const isStreaming = computed(() => props.message.status === 'streaming')
127
131
  /** 思考中:流式且正文还未开始输出 */
128
132
  const isThinking = computed(() => isStreaming.value && !props.message.content)
129
133
 
130
- const reasoningExpanded = ref(true)
134
+ /** 思考默认折叠(含流式进行中),用户点击头部展开 */
135
+ const reasoningExpanded = ref(false)
136
+
137
+ /** 折叠时头部右侧滚动展示的最新一行思考:取最后一行非空文本,随流式输出持续刷新 */
138
+ const latestReasoningLine = computed(() => {
139
+ if (!isThinking.value || reasoningExpanded.value) return ''
140
+ const lines = (props.message.reasoning ?? '').split('\n')
141
+ for (let i = lines.length - 1; i >= 0; i--) {
142
+ const line = lines[i]?.trim()
143
+ if (line) return line
144
+ }
145
+ return ''
146
+ })
131
147
 
132
148
  /** 思考区吸附底部:用户在思考区内上滚时取消吸附,回到底部后恢复 */
133
149
  const reasoningStick = ref(true)
134
150
 
135
- /** 思考内容滚动区:流式输出时吸附底部,始终展示最新思考 */
151
+ /** 思考内容滚动区:展开且流式输出时吸附底部,始终展示最新思考 */
136
152
  const reasoningScrollRef = useTemplateRef<InstanceType<typeof UScroll>>('reasoningScrollRef')
137
153
 
138
154
  const toggleReasoning = async () => {
@@ -152,20 +168,6 @@ const handleReasoningScroll = (position: Required<ScrollPosition>) => {
152
168
  if (position.sh - position.ch - position.y < 8) reasoningStick.value = true
153
169
  }
154
170
 
155
- // 流式结束后自动折叠思考过程;immediate 保证终态消息重挂载(过程块展开/收起、载入历史会话)时呈折叠态
156
- watch(
157
- () => props.message.status,
158
- (status) => {
159
- if (status === 'streaming') {
160
- reasoningExpanded.value = true
161
- reasoningStick.value = true
162
- } else if (status) {
163
- reasoningExpanded.value = false
164
- }
165
- },
166
- { immediate: true }
167
- )
168
-
169
171
  watch(
170
172
  () => props.message.reasoning,
171
173
  async () => {
@@ -3,7 +3,6 @@
3
3
  <UScroll
4
4
  :class="cls.e('list')"
5
5
  ref="scrollRef"
6
- always
7
6
  @scroll="handleScroll"
8
7
  @wheel.passive="handleWheel"
9
8
  >
@@ -83,9 +82,14 @@
83
82
 
84
83
  <!-- 用户上翻浏览时悬浮入口:一键回到最新消息 -->
85
84
  <Transition :name="cls.em('to-latest', 'fade')">
86
- <button v-if="showToLatest" type="button" :class="cls.e('to-latest')" @click="scrollToLatest">
85
+ <button
86
+ v-if="showToLatest"
87
+ type="button"
88
+ :class="cls.e('to-latest')"
89
+ title="回到最新"
90
+ @click="scrollToLatest"
91
+ >
87
92
  <UIcon><ArrowDown /></UIcon>
88
- <span>最新消息</span>
89
93
  </button>
90
94
  </Transition>
91
95
  </div>
@@ -387,7 +391,7 @@ const stickToBottom = ref(true)
387
391
  /** 上一次滚动位置:用于区分用户上翻与自动吸底触发的 scroll 事件 */
388
392
  let lastScrollY = 0
389
393
 
390
- /** 上翻阅读时展示「最新消息」悬浮按钮 */
394
+ /** 上翻阅读时展示回到底部的悬浮按钮 */
391
395
  const showToLatest = computed(() => !stickToBottom.value && visibleMessages.value.length > 0)
392
396
 
393
397
  const handleScroll = (position: Required<ScrollPosition>) => {
@@ -186,6 +186,9 @@ $root-name: ai-chat;
186
186
  max-width: 800px;
187
187
  display: flex;
188
188
  flex-direction: column;
189
+ // 纵向留出呼吸位:首条消息不贴容器顶,滚动时内容不顶边;横向由各消息自身 padding 控制
190
+ padding: fn.use-var(gap, default) 0;
191
+ box-sizing: border-box;
189
192
  }
190
193
 
191
194
  @include m.e(list) {
@@ -343,21 +346,22 @@ $root-name: ai-chat;
343
346
  }
344
347
  }
345
348
 
346
- // 「最新消息」悬浮按钮:上翻浏览时出现,点击回到底部
349
+ // 回到底部悬浮按钮:上翻浏览时出现于列表下方,点击回到底部
347
350
  @include m.e(to-latest) {
348
351
  position: absolute;
349
- top: fn.use-var(gap, small);
352
+ bottom: fn.use-var(gap, small);
350
353
  left: 50%;
351
354
  transform: translateX(-50%);
352
355
  z-index: 1;
356
+ width: 28px;
357
+ height: 28px;
358
+ padding: 0;
353
359
  @include m.flex(flex, center, center);
354
- gap: 4px;
355
- padding: 4px 12px;
356
360
  border: none;
357
- border-radius: 999px;
361
+ border-radius: 50%;
358
362
  background-color: fn.use-var(bg-color, top);
359
363
  color: fn.use-var(text-color, second);
360
- font-size: fn.use-var(font-size-main, small);
364
+ font-size: 14px;
361
365
  box-shadow: 0 2px 8px fn.color-a(shadow, 20, color);
362
366
  cursor: pointer;
363
367
  transition: color 0.15s ease;
@@ -381,12 +385,12 @@ $root-name: ai-chat;
381
385
 
382
386
  @include m.em(to-latest, 'fade-enter-from') {
383
387
  opacity: 0;
384
- transform: translateX(-50%) translateY(-4px);
388
+ transform: translateX(-50%) translateY(4px);
385
389
  }
386
390
 
387
391
  @include m.em(to-latest, 'fade-leave-to') {
388
392
  opacity: 0;
389
- transform: translateX(-50%) translateY(-4px);
393
+ transform: translateX(-50%) translateY(4px);
390
394
  }
391
395
 
392
396
  @include m.e(message) {
@@ -497,9 +501,19 @@ $root-name: ai-chat;
497
501
  }
498
502
 
499
503
  @include m.e(reasoning-title) {
504
+ flex-shrink: 0;
500
505
  line-height: 1.5;
501
506
  }
502
507
 
508
+ // 折叠态头部右侧滚动展示的最新一行思考
509
+ @include m.e(reasoning-line) {
510
+ flex: 1;
511
+ min-width: 0;
512
+ @include m.ellipsis;
513
+ line-height: 1.5;
514
+ color: fn.use-var(text-color, assist);
515
+ }
516
+
503
517
  @include m.e(reasoning-chevron) {
504
518
  font-size: 12px;
505
519
  transition: transform 0.2s;
@@ -10,4 +10,6 @@ import '@veltra/desktop/components/scroll/style'
10
10
  import '@veltra/desktop/components/select/style'
11
11
  import '@veltra/styles/animations/shine.scss'
12
12
  import 'markstream-vue/index.css'
13
+ // 活体球为 ai-chat 内部组件,样式随 ai-chat 一并加载
14
+ import '../ai-orb/style.scss'
13
15
  import './style.scss'