@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.
- package/dist/chat/index.d.ts +3 -3
- package/dist/chat/transports/index.d.ts +2 -2
- package/dist/chat/transports/openai.d.ts +16 -12
- package/dist/chat/transports/openai.js +56 -15
- package/dist/chat/transports/openai.js.map +1 -1
- package/dist/chat/types.d.ts +41 -2
- package/dist/chat/use-chat.d.ts +26 -1
- package/dist/chat/use-chat.js +132 -13
- package/dist/chat/use-chat.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js +222 -41
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/ask-question.js +2 -2
- package/dist/components/ai-chat/ask-question.js.map +1 -1
- package/dist/components/ai-chat/chat-input.js +81 -44
- 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/message-item.js +101 -23
- package/dist/components/ai-chat/message-item.js.map +1 -1
- package/dist/components/ai-chat/message-list.js +217 -32
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/model-picker.js +140 -0
- package/dist/components/ai-chat/model-picker.js.map +1 -0
- package/dist/components/ai-chat/queue-list.js +70 -0
- package/dist/components/ai-chat/queue-list.js.map +1 -0
- package/dist/components/ai-chat/side-panel.js +96 -0
- package/dist/components/ai-chat/side-panel.js.map +1 -0
- package/dist/components/ai-chat/style.d.ts +6 -0
- package/dist/components/ai-chat/style.js +6 -0
- package/dist/components/ai-chat/style2.css +552 -41
- package/dist/components/ai-chat/tool-call.js +61 -27
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/components/ai-orb/ai-orb.js +94 -0
- package/dist/components/ai-orb/ai-orb.js.map +1 -0
- package/dist/components/ai-orb/orb-renderer.js +411 -0
- package/dist/components/ai-orb/orb-renderer.js.map +1 -0
- package/dist/components/ai-orb/style.d.ts +1 -0
- package/dist/components/ai-orb/style.js +1 -0
- package/dist/components/ai-orb/style2.css +23 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +2 -2
- package/dist/providers/types.d.ts +47 -0
- package/dist/style.js +1 -0
- package/dist/tools/ask-question/ask-question.d.ts +2 -17
- package/dist/tools/ask-question/index.d.ts +2 -2
- package/dist/tools/index.d.ts +3 -2
- package/dist/tools/index.js +10 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types/ai-chat.d.ts +35 -7
- package/dist/types/ai-orb.d.ts +27 -0
- package/dist/types/index.d.ts +2 -1
- package/package.json +8 -8
- package/src/chat/__test__/use-chat.test.ts +295 -9
- package/src/chat/transports/__test__/openai.test.ts +317 -0
- package/src/chat/transports/openai.ts +88 -26
- package/src/chat/types.ts +41 -1
- package/src/chat/use-chat.ts +177 -14
- package/src/components/ai-chat/__test__/ai-chat.test.ts +690 -12
- package/src/components/ai-chat/ai-chat.vue +241 -31
- package/src/components/ai-chat/chat-input.vue +78 -31
- package/src/components/ai-chat/di.ts +2 -0
- package/src/components/ai-chat/message-item.vue +98 -11
- package/src/components/ai-chat/message-list.vue +261 -21
- package/src/components/ai-chat/model-picker.vue +177 -0
- package/src/components/ai-chat/queue-list.vue +63 -0
- package/src/components/ai-chat/side-panel.vue +76 -0
- package/src/components/ai-chat/style.scss +621 -43
- package/src/components/ai-chat/style.ts +6 -0
- package/src/components/ai-chat/tool-call.vue +86 -42
- package/src/components/ai-orb/__test__/ai-orb.test.ts +251 -0
- package/src/components/ai-orb/ai-orb.vue +107 -0
- package/src/components/ai-orb/index.ts +1 -0
- package/src/components/ai-orb/orb-renderer.ts +505 -0
- package/src/components/ai-orb/style.scss +10 -0
- package/src/components/ai-orb/style.ts +1 -0
- package/src/components/index.ts +1 -0
- package/src/index.ts +8 -1
- package/src/providers/index.ts +1 -0
- package/src/providers/types.ts +46 -0
- package/src/style.ts +1 -0
- package/src/tools/ask-question/__test__/ask-question.test.ts +4 -4
- package/src/tools/index.ts +14 -1
- package/src/types/ai-chat.ts +35 -6
- package/src/types/ai-orb.ts +27 -0
- package/src/types/index.ts +1 -0
- package/dist/components/ai-chat/ai-chat.vue.d.ts +0 -36
- package/dist/components/ai-chat/index.d.ts +0 -2
- 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 {
|
|
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 {
|
|
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
|
-
|
|
22
|
+
/** UCollapseItem 的 change 仅在用户点击头部时触发(程序化 v-model 变更不触发) */
|
|
23
|
+
const markUserToggled = () => {
|
|
24
24
|
userToggled = true;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
const
|
|
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-
|
|
38
|
+
class: "is-active"
|
|
37
39
|
},
|
|
38
40
|
running: {
|
|
39
41
|
icon: Loading,
|
|
40
|
-
class: "is-
|
|
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(),
|
|
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[
|
|
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
|
-
|
|
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[
|
|
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[
|
|
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
|
-
}
|
|
159
|
+
}, 8, ["class"])
|
|
138
160
|
]),
|
|
139
|
-
default: withCtx(() => [
|
|
161
|
+
default: withCtx(() => [isPanelTool.value ? (openBlock(), createElementBlock("div", {
|
|
140
162
|
key: 0,
|
|
141
|
-
"tool-call"
|
|
142
|
-
},
|
|
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, [
|
|
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"}
|