@veltra/ai 1.4.0 → 2.1.0

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 (87) hide show
  1. package/dist/chat/index.d.ts +3 -3
  2. package/dist/chat/transports/index.d.ts +2 -2
  3. package/dist/chat/transports/openai.d.ts +16 -12
  4. package/dist/chat/transports/openai.js +56 -15
  5. package/dist/chat/transports/openai.js.map +1 -1
  6. package/dist/chat/types.d.ts +41 -2
  7. package/dist/chat/use-chat.d.ts +26 -1
  8. package/dist/chat/use-chat.js +132 -13
  9. package/dist/chat/use-chat.js.map +1 -1
  10. package/dist/components/ai-chat/ai-chat.js +222 -41
  11. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  12. package/dist/components/ai-chat/ask-question.js +2 -2
  13. package/dist/components/ai-chat/ask-question.js.map +1 -1
  14. package/dist/components/ai-chat/chat-input.js +81 -44
  15. package/dist/components/ai-chat/chat-input.js.map +1 -1
  16. package/dist/components/ai-chat/di.js.map +1 -1
  17. package/dist/components/ai-chat/message-item.js +101 -23
  18. package/dist/components/ai-chat/message-item.js.map +1 -1
  19. package/dist/components/ai-chat/message-list.js +217 -32
  20. package/dist/components/ai-chat/message-list.js.map +1 -1
  21. package/dist/components/ai-chat/model-picker.js +140 -0
  22. package/dist/components/ai-chat/model-picker.js.map +1 -0
  23. package/dist/components/ai-chat/queue-list.js +70 -0
  24. package/dist/components/ai-chat/queue-list.js.map +1 -0
  25. package/dist/components/ai-chat/side-panel.js +96 -0
  26. package/dist/components/ai-chat/side-panel.js.map +1 -0
  27. package/dist/components/ai-chat/style.d.ts +6 -0
  28. package/dist/components/ai-chat/style.js +6 -0
  29. package/dist/components/ai-chat/style2.css +552 -41
  30. package/dist/components/ai-chat/tool-call.js +61 -27
  31. package/dist/components/ai-chat/tool-call.js.map +1 -1
  32. package/dist/components/ai-orb/ai-orb.js +94 -0
  33. package/dist/components/ai-orb/ai-orb.js.map +1 -0
  34. package/dist/components/ai-orb/orb-renderer.js +411 -0
  35. package/dist/components/ai-orb/orb-renderer.js.map +1 -0
  36. package/dist/components/ai-orb/style.d.ts +1 -0
  37. package/dist/components/ai-orb/style.js +1 -0
  38. package/dist/components/ai-orb/style2.css +23 -0
  39. package/dist/index.d.ts +8 -6
  40. package/dist/index.js +2 -2
  41. package/dist/providers/types.d.ts +47 -0
  42. package/dist/style.js +1 -0
  43. package/dist/tools/ask-question/ask-question.d.ts +2 -17
  44. package/dist/tools/ask-question/index.d.ts +2 -2
  45. package/dist/tools/index.d.ts +3 -2
  46. package/dist/tools/index.js +10 -0
  47. package/dist/tools/index.js.map +1 -0
  48. package/dist/types/ai-chat.d.ts +35 -7
  49. package/dist/types/ai-orb.d.ts +27 -0
  50. package/dist/types/index.d.ts +2 -1
  51. package/package.json +8 -8
  52. package/src/chat/__test__/use-chat.test.ts +295 -9
  53. package/src/chat/transports/__test__/openai.test.ts +317 -0
  54. package/src/chat/transports/openai.ts +88 -26
  55. package/src/chat/types.ts +41 -1
  56. package/src/chat/use-chat.ts +177 -14
  57. package/src/components/ai-chat/__test__/ai-chat.test.ts +690 -12
  58. package/src/components/ai-chat/ai-chat.vue +241 -31
  59. package/src/components/ai-chat/chat-input.vue +78 -31
  60. package/src/components/ai-chat/di.ts +2 -0
  61. package/src/components/ai-chat/message-item.vue +98 -11
  62. package/src/components/ai-chat/message-list.vue +261 -21
  63. package/src/components/ai-chat/model-picker.vue +177 -0
  64. package/src/components/ai-chat/queue-list.vue +63 -0
  65. package/src/components/ai-chat/side-panel.vue +76 -0
  66. package/src/components/ai-chat/style.scss +621 -43
  67. package/src/components/ai-chat/style.ts +6 -0
  68. package/src/components/ai-chat/tool-call.vue +86 -42
  69. package/src/components/ai-orb/__test__/ai-orb.test.ts +251 -0
  70. package/src/components/ai-orb/ai-orb.vue +107 -0
  71. package/src/components/ai-orb/index.ts +1 -0
  72. package/src/components/ai-orb/orb-renderer.ts +505 -0
  73. package/src/components/ai-orb/style.scss +10 -0
  74. package/src/components/ai-orb/style.ts +1 -0
  75. package/src/components/index.ts +1 -0
  76. package/src/index.ts +8 -1
  77. package/src/providers/index.ts +1 -0
  78. package/src/providers/types.ts +46 -0
  79. package/src/style.ts +1 -0
  80. package/src/tools/ask-question/__test__/ask-question.test.ts +4 -4
  81. package/src/tools/index.ts +14 -1
  82. package/src/types/ai-chat.ts +35 -6
  83. package/src/types/ai-orb.ts +27 -0
  84. package/src/types/index.ts +1 -0
  85. package/dist/components/ai-chat/ai-chat.vue.d.ts +0 -36
  86. package/dist/components/ai-chat/index.d.ts +0 -2
  87. package/dist/components/index.d.ts +0 -3
@@ -1,9 +1,8 @@
1
1
  import { AiChatDIKey } from "./di.js";
2
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, ref, resolveDynamicComponent, toDisplayString, unref, watch, withCtx, withModifiers } from "vue";
3
- import { bem } from "@veltra/utils";
2
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, ref, resolveDynamicComponent, toDisplayString, unref, watch, withCtx, withKeys, withModifiers } from "vue";
3
+ import { ArrowDown, CircleCheckFilled, CircleClose, Close, Loading, WarningFilled } from "@veltra/icons/normal";
4
4
  import { UButton, UCollapseItem, UIcon } from "@veltra/desktop";
5
- import { CircleCheckFilled, CircleClose, Close, Loading, WarningFilled } from "@veltra/icons/normal";
6
- import "@veltra/desktop/components/collapse/style";
5
+ import { bem } from "@veltra/utils";
7
6
  //#region src/components/ai-chat/tool-call.vue
8
7
  const _sfc_main = /*@__PURE__*/ defineComponent({
9
8
  name: "UAiChatToolCall",
@@ -15,16 +14,19 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
15
14
  const emit = __emit;
16
15
  const di = inject(AiChatDIKey);
17
16
  const cls = di?.cls ?? bem("ai-chat");
18
- /** 当前调用对应的工具定义(解析 icon/label/render/autoCollapse) */
17
+ /** 当前调用对应的工具定义(解析 icon/label/render/autoCollapse/terminal) */
19
18
  const tool = computed(() => di?.tools.value[props.toolCall.name]);
20
19
  const isExpanded = ref(false);
21
20
  /** 用户手动切换过后,不再随 status 自动展开/折叠 */
22
21
  let userToggled = false;
23
- function markUserToggled() {
22
+ /** UCollapseItem 的 change 仅在用户点击头部时触发(程序化 v-model 变更不触发) */
23
+ const markUserToggled = () => {
24
24
  userToggled = true;
25
- }
26
- /** 完成后是否自动折叠:缺省有 render 时不折叠,否则折叠 */
27
- const autoCollapse = computed(() => tool.value?.autoCollapse ?? !tool.value?.render);
25
+ };
26
+ /** 面板工具:render 展示在右侧侧边面板,卡片 body 仅保留「查看面板」入口 */
27
+ const isPanelTool = computed(() => tool.value?.renderTo === "panel");
28
+ /** 完成后是否自动折叠:缺省面板工具折叠;有 render 时不折叠,否则折叠 */
29
+ const autoCollapse = computed(() => tool.value?.autoCollapse ?? (isPanelTool.value || !tool.value?.render));
28
30
  watch(() => props.toolCall.status, (status) => {
29
31
  if (userToggled) return;
30
32
  if (status === "running" || status === "awaiting-confirm") isExpanded.value = true;
@@ -33,11 +35,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
33
35
  const STATUS_META = {
34
36
  pending: {
35
37
  icon: Loading,
36
- class: "is-loading"
38
+ class: "is-active"
37
39
  },
38
40
  running: {
39
41
  icon: Loading,
40
- class: "is-loading"
42
+ class: "is-active"
41
43
  },
42
44
  "awaiting-confirm": {
43
45
  icon: WarningFilled,
@@ -58,6 +60,23 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
58
60
  };
59
61
  const statusIcon = computed(() => STATUS_META[props.toolCall.status]?.icon ?? Loading);
60
62
  const statusClass = computed(() => STATUS_META[props.toolCall.status]?.class ?? "");
63
+ /** 进行中(pending/running):工具名带扫光效果 */
64
+ const isActive = computed(() => {
65
+ return props.toolCall.status === "pending" || props.toolCall.status === "running";
66
+ });
67
+ /** 终态(成功/失败/拒绝) */
68
+ const isSettled = computed(() => {
69
+ return [
70
+ "success",
71
+ "error",
72
+ "rejected"
73
+ ].includes(props.toolCall.status);
74
+ });
75
+ /**
76
+ * 折叠后卸载内容 DOM:仅终态且非面板工具时启用——
77
+ * 进行中/待确认要保留内容状态(确认按钮、表单),面板工具的 body 是「查看面板」入口必须可达
78
+ */
79
+ const destroyOnCollapse = computed(() => isSettled.value && !isPanelTool.value);
61
80
  /** 头部图标:工具自定义图标优先,缺省用状态图标 */
62
81
  const headerIcon = computed(() => tool.value?.icon ?? statusIcon.value);
63
82
  const parsedArguments = computed(() => {
@@ -97,21 +116,19 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
97
116
  return slot ? () => slot({ toolCall: props.toolCall }) : void 0;
98
117
  });
99
118
  return (_ctx, _cache) => {
100
- return openBlock(), createElementBlock("div", { class: normalizeClass([
101
- unref(cls).e("tool-call"),
102
- unref(bem).is(__props.toolCall.status),
103
- unref(bem).is("expanded", isExpanded.value)
104
- ]) }, [createVNode(unref(UCollapseItem), {
119
+ return openBlock(), createBlock(unref(UCollapseItem), {
105
120
  modelValue: isExpanded.value,
106
- "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => isExpanded.value = $event),
121
+ "onUpdate:modelValue": _cache[6] || (_cache[6] = ($event) => isExpanded.value = $event),
122
+ class: normalizeClass([unref(cls).e("tool-call"), unref(bem).is(__props.toolCall.status)]),
123
+ "destroy-on-collapse": destroyOnCollapse.value,
107
124
  onChange: markUserToggled
108
125
  }, {
109
- title: withCtx(() => [
126
+ header: withCtx(() => [
110
127
  createVNode(unref(UIcon), { class: normalizeClass([unref(cls).e("tool-call-status"), statusClass.value]) }, {
111
128
  default: withCtx(() => [(openBlock(), createBlock(resolveDynamicComponent(headerIcon.value)))]),
112
129
  _: 1
113
130
  }, 8, ["class"]),
114
- createElementVNode("span", { class: normalizeClass(unref(cls).e("tool-call-name")) }, toDisplayString(tool.value?.label ?? __props.toolCall.name), 3),
131
+ createElementVNode("span", { class: normalizeClass([unref(cls).e("tool-call-name"), isActive.value && "u-shine"]) }, toDisplayString(tool.value?.label ?? __props.toolCall.name), 3),
115
132
  summary.value ? (openBlock(), createElementBlock("span", {
116
133
  key: 0,
117
134
  class: normalizeClass(unref(cls).e("tool-call-summary"))
@@ -119,29 +136,46 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
119
136
  __props.toolCall.status === "awaiting-confirm" ? (openBlock(), createElementBlock("span", {
120
137
  key: 1,
121
138
  class: normalizeClass(unref(cls).e("tool-call-confirm")),
122
- onClick: _cache[2] || (_cache[2] = withModifiers(() => {}, ["stop"]))
139
+ onClick: _cache[2] || (_cache[2] = withModifiers(() => {}, ["stop"])),
140
+ onKeydown: [_cache[3] || (_cache[3] = withKeys(withModifiers(() => {}, ["stop"]), ["enter"])), _cache[4] || (_cache[4] = withKeys(withModifiers(() => {}, ["stop"]), ["space"]))]
123
141
  }, [createVNode(unref(UButton), {
124
142
  size: "small",
125
143
  type: "primary",
126
144
  onClick: _cache[0] || (_cache[0] = ($event) => emit("respond", true))
127
145
  }, {
128
- default: withCtx(() => [..._cache[4] || (_cache[4] = [createTextVNode("允许", -1)])]),
146
+ default: withCtx(() => [..._cache[7] || (_cache[7] = [createTextVNode("允许", -1)])]),
129
147
  _: 1
130
148
  }), createVNode(unref(UButton), {
131
149
  size: "small",
132
150
  text: "",
133
151
  onClick: _cache[1] || (_cache[1] = ($event) => emit("respond", false))
134
152
  }, {
135
- default: withCtx(() => [..._cache[5] || (_cache[5] = [createTextVNode("拒绝", -1)])]),
153
+ default: withCtx(() => [..._cache[8] || (_cache[8] = [createTextVNode("拒绝", -1)])]),
154
+ _: 1
155
+ })], 34)) : createCommentVNode("v-if", true),
156
+ createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("tool-call-chevron")) }, {
157
+ default: withCtx(() => [createVNode(unref(ArrowDown))]),
136
158
  _: 1
137
- })], 2)) : createCommentVNode("v-if", true)
159
+ }, 8, ["class"])
138
160
  ]),
139
- default: withCtx(() => [tool.value?.render ? (openBlock(), createBlock(resolveDynamicComponent(tool.value?.render), {
161
+ default: withCtx(() => [isPanelTool.value ? (openBlock(), createElementBlock("div", {
140
162
  key: 0,
141
- "tool-call": __props.toolCall
142
- }, null, 8, ["tool-call"])) : slotBody.value && hasResult.value ? (openBlock(), createBlock(resolveDynamicComponent(slotBody.value), { key: 1 })) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [prettyArguments.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("div", { class: normalizeClass(unref(cls).e("tool-call-section")) }, "参数", 2), createElementVNode("pre", { class: normalizeClass(unref(cls).e("tool-call-code")) }, toDisplayString(prettyArguments.value), 3)], 64)) : createCommentVNode("v-if", true), hasResult.value ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("div", { class: normalizeClass(unref(cls).e("tool-call-section")) }, toDisplayString(__props.toolCall.status === "error" ? "错误" : "结果"), 3), createElementVNode("pre", { class: normalizeClass(unref(cls).e("tool-call-code")) }, toDisplayString(displayResult.value), 3)], 64)) : createCommentVNode("v-if", true)], 64))]),
163
+ class: normalizeClass(unref(cls).e("tool-call-panel-entry"))
164
+ }, [createElementVNode("span", { class: normalizeClass(unref(cls).e("tool-call-panel-hint")) }, "内容展示在右侧面板", 2), createVNode(unref(UButton), {
165
+ size: "small",
166
+ text: "",
167
+ type: "primary",
168
+ onClick: _cache[5] || (_cache[5] = ($event) => unref(di)?.openPanel(__props.toolCall.id))
169
+ }, {
170
+ default: withCtx(() => [..._cache[9] || (_cache[9] = [createTextVNode(" 查看面板 ", -1)])]),
171
+ _: 1
172
+ })], 2)) : tool.value?.render ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createCommentVNode(" 自定义渲染:工具定义的 render 优先于 tool-<name> 插槽,均替换整个 body "), (openBlock(), createBlock(resolveDynamicComponent(tool.value.render), { "tool-call": __props.toolCall }, null, 8, ["tool-call"]))], 2112)) : slotBody.value && hasResult.value ? (openBlock(), createBlock(resolveDynamicComponent(slotBody.value), { key: 2 })) : (openBlock(), createElementBlock(Fragment, { key: 3 }, [prettyArguments.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("div", { class: normalizeClass(unref(cls).e("tool-call-section")) }, "参数", 2), createElementVNode("pre", { class: normalizeClass(unref(cls).e("tool-call-code")) }, toDisplayString(prettyArguments.value), 3)], 64)) : createCommentVNode("v-if", true), hasResult.value ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("div", { class: normalizeClass(unref(cls).e("tool-call-section")) }, toDisplayString(__props.toolCall.status === "error" ? "错误" : "结果"), 3), createElementVNode("pre", { class: normalizeClass(unref(cls).e("tool-call-code")) }, toDisplayString(displayResult.value), 3)], 64)) : createCommentVNode("v-if", true)], 64))]),
143
173
  _: 1
144
- }, 8, ["modelValue"])], 2);
174
+ }, 8, [
175
+ "modelValue",
176
+ "class",
177
+ "destroy-on-collapse"
178
+ ]);
145
179
  };
146
180
  }
147
181
  });
@@ -1 +1 @@
1
- {"version":3,"file":"tool-call.js","names":[],"sources":["../../../src/components/ai-chat/tool-call.vue"],"sourcesContent":["<template>\n <div :class=\"[cls.e('tool-call'), bem.is(toolCall.status), bem.is('expanded', isExpanded)]\">\n <UCollapseItem v-model=\"isExpanded\" @change=\"markUserToggled\">\n <template #title>\n <UIcon :class=\"[cls.e('tool-call-status'), statusClass]\">\n <component :is=\"headerIcon\" />\n </UIcon>\n <span :class=\"cls.e('tool-call-name')\">{{ tool?.label ?? toolCall.name }}</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 >\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 优先于 tool-<name> 插槽,均替换整个 body -->\n <component :is=\"tool?.render\" v-if=\"tool?.render\" :tool-call=\"toolCall\" />\n <component :is=\"slotBody\" v-else-if=\"slotBody && hasResult\" />\n <template v-else>\n <template v-if=\"prettyArguments\">\n <div :class=\"cls.e('tool-call-section')\">参数</div>\n <pre :class=\"cls.e('tool-call-code')\">{{ prettyArguments }}</pre>\n </template>\n\n <template v-if=\"hasResult\">\n <div :class=\"cls.e('tool-call-section')\">\n {{ toolCall.status === 'error' ? '错误' : '结果' }}\n </div>\n <pre :class=\"cls.e('tool-call-code')\">{{ displayResult }}</pre>\n </template>\n </template>\n </UCollapseItem>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UCollapseItem, UIcon } from '@veltra/desktop'\nimport '@veltra/desktop/components/collapse/style'\nimport { CircleCheckFilled, CircleClose, Close, Loading, WarningFilled } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, watch, type Component } from 'vue'\n\nimport type { ChatToolCall } from '../../chat/types'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatToolCall' })\n\nconst props = defineProps<{ toolCall: ChatToolCall }>()\n\nconst emit = defineEmits<{ (e: 'respond', approved: boolean): void }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 当前调用对应的工具定义(解析 icon/label/render/autoCollapse) */\nconst tool = computed(() => di?.tools.value[props.toolCall.name])\n\nconst isExpanded = ref(false)\n/** 用户手动切换过后,不再随 status 自动展开/折叠 */\nlet userToggled: boolean = false\n\nfunction markUserToggled() {\n userToggled = true\n}\n\n/** 完成后是否自动折叠:缺省有 render 时不折叠,否则折叠 */\nconst autoCollapse = computed(() => tool.value?.autoCollapse ?? !tool.value?.render)\n\nwatch(\n () => props.toolCall.status,\n (status) => {\n if (userToggled) return\n if (status === 'running' || status === 'awaiting-confirm') {\n isExpanded.value = true\n } else {\n isExpanded.value = !autoCollapse.value\n }\n },\n { immediate: true }\n)\n\nconst STATUS_META: Record<string, { icon: Component; class: string }> = {\n pending: { icon: Loading, class: 'is-loading' },\n running: { icon: Loading, class: 'is-loading' },\n 'awaiting-confirm': { icon: WarningFilled, class: 'is-warning' },\n success: { icon: CircleCheckFilled, class: 'is-success' },\n error: { icon: CircleClose, class: 'is-danger' },\n rejected: { icon: Close, class: 'is-danger' }\n}\n\nconst statusIcon = computed(() => STATUS_META[props.toolCall.status]?.icon ?? Loading)\nconst statusClass = computed(() => STATUS_META[props.toolCall.status]?.class ?? '')\n\n/** 头部图标:工具自定义图标优先,缺省用状态图标 */\nconst headerIcon = computed(() => tool.value?.icon ?? statusIcon.value)\n\nconst parsedArguments = computed<Record<string, unknown> | null>(() => {\n if (!props.toolCall.arguments) return null\n try {\n return JSON.parse(props.toolCall.arguments)\n } catch {\n return null\n }\n})\n\n/** 折叠态参数摘要:取第一个字符串参数值 */\nconst summary = computed(() => {\n const args = parsedArguments.value\n if (!args) return ''\n for (const value of Object.values(args)) {\n if (typeof value === 'string' && value) {\n return value.length > 40 ? `${value.slice(0, 40)}…` : value\n }\n }\n return ''\n})\n\nconst prettyArguments = computed(() => {\n const args = parsedArguments.value\n if (args) return JSON.stringify(args, null, 2)\n return props.toolCall.arguments || ''\n})\n\nconst hasResult = computed(() => {\n return props.toolCall.result != null || props.toolCall.error != null\n})\n\nconst displayResult = computed(() => {\n const raw = props.toolCall.error ?? props.toolCall.result ?? ''\n try {\n return JSON.stringify(JSON.parse(raw), null, 2)\n } catch {\n return raw\n }\n})\n\n/** 使用方通过 tool-<name> 插槽自定义该工具的展示(有结果时替换整个 body) */\nconst slotBody = computed(() => {\n const slot = di?.slots[`tool-${props.toolCall.name}`]\n return slot ? () => slot({ toolCall: props.toolCall }) : undefined\n})\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"tool-call.js","names":[],"sources":["../../../src/components/ai-chat/tool-call.vue"],"sourcesContent":["<template>\n <UCollapseItem\n v-model=\"isExpanded\"\n :class=\"[cls.e('tool-call'), bem.is(toolCall.status)]\"\n :destroy-on-collapse=\"destroyOnCollapse\"\n @change=\"markUserToggled\"\n >\n <template #header>\n <UIcon :class=\"[cls.e('tool-call-status'), statusClass]\">\n <component :is=\"headerIcon\" />\n </UIcon>\n <span :class=\"[cls.e('tool-call-name'), isActive && 'u-shine']\">\n {{ tool?.label ?? toolCall.name }}\n </span>\n <span v-if=\"summary\" :class=\"cls.e('tool-call-summary')\">{{ summary }}</span>\n\n <span\n v-if=\"toolCall.status === 'awaiting-confirm'\"\n :class=\"cls.e('tool-call-confirm')\"\n @click.stop\n @keydown.enter.stop\n @keydown.space.stop\n >\n <UButton size=\"small\" type=\"primary\" @click=\"emit('respond', true)\">允许</UButton>\n <UButton size=\"small\" text @click=\"emit('respond', false)\">拒绝</UButton>\n </span>\n\n <UIcon :class=\"cls.e('tool-call-chevron')\"><ArrowDown /></UIcon>\n </template>\n\n <!-- 面板工具:render 展示在右侧侧边面板,卡片 body 仅提供查看入口 -->\n <div v-if=\"isPanelTool\" :class=\"cls.e('tool-call-panel-entry')\">\n <span :class=\"cls.e('tool-call-panel-hint')\">内容展示在右侧面板</span>\n <UButton size=\"small\" text type=\"primary\" @click=\"di?.openPanel(toolCall.id)\">\n 查看面板\n </UButton>\n </div>\n <!-- 自定义渲染:工具定义的 render 优先于 tool-<name> 插槽,均替换整个 body -->\n <component :is=\"tool.render\" v-else-if=\"tool?.render\" :tool-call=\"toolCall\" />\n <component :is=\"slotBody\" v-else-if=\"slotBody && hasResult\" />\n <template v-else>\n <template v-if=\"prettyArguments\">\n <div :class=\"cls.e('tool-call-section')\">参数</div>\n <pre :class=\"cls.e('tool-call-code')\">{{ prettyArguments }}</pre>\n </template>\n\n <template v-if=\"hasResult\">\n <div :class=\"cls.e('tool-call-section')\">\n {{ toolCall.status === 'error' ? '错误' : '结果' }}\n </div>\n <pre :class=\"cls.e('tool-call-code')\">{{ displayResult }}</pre>\n </template>\n </template>\n </UCollapseItem>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UCollapseItem, UIcon } from '@veltra/desktop'\nimport {\n ArrowDown,\n CircleCheckFilled,\n CircleClose,\n Close,\n Loading,\n WarningFilled\n} from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, watch, type Component } from 'vue'\n\nimport type { ChatToolCall } from '../../chat/types'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatToolCall' })\n\nconst props = defineProps<{ toolCall: ChatToolCall }>()\n\nconst emit = defineEmits<{ (e: 'respond', approved: boolean): void }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 当前调用对应的工具定义(解析 icon/label/render/autoCollapse/terminal) */\nconst tool = computed(() => di?.tools.value[props.toolCall.name])\n\nconst isExpanded = ref(false)\n/** 用户手动切换过后,不再随 status 自动展开/折叠 */\nlet userToggled: boolean = false\n\n/** UCollapseItem 的 change 仅在用户点击头部时触发(程序化 v-model 变更不触发) */\nconst markUserToggled = () => {\n userToggled = true\n}\n\n/** 面板工具:render 展示在右侧侧边面板,卡片 body 仅保留「查看面板」入口 */\nconst isPanelTool = computed(() => tool.value?.renderTo === 'panel')\n\n/** 完成后是否自动折叠:缺省面板工具折叠;有 render 时不折叠,否则折叠 */\nconst autoCollapse = computed(\n () => tool.value?.autoCollapse ?? (isPanelTool.value || !tool.value?.render)\n)\n\nwatch(\n () => props.toolCall.status,\n (status) => {\n if (userToggled) return\n if (status === 'running' || status === 'awaiting-confirm') {\n isExpanded.value = true\n } else {\n isExpanded.value = !autoCollapse.value\n }\n },\n { immediate: true }\n)\n\nconst STATUS_META: Record<string, { icon: Component; class: string }> = {\n pending: { icon: Loading, class: 'is-active' },\n running: { icon: Loading, class: 'is-active' },\n 'awaiting-confirm': { icon: WarningFilled, class: 'is-warning' },\n success: { icon: CircleCheckFilled, class: 'is-success' },\n error: { icon: CircleClose, class: 'is-danger' },\n rejected: { icon: Close, class: 'is-danger' }\n}\n\nconst statusIcon = computed(() => STATUS_META[props.toolCall.status]?.icon ?? Loading)\nconst statusClass = computed(() => STATUS_META[props.toolCall.status]?.class ?? '')\n\n/** 进行中(pending/running):工具名带扫光效果 */\nconst isActive = computed(() => {\n return props.toolCall.status === 'pending' || props.toolCall.status === 'running'\n})\n\n/** 终态(成功/失败/拒绝) */\nconst isSettled = computed(() => {\n return ['success', 'error', 'rejected'].includes(props.toolCall.status)\n})\n\n/**\n * 折叠后卸载内容 DOM:仅终态且非面板工具时启用——\n * 进行中/待确认要保留内容状态(确认按钮、表单),面板工具的 body 是「查看面板」入口必须可达\n */\nconst destroyOnCollapse = computed(() => isSettled.value && !isPanelTool.value)\n\n/** 头部图标:工具自定义图标优先,缺省用状态图标 */\nconst headerIcon = computed(() => tool.value?.icon ?? statusIcon.value)\n\nconst parsedArguments = computed<Record<string, unknown> | null>(() => {\n if (!props.toolCall.arguments) return null\n try {\n return JSON.parse(props.toolCall.arguments)\n } catch {\n return null\n }\n})\n\n/** 折叠态参数摘要:取第一个字符串参数值 */\nconst summary = computed(() => {\n const args = parsedArguments.value\n if (!args) return ''\n for (const value of Object.values(args)) {\n if (typeof value === 'string' && value) {\n return value.length > 40 ? `${value.slice(0, 40)}…` : value\n }\n }\n return ''\n})\n\nconst prettyArguments = computed(() => {\n const args = parsedArguments.value\n if (args) return JSON.stringify(args, null, 2)\n return props.toolCall.arguments || ''\n})\n\nconst hasResult = computed(() => {\n return props.toolCall.result != null || props.toolCall.error != null\n})\n\nconst displayResult = computed(() => {\n const raw = props.toolCall.error ?? props.toolCall.result ?? ''\n try {\n return JSON.stringify(JSON.parse(raw), null, 2)\n } catch {\n return raw\n }\n})\n\n/** 使用方通过 tool-<name> 插槽自定义该工具的展示(有结果时替换整个 body) */\nconst slotBody = computed(() => {\n const slot = di?.slots[`tool-${props.toolCall.name}`]\n return slot ? () => slot({ toolCall: props.toolCall }) : undefined\n})\n</script>\n"],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import { createOrbRenderer } from "./orb-renderer.js";
2
+ import { createElementBlock, defineComponent, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, unref, useTemplateRef, watch } from "vue";
3
+ import { bem } from "@veltra/utils";
4
+ //#region src/components/ai-orb/ai-orb.vue
5
+ const _sfc_main = /*@__PURE__*/ defineComponent({
6
+ name: "UAiOrb",
7
+ __name: "ai-orb",
8
+ props: {
9
+ size: { default: 48 },
10
+ status: { default: "idle" }
11
+ },
12
+ emits: ["click"],
13
+ setup(__props, { expose: __expose, emit: __emit }) {
14
+ const props = __props;
15
+ const emit = __emit;
16
+ const cls = bem("ai-orb");
17
+ const canvasRef = useTemplateRef("canvasRef");
18
+ let renderer = null;
19
+ let observer = null;
20
+ let motionQuery = null;
21
+ /** 指针坐标 → 单位球空间(球心为原点,1 = 球半径),交给渲染器做视线跟随 */
22
+ const handlePointerMove = (e) => {
23
+ const el = canvasRef.value;
24
+ if (!el || !renderer) return;
25
+ const rect = el.getBoundingClientRect();
26
+ const ballRadius = rect.width * .36;
27
+ renderer.setPointer({
28
+ x: (e.clientX - rect.left - rect.width / 2) / ballRadius,
29
+ y: (e.clientY - rect.top - rect.height / 2) / ballRadius
30
+ });
31
+ };
32
+ const handlePointerLeave = () => renderer?.setPointer(null);
33
+ const handlePointerDown = () => renderer?.poke();
34
+ const applyVisibility = (visible) => {
35
+ if (!renderer) return;
36
+ if (motionQuery?.matches) {
37
+ renderer.stop();
38
+ renderer.renderOnce();
39
+ return;
40
+ }
41
+ if (visible) renderer.start();
42
+ else renderer.stop();
43
+ };
44
+ const handleMotionChange = () => {
45
+ applyVisibility(true);
46
+ };
47
+ onMounted(() => {
48
+ const canvas = canvasRef.value;
49
+ if (!canvas) return;
50
+ renderer = createOrbRenderer(canvas, {
51
+ size: props.size,
52
+ status: props.status
53
+ });
54
+ motionQuery = window.matchMedia?.("(prefers-reduced-motion: reduce)") ?? null;
55
+ motionQuery?.addEventListener?.("change", handleMotionChange);
56
+ if (typeof IntersectionObserver !== "undefined") {
57
+ observer = new IntersectionObserver((entries) => {
58
+ applyVisibility(entries[0]?.isIntersecting ?? true);
59
+ });
60
+ observer.observe(canvas);
61
+ } else applyVisibility(true);
62
+ });
63
+ watch(() => props.status, (status) => renderer?.setStatus(status));
64
+ watch(() => props.size, (size) => renderer?.resize(size));
65
+ const react = (reaction) => renderer?.trigger(reaction);
66
+ __expose({ react });
67
+ onBeforeUnmount(() => {
68
+ observer?.disconnect();
69
+ motionQuery?.removeEventListener?.("change", handleMotionChange);
70
+ renderer?.stop();
71
+ renderer = null;
72
+ });
73
+ return (_ctx, _cache) => {
74
+ return openBlock(), createElementBlock("canvas", {
75
+ ref_key: "canvasRef",
76
+ ref: canvasRef,
77
+ class: normalizeClass(unref(cls).b),
78
+ style: normalizeStyle({
79
+ width: `${__props.size}px`,
80
+ height: `${__props.size}px`
81
+ }),
82
+ "aria-hidden": "true",
83
+ onPointermove: handlePointerMove,
84
+ onPointerleave: handlePointerLeave,
85
+ onPointerdown: handlePointerDown,
86
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("click"))
87
+ }, null, 38);
88
+ };
89
+ }
90
+ });
91
+ //#endregion
92
+ export { _sfc_main as default };
93
+
94
+ //# sourceMappingURL=ai-orb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-orb.js","names":[],"sources":["../../../src/components/ai-orb/ai-orb.vue"],"sourcesContent":["<template>\n <canvas\n ref=\"canvasRef\"\n :class=\"cls.b\"\n :style=\"{ width: `${size}px`, height: `${size}px` }\"\n aria-hidden=\"true\"\n @pointermove=\"handlePointerMove\"\n @pointerleave=\"handlePointerLeave\"\n @pointerdown=\"handlePointerDown\"\n @click=\"emit('click')\"\n />\n</template>\n\n<script lang=\"ts\" setup>\nimport { bem } from '@veltra/utils'\nimport { onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue'\n\nimport type { AiOrbEmits, AiOrbExposed, AiOrbProps, AiOrbReaction } from '../../types/ai-orb'\nimport { createOrbRenderer, type AiOrbRenderer } from './orb-renderer'\n\ndefineOptions({ name: 'UAiOrb' })\n\nconst props = withDefaults(defineProps<AiOrbProps>(), { size: 48, status: 'idle' })\nconst emit = defineEmits<AiOrbEmits>()\n\nconst cls = bem('ai-orb')\n\nconst canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')\n\nlet renderer: AiOrbRenderer | null = null\nlet observer: IntersectionObserver | null = null\nlet motionQuery: MediaQueryList | null = null\n\n/** 指针坐标 → 单位球空间(球心为原点,1 = 球半径),交给渲染器做视线跟随 */\nconst handlePointerMove = (e: PointerEvent) => {\n const el = canvasRef.value\n if (!el || !renderer) return\n const rect = el.getBoundingClientRect()\n const ballRadius = rect.width * 0.36\n renderer.setPointer({\n x: (e.clientX - rect.left - rect.width / 2) / ballRadius,\n y: (e.clientY - rect.top - rect.height / 2) / ballRadius\n })\n}\n\nconst handlePointerLeave = () => renderer?.setPointer(null)\n\nconst handlePointerDown = () => renderer?.poke()\n\nconst applyVisibility = (visible: boolean) => {\n if (!renderer) return\n if (motionQuery?.matches) {\n // 减少动态偏好:只画一帧静态画面,不启动画循环\n renderer.stop()\n renderer.renderOnce()\n return\n }\n if (visible) renderer.start()\n else renderer.stop()\n}\n\nconst handleMotionChange = () => {\n // 偏好像素级开关变化时重评估(不可见时 start 也会被 IntersectionObserver 纠正)\n applyVisibility(true)\n}\n\nonMounted(() => {\n const canvas = canvasRef.value\n if (!canvas) return\n\n renderer = createOrbRenderer(canvas, { size: props.size, status: props.status })\n\n motionQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)') ?? null\n motionQuery?.addEventListener?.('change', handleMotionChange)\n\n if (typeof IntersectionObserver !== 'undefined') {\n // 滚出视口的球暂停动画循环,长会话下同屏多个球也不累积开销\n observer = new IntersectionObserver((entries) => {\n applyVisibility(entries[0]?.isIntersecting ?? true)\n })\n observer.observe(canvas)\n } else {\n applyVisibility(true)\n }\n})\n\nwatch(\n () => props.status,\n (status) => renderer?.setStatus(status)\n)\n\nwatch(\n () => props.size,\n (size) => renderer?.resize(size)\n)\n\nconst react = (reaction: AiOrbReaction) => renderer?.trigger(reaction)\n\ndefineExpose<AiOrbExposed>({ react })\n\nonBeforeUnmount(() => {\n observer?.disconnect()\n motionQuery?.removeEventListener?.('change', handleMotionChange)\n renderer?.stop()\n renderer = null\n})\n</script>\n"],"mappings":";;;;;;;;;;;;;EAsBA,MAAM,QAAQ;EACd,MAAM,OAAO;EAEb,MAAM,MAAM,IAAI,QAAQ;EAExB,MAAM,YAAY,eAAkC,WAAW;EAE/D,IAAI,WAAiC;EACrC,IAAI,WAAwC;EAC5C,IAAI,cAAqC;;EAGzC,MAAM,qBAAqB,MAAoB;GAC7C,MAAM,KAAK,UAAU;GACrB,IAAI,CAAC,MAAM,CAAC,UAAU;GACtB,MAAM,OAAO,GAAG,sBAAsB;GACtC,MAAM,aAAa,KAAK,QAAQ;GAChC,SAAS,WAAW;IAClB,IAAI,EAAE,UAAU,KAAK,OAAO,KAAK,QAAQ,KAAK;IAC9C,IAAI,EAAE,UAAU,KAAK,MAAM,KAAK,SAAS,KAAK;GAChD,CAAC;EACH;EAEA,MAAM,2BAA2B,UAAU,WAAW,IAAI;EAE1D,MAAM,0BAA0B,UAAU,KAAK;EAE/C,MAAM,mBAAmB,YAAqB;GAC5C,IAAI,CAAC,UAAU;GACf,IAAI,aAAa,SAAS;IAExB,SAAS,KAAK;IACd,SAAS,WAAW;IACpB;GACF;GACA,IAAI,SAAS,SAAS,MAAM;QACvB,SAAS,KAAK;EACrB;EAEA,MAAM,2BAA2B;GAE/B,gBAAgB,IAAI;EACtB;EAEA,gBAAgB;GACd,MAAM,SAAS,UAAU;GACzB,IAAI,CAAC,QAAQ;GAEb,WAAW,kBAAkB,QAAQ;IAAE,MAAM,MAAM;IAAM,QAAQ,MAAM;GAAO,CAAC;GAE/E,cAAc,OAAO,aAAa,kCAAkC,KAAK;GACzE,aAAa,mBAAmB,UAAU,kBAAkB;GAE5D,IAAI,OAAO,yBAAyB,aAAa;IAE/C,WAAW,IAAI,sBAAsB,YAAY;KAC/C,gBAAgB,QAAQ,EAAE,EAAE,kBAAkB,IAAI;IACpD,CAAC;IACD,SAAS,QAAQ,MAAM;GACzB,OACE,gBAAgB,IAAI;EAExB,CAAC;EAED,YACQ,MAAM,SACX,WAAW,UAAU,UAAU,MAAM,CACxC;EAEA,YACQ,MAAM,OACX,SAAS,UAAU,OAAO,IAAI,CACjC;EAEA,MAAM,SAAS,aAA4B,UAAU,QAAQ,QAAQ;EAErE,SAA2B,EAAE,MAAM,CAAC;EAEpC,sBAAsB;GACpB,UAAU,WAAW;GACrB,aAAa,sBAAsB,UAAU,kBAAkB;GAC/D,UAAU,KAAK;GACf,WAAW;EACb,CAAC;;GAxGC,OAAA,UAAA,GAAA,mBASE,UAAA;IARI,SAAA;IAAJ,KAAI;IACH,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,CAAC;IACZ,OAAK,eAAA;KAAA,OAAA,GAAc,QAAA,KAAI;KAAA,QAAA,GAAiB,QAAA,KAAI;IAAA,CAAA;IAC7C,eAAY;IACX,eAAa;IACb,gBAAc;IACd,eAAa;IACb,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,OAAA"}