@veltra/ai 1.4.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 (71) hide show
  1. package/dist/chat/index.d.ts +5 -0
  2. package/dist/chat/transports/index.d.ts +2 -0
  3. package/dist/chat/transports/openai.d.ts +23 -0
  4. package/dist/chat/transports/openai.js +176 -0
  5. package/dist/chat/transports/openai.js.map +1 -0
  6. package/dist/chat/types.d.ts +125 -0
  7. package/dist/chat/use-chat.d.ts +25 -0
  8. package/dist/chat/use-chat.js +214 -0
  9. package/dist/chat/use-chat.js.map +1 -0
  10. package/dist/components/ai-chat/ai-chat.js +88 -0
  11. package/dist/components/ai-chat/ai-chat.js.map +1 -0
  12. package/dist/components/ai-chat/ai-chat.vue.d.ts +36 -0
  13. package/dist/components/ai-chat/ask-question.js +187 -0
  14. package/dist/components/ai-chat/ask-question.js.map +1 -0
  15. package/dist/components/ai-chat/chat-input.js +143 -0
  16. package/dist/components/ai-chat/chat-input.js.map +1 -0
  17. package/dist/components/ai-chat/di.js +6 -0
  18. package/dist/components/ai-chat/di.js.map +1 -0
  19. package/dist/components/ai-chat/index.d.ts +2 -0
  20. package/dist/components/ai-chat/message-item.js +120 -0
  21. package/dist/components/ai-chat/message-item.js.map +1 -0
  22. package/dist/components/ai-chat/message-list.js +75 -0
  23. package/dist/components/ai-chat/message-list.js.map +1 -0
  24. package/dist/components/ai-chat/style.d.ts +5 -0
  25. package/dist/components/ai-chat/style.js +6 -0
  26. package/dist/components/ai-chat/style2.css +416 -0
  27. package/dist/components/ai-chat/tool-call.js +151 -0
  28. package/dist/components/ai-chat/tool-call.js.map +1 -0
  29. package/dist/components/index.d.ts +3 -0
  30. package/dist/index.d.ts +11 -0
  31. package/dist/index.js +5 -0
  32. package/dist/style.d.ts +1 -0
  33. package/dist/style.js +1 -0
  34. package/dist/tools/ask-question/ask-question.d.ts +45 -0
  35. package/dist/tools/ask-question/ask-question.js +65 -0
  36. package/dist/tools/ask-question/ask-question.js.map +1 -0
  37. package/dist/tools/ask-question/deferred.js +22 -0
  38. package/dist/tools/ask-question/deferred.js.map +1 -0
  39. package/dist/tools/ask-question/index.d.ts +2 -0
  40. package/dist/tools/index.d.ts +3 -0
  41. package/dist/types/ai-chat.d.ts +49 -0
  42. package/dist/types/index.d.ts +2 -0
  43. package/package.json +40 -0
  44. package/src/chat/__test__/use-chat.test.ts +246 -0
  45. package/src/chat/index.ts +3 -0
  46. package/src/chat/transports/index.ts +1 -0
  47. package/src/chat/transports/openai.ts +237 -0
  48. package/src/chat/types.ts +136 -0
  49. package/src/chat/use-chat.ts +245 -0
  50. package/src/components/ai-chat/__test__/ai-chat.test.ts +316 -0
  51. package/src/components/ai-chat/ai-chat.vue +67 -0
  52. package/src/components/ai-chat/ask-question.vue +232 -0
  53. package/src/components/ai-chat/chat-input.vue +127 -0
  54. package/src/components/ai-chat/di.ts +15 -0
  55. package/src/components/ai-chat/index.ts +1 -0
  56. package/src/components/ai-chat/message-item.vue +120 -0
  57. package/src/components/ai-chat/message-list.vue +81 -0
  58. package/src/components/ai-chat/style.scss +439 -0
  59. package/src/components/ai-chat/style.ts +6 -0
  60. package/src/components/ai-chat/tool-call.vue +147 -0
  61. package/src/components/index.ts +1 -0
  62. package/src/env.d.ts +12 -0
  63. package/src/index.ts +4 -0
  64. package/src/style.ts +1 -0
  65. package/src/tools/ask-question/__test__/ask-question.test.ts +198 -0
  66. package/src/tools/ask-question/ask-question.ts +114 -0
  67. package/src/tools/ask-question/deferred.ts +27 -0
  68. package/src/tools/ask-question/index.ts +1 -0
  69. package/src/tools/index.ts +1 -0
  70. package/src/types/ai-chat.ts +56 -0
  71. package/src/types/index.ts +1 -0
@@ -0,0 +1,88 @@
1
+ import { useChat } from "../../chat/use-chat.js";
2
+ import { AiChatDIKey } from "./di.js";
3
+ import _sfc_main$1 from "./chat-input.js";
4
+ import _sfc_main$2 from "./message-list.js";
5
+ import { computed, createElementBlock, createSlots, createVNode, defineComponent, normalizeClass, openBlock, provide, renderSlot, unref, useSlots, withCtx } from "vue";
6
+ import { bem } from "@veltra/utils";
7
+ //#region src/components/ai-chat/ai-chat.vue
8
+ const _sfc_main = /*@__PURE__*/ defineComponent({
9
+ name: "UAiChat",
10
+ __name: "ai-chat",
11
+ props: {
12
+ transport: { type: Function },
13
+ tools: {},
14
+ systemPrompt: {},
15
+ messages: {},
16
+ welcome: {},
17
+ placeholder: {},
18
+ accept: {},
19
+ maxAttachmentSize: {},
20
+ rendererProps: {}
21
+ },
22
+ emits: [
23
+ "update:messages",
24
+ "send",
25
+ "finish",
26
+ "error",
27
+ "tool-call"
28
+ ],
29
+ setup(__props, { expose: __expose, emit: __emit }) {
30
+ const props = __props;
31
+ const emit = __emit;
32
+ const cls = bem("ai-chat");
33
+ provide(AiChatDIKey, {
34
+ cls,
35
+ slots: useSlots(),
36
+ tools: computed(() => {
37
+ return Object.fromEntries((props.tools ?? []).map((tool) => [tool.name, tool]));
38
+ })
39
+ });
40
+ const { messages, running, send, abort, regenerate, clear, respondToolCall } = useChat({
41
+ props,
42
+ emit
43
+ });
44
+ __expose({
45
+ send,
46
+ abort,
47
+ regenerate,
48
+ clear
49
+ });
50
+ return (_ctx, _cache) => {
51
+ return openBlock(), createElementBlock("div", { class: normalizeClass(unref(cls).b) }, [createVNode(_sfc_main$2, {
52
+ messages: unref(messages),
53
+ welcome: __props.welcome,
54
+ "renderer-props": __props.rendererProps,
55
+ onRespond: unref(respondToolCall),
56
+ onRegenerate: unref(regenerate)
57
+ }, createSlots({ _: 2 }, [_ctx.$slots.welcome ? {
58
+ name: "welcome",
59
+ fn: withCtx(() => [renderSlot(_ctx.$slots, "welcome")]),
60
+ key: "0"
61
+ } : void 0]), 1032, [
62
+ "messages",
63
+ "welcome",
64
+ "renderer-props",
65
+ "onRespond",
66
+ "onRegenerate"
67
+ ]), createVNode(_sfc_main$1, {
68
+ running: unref(running),
69
+ placeholder: __props.placeholder,
70
+ accept: __props.accept,
71
+ "max-attachment-size": __props.maxAttachmentSize,
72
+ onSend: unref(send),
73
+ onAbort: unref(abort)
74
+ }, null, 8, [
75
+ "running",
76
+ "placeholder",
77
+ "accept",
78
+ "max-attachment-size",
79
+ "onSend",
80
+ "onAbort"
81
+ ])], 2);
82
+ };
83
+ }
84
+ });
85
+ //#endregion
86
+ export { _sfc_main as default };
87
+
88
+ //# sourceMappingURL=ai-chat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-chat.js","names":["$slots"],"sources":["../../../src/components/ai-chat/ai-chat.vue"],"sourcesContent":["<template>\n <div :class=\"cls.b\">\n <MessageList\n :messages=\"messages\"\n :welcome=\"welcome\"\n :renderer-props=\"rendererProps\"\n @respond=\"respondToolCall\"\n @regenerate=\"regenerate\"\n >\n <template v-if=\"$slots.welcome\" #welcome>\n <slot name=\"welcome\" />\n </template>\n </MessageList>\n\n <ChatInput\n :running=\"running\"\n :placeholder=\"placeholder\"\n :accept=\"accept\"\n :max-attachment-size=\"maxAttachmentSize\"\n @send=\"send\"\n @abort=\"abort\"\n />\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { bem } from '@veltra/utils'\nimport { computed, provide, useSlots } from 'vue'\n\nimport type { ChatTool, ChatToolCall } from '../../chat/types'\nimport { useChat } from '../../chat/use-chat'\nimport type { _AiChatExposed, AiChatEmits, AiChatProps } from '../../types'\nimport ChatInput from './chat-input.vue'\nimport { AiChatDIKey } from './di'\nimport MessageList from './message-list.vue'\n\ndefineOptions({ name: 'UAiChat' })\n\nconst props = defineProps<AiChatProps>()\n\nconst emit = defineEmits<AiChatEmits>()\n\ndefineSlots<{\n /** 空状态欢迎区插槽 */\n welcome(): any\n /** 按工具名自定义工具结果展示,如 tool-getWeather */\n [name: `tool-${string}`]: (scope: { toolCall: ChatToolCall }) => any\n}>()\n\nconst cls = bem('ai-chat')\n\nconst slots = useSlots()\n\n/** 按 name 索引的工具定义,供工具卡片解析 icon/label/render/autoCollapse */\nconst toolMap = computed<Record<string, ChatTool | undefined>>(() => {\n return Object.fromEntries((props.tools ?? []).map((tool) => [tool.name, tool]))\n})\n\nprovide(AiChatDIKey, { cls, slots, tools: toolMap })\n\nconst { messages, running, send, abort, regenerate, clear, respondToolCall } = useChat({\n props,\n emit\n})\n\ndefineExpose<_AiChatExposed>({ send, abort, regenerate, clear })\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsCA,MAAM,QAAQ;EAEd,MAAM,OAAO;EASb,MAAM,MAAM,IAAI,SAAS;EASzB,QAAQ,aAAa;GAAE;GAAK,OAPd,SAOc;GAAO,OAJnB,eAAqD;IACnE,OAAO,OAAO,aAAa,MAAM,SAAS,CAAC,EAAA,CAAG,KAAK,SAAS,CAAC,KAAK,MAAM,IAAI,CAAC,CAAC;GAChF,CAE0C;EAAQ,CAAC;EAEnD,MAAM,EAAE,UAAU,SAAS,MAAM,OAAO,YAAY,OAAO,oBAAoB,QAAQ;GACrF;GACA;EACF,CAAC;EAED,SAA6B;GAAE;GAAM;GAAO;GAAY;EAAM,CAAC;;uBAhE7D,mBAqBM,OAAA,EArBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,CAAC,EAAA,GAAA,CAChB,YAUc,aAAA;IATX,UAAU,MAAA,QAAA;IACV,SAAS,QAAA;IACT,kBAAgB,QAAA;IAChB,WAAS,MAAA,eAAA;IACT,cAAY,MAAA,UAAA;6BAEGA,KAAAA,OAAO,UAAA;UAAU;sBACR,CAAvB,WAAuB,KAAA,QAAA,SAAA,CAAA,CAAA;;;;;;;;OAI3B,YAOE,aAAA;IANC,SAAS,MAAA,OAAA;IACT,aAAa,QAAA;IACb,QAAQ,QAAA;IACR,uBAAqB,QAAA;IACrB,QAAM,MAAA,IAAA;IACN,SAAO,MAAA,KAAA"}
@@ -0,0 +1,36 @@
1
+ import { ChatMessage, ChatToolCall } from "../../chat/types.js";
2
+ import { AiChatProps, _AiChatExposed } from "../../types/ai-chat.js";
3
+ import "../../types/index.js";
4
+ import "../../index.js";
5
+ //#region src/components/ai-chat/ai-chat.vue.d.ts
6
+ type __VLS_Slots = {
7
+ /** 空状态欢迎区插槽 */
8
+ welcome(): any;
9
+ /** 按工具名自定义工具结果展示,如 tool-getWeather */
10
+ [name: `tool-${string}`]: (scope: {
11
+ toolCall: ChatToolCall;
12
+ }) => any;
13
+ };
14
+ declare const __VLS_base: import("vue").DefineComponent<AiChatProps, _AiChatExposed, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
+ "update:messages": (messages: ChatMessage[]) => any;
16
+ send: (message: ChatMessage) => any;
17
+ finish: (message: ChatMessage) => any;
18
+ error: (error: Error) => any;
19
+ "tool-call": (toolCall: ChatToolCall) => any;
20
+ }, string, import("vue").PublicProps, Readonly<AiChatProps> & Readonly<{
21
+ "onUpdate:messages"?: ((messages: ChatMessage[]) => any) | undefined;
22
+ onSend?: ((message: ChatMessage) => any) | undefined;
23
+ onFinish?: ((message: ChatMessage) => any) | undefined;
24
+ onError?: ((error: Error) => any) | undefined;
25
+ "onTool-call"?: ((toolCall: ChatToolCall) => any) | undefined;
26
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
27
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
28
+ declare const _default: typeof __VLS_export;
29
+ type __VLS_WithSlots<T, S> = T & {
30
+ new (): {
31
+ $slots: S;
32
+ };
33
+ };
34
+ //#endregion
35
+ export { _default as default };
36
+ //# sourceMappingURL=ai-chat.vue.d.ts.map
@@ -0,0 +1,187 @@
1
+ import { AiChatDIKey } from "./di.js";
2
+ import { resolveAskQuestion } from "../../tools/ask-question/deferred.js";
3
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, ref, renderList, toDisplayString, unref, watch, withCtx } from "vue";
4
+ import { bem } from "@veltra/utils";
5
+ import { UButton, UInput } from "@veltra/desktop";
6
+ //#region src/components/ai-chat/ask-question.vue
7
+ const _hoisted_1 = ["onClick"];
8
+ const _hoisted_2 = ["aria-checked", "onClick"];
9
+ const _sfc_main = /*@__PURE__*/ defineComponent({
10
+ name: "UAiChatAskQuestion",
11
+ __name: "ask-question",
12
+ props: { toolCall: {} },
13
+ setup(__props) {
14
+ const props = __props;
15
+ const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
16
+ /** 模型输出的提问参数 */
17
+ const questions = computed(() => {
18
+ try {
19
+ const args = JSON.parse(props.toolCall.arguments || "{}");
20
+ return Array.isArray(args.questions) ? args.questions : [];
21
+ } catch {
22
+ return [];
23
+ }
24
+ });
25
+ /** 作答中:execute 挂起等待用户提交 */
26
+ const answering = computed(() => {
27
+ return props.toolCall.result == null && (props.toolCall.status === "running" || props.toolCall.status === "pending");
28
+ });
29
+ /** 当前题索引 */
30
+ const current = ref(0);
31
+ /** 各题选中的预设选项 */
32
+ const selections = ref([]);
33
+ /** 各题的自定义输入 */
34
+ const customs = ref([]);
35
+ watch(questions, (list) => {
36
+ if (props.toolCall.result != null) return;
37
+ selections.value = list.map(() => null);
38
+ customs.value = list.map(() => "");
39
+ }, { immediate: true });
40
+ const currentQuestion = computed(() => questions.value[current.value]);
41
+ /** 某题是否已作答:自定义输入非空优先,否则看选中项 */
42
+ const isAnswered = (index) => {
43
+ return Boolean(customs.value[index]?.trim() || selections.value[index]);
44
+ };
45
+ const allAnswered = computed(() => {
46
+ return questions.value.length > 0 && questions.value.every((_, i) => isAnswered(i));
47
+ });
48
+ const goTo = (index) => {
49
+ if (index < 0 || index >= questions.value.length) return;
50
+ current.value = index;
51
+ };
52
+ /** 点选选项:再次点击取消选择;选中时清空自定义输入(两者互斥) */
53
+ const pickOption = (option) => {
54
+ const i = current.value;
55
+ const selected = selections.value[i] === option ? null : option;
56
+ selections.value[i] = selected;
57
+ if (selected) customs.value[i] = "";
58
+ };
59
+ /** 自定义输入:有内容时清除已选选项(两者互斥) */
60
+ const onCustomInput = (value) => {
61
+ customs.value[current.value] = value;
62
+ if (value) selections.value[current.value] = null;
63
+ };
64
+ const handleSubmit = () => {
65
+ const firstUnanswered = questions.value.findIndex((_, i) => !isAnswered(i));
66
+ if (firstUnanswered >= 0) {
67
+ current.value = firstUnanswered;
68
+ return;
69
+ }
70
+ const answers = questions.value.map((item, i) => ({
71
+ question: item.question,
72
+ answer: customs.value[i]?.trim() || selections.value[i] || ""
73
+ }));
74
+ resolveAskQuestion(props.toolCall.id, { answers });
75
+ };
76
+ /** 提交后的问答摘要(含历史恢复:从序列化结果解析) */
77
+ const doneAnswers = computed(() => {
78
+ if (props.toolCall.status !== "success" || !props.toolCall.result) return [];
79
+ let result;
80
+ try {
81
+ result = JSON.parse(props.toolCall.result);
82
+ } catch {
83
+ return [];
84
+ }
85
+ if (!Array.isArray(result?.answers)) return [];
86
+ return result.answers.map((item) => ({
87
+ ...item,
88
+ /** 命中预设选项的回答渲染为 chip */
89
+ isOption: questions.value.some((q) => q.question === item.question && q.options?.includes(item.answer))
90
+ }));
91
+ });
92
+ const statusText = computed(() => {
93
+ if (props.toolCall.status !== "error") return "";
94
+ const err = props.toolCall.error ?? "";
95
+ if (err.includes("Abort") || err.includes("取消")) return "已取消";
96
+ return err || "提问失败";
97
+ });
98
+ return (_ctx, _cache) => {
99
+ return openBlock(), createElementBlock("div", { class: normalizeClass(unref(cls).e("ask-question")) }, [createCommentVNode(" 作答中:分页向导 "), answering.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
100
+ questions.value.length > 1 ? (openBlock(), createElementBlock("div", {
101
+ key: 0,
102
+ class: normalizeClass(unref(cls).e("ask-question-progress"))
103
+ }, [createElementVNode("span", { class: normalizeClass(unref(cls).e("ask-question-count")) }, toDisplayString(current.value + 1) + " / " + toDisplayString(questions.value.length), 3), createElementVNode("span", { class: normalizeClass(unref(cls).e("ask-question-dots")) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(questions.value, (_, i) => {
104
+ return openBlock(), createElementBlock("span", {
105
+ key: i,
106
+ class: normalizeClass([
107
+ unref(cls).e("ask-question-dot"),
108
+ unref(bem).is("current", i === current.value),
109
+ unref(bem).is("answered", isAnswered(i))
110
+ ]),
111
+ onClick: ($event) => goTo(i)
112
+ }, null, 10, _hoisted_1);
113
+ }), 128))], 2)], 2)) : createCommentVNode("v-if", true),
114
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("ask-question-q")) }, [questions.value.length > 1 ? (openBlock(), createElementBlock("span", {
115
+ key: 0,
116
+ class: normalizeClass(unref(cls).e("ask-question-no")),
117
+ "aria-hidden": "true"
118
+ }, toDisplayString(current.value + 1), 3)) : createCommentVNode("v-if", true), createTextVNode(" " + toDisplayString(currentQuestion.value?.question), 1)], 2),
119
+ currentQuestion.value?.options?.length ? (openBlock(), createElementBlock("div", {
120
+ key: 1,
121
+ class: normalizeClass(unref(cls).e("ask-question-options")),
122
+ role: "radiogroup"
123
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(currentQuestion.value.options, (option) => {
124
+ return openBlock(), createElementBlock("button", {
125
+ key: option,
126
+ type: "button",
127
+ role: "radio",
128
+ "aria-checked": selections.value[current.value] === option,
129
+ class: normalizeClass([unref(cls).e("ask-question-opt"), unref(bem).is("selected", selections.value[current.value] === option)]),
130
+ onClick: ($event) => pickOption(option)
131
+ }, [createElementVNode("span", {
132
+ class: normalizeClass([unref(cls).e("ask-question-glyph"), unref(bem).is("selected", selections.value[current.value] === option)]),
133
+ "aria-hidden": "true"
134
+ }, null, 2), createElementVNode("span", { class: normalizeClass(unref(cls).e("ask-question-opt-text")) }, toDisplayString(option), 3)], 10, _hoisted_2);
135
+ }), 128))], 2)) : createCommentVNode("v-if", true),
136
+ createVNode(unref(UInput), {
137
+ "model-value": customs.value[current.value],
138
+ placeholder: currentQuestion.value?.placeholder ?? "自定义回答…",
139
+ "onUpdate:modelValue": onCustomInput
140
+ }, null, 8, ["model-value", "placeholder"]),
141
+ createElementVNode("footer", { class: normalizeClass(unref(cls).e("ask-question-actions")) }, [questions.value.length > 1 ? (openBlock(), createBlock(unref(UButton), {
142
+ key: 0,
143
+ size: "small",
144
+ text: "",
145
+ disabled: current.value === 0,
146
+ onClick: _cache[0] || (_cache[0] = ($event) => goTo(current.value - 1))
147
+ }, {
148
+ default: withCtx(() => [..._cache[2] || (_cache[2] = [createTextVNode(" 上一个 ", -1)])]),
149
+ _: 1
150
+ }, 8, ["disabled"])) : createCommentVNode("v-if", true), current.value < questions.value.length - 1 ? (openBlock(), createBlock(unref(UButton), {
151
+ key: 1,
152
+ size: "small",
153
+ type: "primary",
154
+ onClick: _cache[1] || (_cache[1] = ($event) => goTo(current.value + 1))
155
+ }, {
156
+ default: withCtx(() => [..._cache[3] || (_cache[3] = [createTextVNode(" 下一个 ", -1)])]),
157
+ _: 1
158
+ })) : (openBlock(), createBlock(unref(UButton), {
159
+ key: 2,
160
+ size: "small",
161
+ type: "primary",
162
+ disabled: !allAnswered.value,
163
+ onClick: handleSubmit
164
+ }, {
165
+ default: withCtx(() => [..._cache[4] || (_cache[4] = [createTextVNode(" 提交 ", -1)])]),
166
+ _: 1
167
+ }, 8, ["disabled"]))], 2)
168
+ ], 64)) : __props.toolCall.status === "success" ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createCommentVNode(" 提交后:问答摘要 "), (openBlock(true), createElementBlock(Fragment, null, renderList(doneAnswers.value, (item, i) => {
169
+ return openBlock(), createElementBlock("div", {
170
+ key: i,
171
+ class: normalizeClass(unref(cls).e("ask-question-result"))
172
+ }, [createElementVNode("div", { class: normalizeClass(unref(cls).e("ask-question-result-q")) }, [doneAnswers.value.length > 1 ? (openBlock(), createElementBlock("span", {
173
+ key: 0,
174
+ class: normalizeClass(unref(cls).e("ask-question-result-no")),
175
+ "aria-hidden": "true"
176
+ }, toDisplayString(i + 1), 3)) : createCommentVNode("v-if", true), createTextVNode(" " + toDisplayString(item.question), 1)], 2), createElementVNode("div", { class: normalizeClass(unref(cls).e("ask-question-result-a")) }, [item.isOption ? (openBlock(), createElementBlock("span", {
177
+ key: 0,
178
+ class: normalizeClass(unref(cls).e("ask-question-chip"))
179
+ }, toDisplayString(item.answer), 3)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(item.answer), 1)], 64))], 2)], 2);
180
+ }), 128))], 64)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [createCommentVNode(" 异常 / 取消 "), createElementVNode("div", { class: normalizeClass(unref(cls).e("ask-question-status")) }, toDisplayString(statusText.value), 3)], 2112))], 2);
181
+ };
182
+ }
183
+ });
184
+ //#endregion
185
+ export { _sfc_main as default };
186
+
187
+ //# sourceMappingURL=ask-question.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ask-question.js","names":[],"sources":["../../../src/components/ai-chat/ask-question.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('ask-question')\">\n <!-- 作答中:分页向导 -->\n <template v-if=\"answering\">\n <div v-if=\"questions.length > 1\" :class=\"cls.e('ask-question-progress')\">\n <span :class=\"cls.e('ask-question-count')\">{{ current + 1 }} / {{ questions.length }}</span>\n <span :class=\"cls.e('ask-question-dots')\">\n <span\n v-for=\"(_, i) in questions\"\n :key=\"i\"\n :class=\"[\n cls.e('ask-question-dot'),\n bem.is('current', i === current),\n bem.is('answered', isAnswered(i))\n ]\"\n @click=\"goTo(i)\"\n />\n </span>\n </div>\n\n <div :class=\"cls.e('ask-question-q')\">\n <span v-if=\"questions.length > 1\" :class=\"cls.e('ask-question-no')\" aria-hidden=\"true\">\n {{ current + 1 }}\n </span>\n {{ currentQuestion?.question }}\n </div>\n\n <div\n v-if=\"currentQuestion?.options?.length\"\n :class=\"cls.e('ask-question-options')\"\n role=\"radiogroup\"\n >\n <button\n v-for=\"option in currentQuestion.options\"\n :key=\"option\"\n type=\"button\"\n role=\"radio\"\n :aria-checked=\"selections[current] === option\"\n :class=\"[cls.e('ask-question-opt'), bem.is('selected', selections[current] === option)]\"\n @click=\"pickOption(option)\"\n >\n <span\n :class=\"[\n cls.e('ask-question-glyph'),\n bem.is('selected', selections[current] === option)\n ]\"\n aria-hidden=\"true\"\n />\n <span :class=\"cls.e('ask-question-opt-text')\">{{ option }}</span>\n </button>\n </div>\n\n <UInput\n :model-value=\"customs[current]\"\n :placeholder=\"currentQuestion?.placeholder ?? '自定义回答…'\"\n @update:model-value=\"onCustomInput\"\n />\n\n <footer :class=\"cls.e('ask-question-actions')\">\n <UButton\n v-if=\"questions.length > 1\"\n size=\"small\"\n text\n :disabled=\"current === 0\"\n @click=\"goTo(current - 1)\"\n >\n 上一个\n </UButton>\n <UButton\n v-if=\"current < questions.length - 1\"\n size=\"small\"\n type=\"primary\"\n @click=\"goTo(current + 1)\"\n >\n 下一个\n </UButton>\n <UButton v-else size=\"small\" type=\"primary\" :disabled=\"!allAnswered\" @click=\"handleSubmit\">\n 提交\n </UButton>\n </footer>\n </template>\n\n <!-- 提交后:问答摘要 -->\n <template v-else-if=\"toolCall.status === 'success'\">\n <div v-for=\"(item, i) in doneAnswers\" :key=\"i\" :class=\"cls.e('ask-question-result')\">\n <div :class=\"cls.e('ask-question-result-q')\">\n <span\n v-if=\"doneAnswers.length > 1\"\n :class=\"cls.e('ask-question-result-no')\"\n aria-hidden=\"true\"\n >\n {{ i + 1 }}\n </span>\n {{ item.question }}\n </div>\n <div :class=\"cls.e('ask-question-result-a')\">\n <span v-if=\"item.isOption\" :class=\"cls.e('ask-question-chip')\">{{ item.answer }}</span>\n <template v-else>{{ item.answer }}</template>\n </div>\n </div>\n </template>\n\n <!-- 异常 / 取消 -->\n <div v-else :class=\"cls.e('ask-question-status')\">{{ statusText }}</div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UInput } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, watch } from 'vue'\n\nimport type { ChatToolCall } from '../../chat/types'\nimport type {\n AskQuestionArgs,\n AskQuestionItem,\n AskQuestionResult\n} from '../../tools/ask-question/ask-question'\nimport { resolveAskQuestion } from '../../tools/ask-question/deferred'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatAskQuestion' })\n\nconst props = defineProps<{ toolCall: ChatToolCall }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 模型输出的提问参数 */\nconst questions = computed<AskQuestionItem[]>(() => {\n try {\n const args = JSON.parse(props.toolCall.arguments || '{}') as Partial<AskQuestionArgs>\n return Array.isArray(args.questions) ? args.questions : []\n } catch {\n return []\n }\n})\n\n/** 作答中:execute 挂起等待用户提交 */\nconst answering = computed(() => {\n return (\n props.toolCall.result == null &&\n (props.toolCall.status === 'running' || props.toolCall.status === 'pending')\n )\n})\n\n/** 当前题索引 */\nconst current = ref(0)\n/** 各题选中的预设选项 */\nconst selections = ref<(string | null)[]>([])\n/** 各题的自定义输入 */\nconst customs = ref<string[]>([])\n\nwatch(\n questions,\n (list) => {\n if (props.toolCall.result != null) return\n selections.value = list.map(() => null)\n customs.value = list.map(() => '')\n },\n { immediate: true }\n)\n\nconst currentQuestion = computed(() => questions.value[current.value])\n\n/** 某题是否已作答:自定义输入非空优先,否则看选中项 */\nconst isAnswered = (index: number) => {\n return Boolean(customs.value[index]?.trim() || selections.value[index])\n}\n\nconst allAnswered = computed(() => {\n return questions.value.length > 0 && questions.value.every((_, i) => isAnswered(i))\n})\n\nconst goTo = (index: number) => {\n if (index < 0 || index >= questions.value.length) return\n current.value = index\n}\n\n/** 点选选项:再次点击取消选择;选中时清空自定义输入(两者互斥) */\nconst pickOption = (option: string) => {\n const i = current.value\n const selected = selections.value[i] === option ? null : option\n selections.value[i] = selected\n if (selected) customs.value[i] = ''\n}\n\n/** 自定义输入:有内容时清除已选选项(两者互斥) */\nconst onCustomInput = (value: string) => {\n customs.value[current.value] = value\n if (value) selections.value[current.value] = null\n}\n\nconst handleSubmit = () => {\n const firstUnanswered = questions.value.findIndex((_, i) => !isAnswered(i))\n if (firstUnanswered >= 0) {\n current.value = firstUnanswered\n return\n }\n const answers = questions.value.map((item, i) => ({\n question: item.question,\n answer: customs.value[i]?.trim() || selections.value[i] || ''\n }))\n resolveAskQuestion(props.toolCall.id, { answers })\n}\n\n/** 提交后的问答摘要(含历史恢复:从序列化结果解析) */\nconst doneAnswers = computed(() => {\n if (props.toolCall.status !== 'success' || !props.toolCall.result) return []\n let result: AskQuestionResult\n try {\n result = JSON.parse(props.toolCall.result)\n } catch {\n return []\n }\n if (!Array.isArray(result?.answers)) return []\n return result.answers.map((item) => ({\n ...item,\n /** 命中预设选项的回答渲染为 chip */\n isOption: questions.value.some(\n (q) => q.question === item.question && q.options?.includes(item.answer)\n )\n }))\n})\n\nconst statusText = computed(() => {\n if (props.toolCall.status !== 'error') return ''\n const err = props.toolCall.error ?? ''\n if (err.includes('Abort') || err.includes('取消')) return '已取消'\n return err || '提问失败'\n})\n</script>\n"],"mappings":"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"}
@@ -0,0 +1,143 @@
1
+ import { AiChatDIKey } from "./di.js";
2
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, ref, renderList, shallowRef, unref, vModelText, withCtx, withDirectives, withKeys, withModifiers } from "vue";
3
+ import { bem } from "@veltra/utils";
4
+ import { UButton, UIcon } from "@veltra/desktop";
5
+ import { Attach, Close, Send } from "@veltra/icons/normal";
6
+ //#region src/components/ai-chat/chat-input.vue
7
+ const _hoisted_1 = ["src", "alt"];
8
+ const _hoisted_2 = ["placeholder", "onKeydown"];
9
+ const _hoisted_3 = ["accept"];
10
+ const _sfc_main = /*@__PURE__*/ defineComponent({
11
+ name: "UAiChatInput",
12
+ __name: "chat-input",
13
+ props: {
14
+ running: { type: Boolean },
15
+ placeholder: {},
16
+ accept: {},
17
+ maxAttachmentSize: {}
18
+ },
19
+ emits: ["send", "abort"],
20
+ setup(__props, { emit: __emit }) {
21
+ const props = __props;
22
+ const emit = __emit;
23
+ const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
24
+ const text = ref("");
25
+ const attachments = ref([]);
26
+ const textareaRef = shallowRef();
27
+ const fileRef = shallowRef();
28
+ const canSend = computed(() => {
29
+ return !props.running && (!!text.value.trim() || attachments.value.length > 0);
30
+ });
31
+ const autoResize = () => {
32
+ const el = textareaRef.value;
33
+ if (!el) return;
34
+ el.style.height = "auto";
35
+ el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
36
+ };
37
+ const handleSend = () => {
38
+ if (!canSend.value) return;
39
+ emit("send", text.value.trim(), attachments.value);
40
+ text.value = "";
41
+ attachments.value = [];
42
+ autoResize();
43
+ };
44
+ const handleFiles = (event) => {
45
+ const input = event.target;
46
+ const files = Array.from(input.files ?? []);
47
+ input.value = "";
48
+ const maxSize = props.maxAttachmentSize ?? 10 * 1024 * 1024;
49
+ for (const file of files) {
50
+ if (file.size > maxSize) {
51
+ console.warn(`[UAiChat] 附件 ${file.name} 超过大小限制,已忽略`);
52
+ continue;
53
+ }
54
+ const reader = new FileReader();
55
+ reader.onload = () => {
56
+ attachments.value.push({
57
+ name: file.name,
58
+ mimeType: file.type,
59
+ size: file.size,
60
+ dataUrl: String(reader.result)
61
+ });
62
+ };
63
+ reader.readAsDataURL(file);
64
+ }
65
+ };
66
+ return (_ctx, _cache) => {
67
+ return openBlock(), createElementBlock("div", { class: normalizeClass(unref(cls).e("input-area")) }, [
68
+ attachments.value.length ? (openBlock(), createElementBlock("div", {
69
+ key: 0,
70
+ class: normalizeClass(unref(cls).e("input-attachments"))
71
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(attachments.value, (att, index) => {
72
+ return openBlock(), createElementBlock("div", {
73
+ key: att.dataUrl,
74
+ class: normalizeClass(unref(cls).e("input-attachment"))
75
+ }, [createElementVNode("img", {
76
+ src: att.dataUrl,
77
+ alt: att.name
78
+ }, null, 8, _hoisted_1), createVNode(unref(UIcon), {
79
+ class: normalizeClass(unref(cls).e("input-attachment-remove")),
80
+ onClick: ($event) => attachments.value.splice(index, 1)
81
+ }, {
82
+ default: withCtx(() => [createVNode(unref(Close))]),
83
+ _: 1
84
+ }, 8, ["class", "onClick"])], 2);
85
+ }), 128))], 2)) : createCommentVNode("v-if", true),
86
+ withDirectives(createElementVNode("textarea", {
87
+ ref_key: "textareaRef",
88
+ ref: textareaRef,
89
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => text.value = $event),
90
+ class: normalizeClass(unref(cls).e("input")),
91
+ placeholder: __props.placeholder ?? "输入消息,Enter 发送,Shift + Enter 换行",
92
+ rows: "1",
93
+ onKeydown: withKeys(withModifiers(handleSend, ["exact", "prevent"]), ["enter"]),
94
+ onInput: autoResize
95
+ }, null, 42, _hoisted_2), [[vModelText, text.value]]),
96
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("input-toolbar")) }, [
97
+ createVNode(unref(UIcon), {
98
+ class: normalizeClass(unref(cls).e("input-attach")),
99
+ title: "添加图片",
100
+ onClick: _cache[1] || (_cache[1] = ($event) => fileRef.value?.click())
101
+ }, {
102
+ default: withCtx(() => [createVNode(unref(Attach))]),
103
+ _: 1
104
+ }, 8, ["class"]),
105
+ createElementVNode("input", {
106
+ ref_key: "fileRef",
107
+ ref: fileRef,
108
+ type: "file",
109
+ hidden: "",
110
+ multiple: "",
111
+ accept: __props.accept ?? "image/*",
112
+ onChange: handleFiles
113
+ }, null, 40, _hoisted_3),
114
+ __props.running ? (openBlock(), createBlock(unref(UButton), {
115
+ key: 0,
116
+ type: "danger",
117
+ circle: "",
118
+ onClick: _cache[2] || (_cache[2] = ($event) => emit("abort"))
119
+ }, {
120
+ default: withCtx(() => [createElementVNode("span", { class: normalizeClass(unref(cls).e("input-stop")) }, null, 2)]),
121
+ _: 1
122
+ })) : (openBlock(), createBlock(unref(UButton), {
123
+ key: 1,
124
+ type: "primary",
125
+ circle: "",
126
+ disabled: !canSend.value,
127
+ onClick: handleSend
128
+ }, {
129
+ default: withCtx(() => [createVNode(unref(UIcon), null, {
130
+ default: withCtx(() => [createVNode(unref(Send))]),
131
+ _: 1
132
+ })]),
133
+ _: 1
134
+ }, 8, ["disabled"]))
135
+ ], 2)
136
+ ], 2);
137
+ };
138
+ }
139
+ });
140
+ //#endregion
141
+ export { _sfc_main as default };
142
+
143
+ //# sourceMappingURL=chat-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-input.js","names":[],"sources":["../../../src/components/ai-chat/chat-input.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('input-area')\">\n <div v-if=\"attachments.length\" :class=\"cls.e('input-attachments')\">\n <div\n v-for=\"(att, index) in attachments\"\n :key=\"att.dataUrl\"\n :class=\"cls.e('input-attachment')\"\n >\n <img :src=\"att.dataUrl\" :alt=\"att.name\" />\n <UIcon :class=\"cls.e('input-attachment-remove')\" @click=\"attachments.splice(index, 1)\">\n <Close />\n </UIcon>\n </div>\n </div>\n\n <textarea\n ref=\"textareaRef\"\n v-model=\"text\"\n :class=\"cls.e('input')\"\n :placeholder=\"placeholder ?? '输入消息,Enter 发送,Shift + Enter 换行'\"\n rows=\"1\"\n @keydown.enter.exact.prevent=\"handleSend\"\n @input=\"autoResize\"\n />\n\n <div :class=\"cls.e('input-toolbar')\">\n <UIcon :class=\"cls.e('input-attach')\" title=\"添加图片\" @click=\"fileRef?.click()\">\n <Attach />\n </UIcon>\n <input\n ref=\"fileRef\"\n type=\"file\"\n hidden\n multiple\n :accept=\"accept ?? 'image/*'\"\n @change=\"handleFiles\"\n />\n\n <UButton v-if=\"running\" type=\"danger\" circle @click=\"emit('abort')\">\n <span :class=\"cls.e('input-stop')\" />\n </UButton>\n <UButton v-else type=\"primary\" circle :disabled=\"!canSend\" @click=\"handleSend\">\n <UIcon><Send /></UIcon>\n </UButton>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UIcon } from '@veltra/desktop'\nimport { Attach, Close, Send } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, shallowRef } from 'vue'\n\nimport type { ChatAttachment } from '../../chat/types'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatInput' })\n\nconst props = defineProps<{\n /** 是否生成中(显示停止按钮) */\n running: boolean\n /** 输入框占位文本 */\n placeholder?: string\n /** 附件 accept 类型 */\n accept?: string\n /** 单个附件最大字节数 */\n maxAttachmentSize?: number\n}>()\n\nconst emit = defineEmits<{\n (e: 'send', content: string, attachments: ChatAttachment[]): void\n (e: 'abort'): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst text = ref('')\nconst attachments = ref<ChatAttachment[]>([])\nconst textareaRef = shallowRef<HTMLTextAreaElement>()\nconst fileRef = shallowRef<HTMLInputElement>()\n\nconst canSend = computed(() => {\n return !props.running && (!!text.value.trim() || attachments.value.length > 0)\n})\n\nconst autoResize = () => {\n const el = textareaRef.value\n if (!el) return\n el.style.height = 'auto'\n el.style.height = `${Math.min(el.scrollHeight, 160)}px`\n}\n\nconst handleSend = () => {\n if (!canSend.value) return\n emit('send', text.value.trim(), attachments.value)\n text.value = ''\n attachments.value = []\n autoResize()\n}\n\nconst handleFiles = (event: Event) => {\n const input = event.target as HTMLInputElement\n const files = Array.from(input.files ?? [])\n input.value = ''\n\n const maxSize = props.maxAttachmentSize ?? 10 * 1024 * 1024\n\n for (const file of files) {\n if (file.size > maxSize) {\n console.warn(`[UAiChat] 附件 ${file.name} 超过大小限制,已忽略`)\n continue\n }\n const reader = new FileReader()\n reader.onload = () => {\n attachments.value.push({\n name: file.name,\n mimeType: file.type,\n size: file.size,\n dataUrl: String(reader.result)\n })\n }\n reader.readAsDataURL(file)\n }\n}\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;EA2DA,MAAM,QAAQ;EAWd,MAAM,OAAO;EAMb,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;EAEpC,MAAM,OAAO,IAAI,EAAE;EACnB,MAAM,cAAc,IAAsB,CAAC,CAAC;EAC5C,MAAM,cAAc,WAAgC;EACpD,MAAM,UAAU,WAA6B;EAE7C,MAAM,UAAU,eAAe;GAC7B,OAAO,CAAC,MAAM,YAAY,CAAC,CAAC,KAAK,MAAM,KAAK,KAAK,YAAY,MAAM,SAAS;EAC9E,CAAC;EAED,MAAM,mBAAmB;GACvB,MAAM,KAAK,YAAY;GACvB,IAAI,CAAC,IAAI;GACT,GAAG,MAAM,SAAS;GAClB,GAAG,MAAM,SAAS,GAAG,KAAK,IAAI,GAAG,cAAc,GAAG,EAAE;EACtD;EAEA,MAAM,mBAAmB;GACvB,IAAI,CAAC,QAAQ,OAAO;GACpB,KAAK,QAAQ,KAAK,MAAM,KAAK,GAAG,YAAY,KAAK;GACjD,KAAK,QAAQ;GACb,YAAY,QAAQ,CAAC;GACrB,WAAW;EACb;EAEA,MAAM,eAAe,UAAiB;GACpC,MAAM,QAAQ,MAAM;GACpB,MAAM,QAAQ,MAAM,KAAK,MAAM,SAAS,CAAC,CAAC;GAC1C,MAAM,QAAQ;GAEd,MAAM,UAAU,MAAM,qBAAqB,KAAK,OAAO;GAEvD,KAAK,MAAM,QAAQ,OAAO;IACxB,IAAI,KAAK,OAAO,SAAS;KACvB,QAAQ,KAAK,gBAAgB,KAAK,KAAK,YAAY;KACnD;IACF;IACA,MAAM,SAAS,IAAI,WAAW;IAC9B,OAAO,eAAe;KACpB,YAAY,MAAM,KAAK;MACrB,MAAM,KAAK;MACX,UAAU,KAAK;MACf,MAAM,KAAK;MACX,SAAS,OAAO,OAAO,MAAM;KAC/B,CAAC;IACH;IACA,OAAO,cAAc,IAAI;GAC3B;EACF;;uBA5HE,mBA4CM,OAAA,EA5CA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA,EAAA,GAAA;IACL,YAAA,MAAY,UAAA,UAAA,GAAvB,mBAWM,OAAA;;KAX0B,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA;0BAC1C,mBASM,UAAA,MAAA,WARmB,YAAA,QAAf,KAAK,UAAK;yBADpB,mBASM,OAAA;MAPH,KAAK,IAAI;MACT,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,kBAAA,CAAA;SAEb,mBAA0C,OAAA;MAApC,KAAK,IAAI;MAAU,KAAK,IAAI;8BAClC,YAEQ,MAAA,KAAA,GAAA;MAFA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,yBAAA,CAAA;MAA8B,UAAK,WAAE,YAAA,MAAY,OAAO,OAAK,CAAA;;6BACtE,CAAT,YAAS,MAAA,KAAA,CAAA,CAAA,CAAA;;;;mBAKf,mBAQE,YAAA;cAPI;KAAJ,KAAI;kEACK,KAAI,QAAA;KACZ,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,OAAA,CAAA;KACZ,aAAa,QAAA,eAAW;KACzB,MAAK;KACJ,WAAO,SAAA,cAAsB,YAAU,CAAA,SAAA,SAAA,CAAA,GAAA,CAAA,OAAA,CAAA;KACvC,SAAO;4CALC,KAAA,KAAI,CAAA,CAAA;IAQf,mBAmBM,OAAA,EAnBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,eAAA,CAAA,EAAA,GAAA;KAChB,YAEQ,MAAA,KAAA,GAAA;MAFA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA;MAAkB,OAAM;MAAQ,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,QAAA,OAAS,MAAK;;6BAC7D,CAAV,YAAU,MAAA,MAAA,CAAA,CAAA,CAAA;;;KAEZ,mBAOE,SAAA;eANI;MAAJ,KAAI;MACJ,MAAK;MACL,QAAA;MACA,UAAA;MACC,QAAQ,QAAA,UAAM;MACd,UAAQ;;KAGI,QAAA,WAAA,UAAA,GAAf,YAEU,MAAA,OAAA,GAAA;;MAFc,MAAK;MAAS,QAAA;MAAQ,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,OAAA;;6BAClB,CAArC,mBAAqC,QAAA,EAA9B,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,YAAA,CAAA,EAAA,GAAA,MAAA,CAAA,CAAA,CAAA;;yBAErB,YAEU,MAAA,OAAA,GAAA;;MAFM,MAAK;MAAU,QAAA;MAAQ,UAAQ,CAAG,QAAA;MAAU,SAAO;;6BAC1C,CAAvB,YAAuB,MAAA,KAAA,GAAA,MAAA;8BAAR,CAAR,YAAQ,MAAA,IAAA,CAAA,CAAA,CAAA"}
@@ -0,0 +1,6 @@
1
+ //#region src/components/ai-chat/di.ts
2
+ const AiChatDIKey = Symbol("AiChatDIKey");
3
+ //#endregion
4
+ export { AiChatDIKey };
5
+
6
+ //# sourceMappingURL=di.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"di.js","names":[],"sources":["../../../src/components/ai-chat/di.ts"],"sourcesContent":["import type { BEM } from '@veltra/utils'\nimport type { ComputedRef, InjectionKey, Slots } from 'vue'\n\nimport type { ChatTool } from '../../chat/types'\n\nexport interface AiChatContext {\n /** ai-chat 组件的 BEM 类生成器 */\n cls: BEM<'ai-chat'>\n /** ai-chat 使用方提供的插槽(用于 tool-<name> 动态插槽) */\n slots: Slots\n /** 按 name 索引的工具定义(解析 icon/label/render/autoCollapse) */\n tools: ComputedRef<Record<string, ChatTool | undefined>>\n}\n\nexport const AiChatDIKey: InjectionKey<AiChatContext> = Symbol('AiChatDIKey')\n"],"mappings":";AAcA,MAAa,cAA2C,OAAO,aAAa"}
@@ -0,0 +1,2 @@
1
+ import _default from "./ai-chat.vue.js";
2
+ export { _default as UAiChat };
@@ -0,0 +1,120 @@
1
+ import { AiChatDIKey } from "./di.js";
2
+ import _sfc_main$1 from "./tool-call.js";
3
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeProps, normalizeClass, openBlock, ref, renderList, toDisplayString, unref, vShow, watch, withCtx, withDirectives } from "vue";
4
+ import { bem } from "@veltra/utils";
5
+ import { UIcon } from "@veltra/desktop";
6
+ import { ArrowDown, InfoCircle, Loading, Refresh } from "@veltra/icons/normal";
7
+ import MarkdownRender from "markstream-vue";
8
+ //#region src/components/ai-chat/message-item.vue
9
+ const _hoisted_1 = ["src", "alt"];
10
+ const _sfc_main = /*@__PURE__*/ defineComponent({
11
+ name: "UAiChatMessageItem",
12
+ __name: "message-item",
13
+ props: {
14
+ message: {},
15
+ isLast: { type: Boolean },
16
+ rendererProps: {}
17
+ },
18
+ emits: ["respond", "regenerate"],
19
+ setup(__props, { emit: __emit }) {
20
+ const props = __props;
21
+ const emit = __emit;
22
+ const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
23
+ const isStreaming = computed(() => props.message.status === "streaming");
24
+ /** 思考中:流式且正文还未开始输出 */
25
+ const isThinking = computed(() => isStreaming.value && !props.message.content);
26
+ const reasoningExpanded = ref(true);
27
+ watch(() => props.message.status, (status) => {
28
+ if (status === "streaming") reasoningExpanded.value = true;
29
+ else if (status) reasoningExpanded.value = false;
30
+ });
31
+ const showActions = computed(() => {
32
+ if (props.message.role !== "assistant" || !props.isLast) return false;
33
+ return [
34
+ "done",
35
+ "error",
36
+ "aborted"
37
+ ].includes(props.message.status ?? "");
38
+ });
39
+ return (_ctx, _cache) => {
40
+ return openBlock(), createElementBlock("div", { class: normalizeClass([
41
+ unref(cls).e("message"),
42
+ unref(cls).em("message", __props.message.role),
43
+ unref(bem).is("streaming", isStreaming.value)
44
+ ]) }, [createCommentVNode(" 用户消息:右对齐气泡 "), __props.message.role === "user" ? (openBlock(), createElementBlock("div", {
45
+ key: 0,
46
+ class: normalizeClass(unref(cls).e("message-bubble"))
47
+ }, [__props.message.attachments?.length ? (openBlock(), createElementBlock("div", {
48
+ key: 0,
49
+ class: normalizeClass(unref(cls).e("message-attachments"))
50
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.message.attachments, (att) => {
51
+ return openBlock(), createElementBlock("img", {
52
+ key: att.dataUrl,
53
+ src: att.dataUrl,
54
+ alt: att.name,
55
+ class: normalizeClass(unref(cls).e("message-attachment"))
56
+ }, null, 10, _hoisted_1);
57
+ }), 128))], 2)) : createCommentVNode("v-if", true), __props.message.content ? (openBlock(), createElementBlock("div", {
58
+ key: 1,
59
+ class: normalizeClass(unref(cls).e("message-text"))
60
+ }, toDisplayString(__props.message.content), 3)) : createCommentVNode("v-if", true)], 2)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
61
+ createCommentVNode(" assistant 消息 "),
62
+ __props.message.reasoning ? (openBlock(), createElementBlock("div", {
63
+ key: 0,
64
+ class: normalizeClass([unref(cls).e("reasoning"), unref(bem).is("expanded", reasoningExpanded.value)])
65
+ }, [createElementVNode("div", {
66
+ class: normalizeClass(unref(cls).e("reasoning-header")),
67
+ onClick: _cache[0] || (_cache[0] = ($event) => reasoningExpanded.value = !reasoningExpanded.value)
68
+ }, [
69
+ createVNode(unref(UIcon), { class: normalizeClass([unref(cls).e("reasoning-icon"), isThinking.value && unref(cls).is("loading")]) }, {
70
+ default: withCtx(() => [isThinking.value ? (openBlock(), createBlock(unref(Loading), { key: 0 })) : (openBlock(), createBlock(unref(InfoCircle), { key: 1 }))]),
71
+ _: 1
72
+ }, 8, ["class"]),
73
+ createElementVNode("span", null, toDisplayString(isThinking.value ? "思考中…" : "思考过程"), 1),
74
+ createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("reasoning-chevron")) }, {
75
+ default: withCtx(() => [createVNode(unref(ArrowDown))]),
76
+ _: 1
77
+ }, 8, ["class"])
78
+ ], 2), withDirectives(createElementVNode("div", { class: normalizeClass(unref(cls).e("reasoning-content")) }, toDisplayString(__props.message.reasoning), 3), [[vShow, reasoningExpanded.value]])], 2)) : createCommentVNode("v-if", true),
79
+ __props.message.content || isStreaming.value ? (openBlock(), createBlock(unref(MarkdownRender), mergeProps({
80
+ key: 1,
81
+ mode: "chat",
82
+ class: unref(cls).e("message-content"),
83
+ content: __props.message.content,
84
+ final: !isStreaming.value,
85
+ fade: false
86
+ }, __props.rendererProps), null, 16, [
87
+ "class",
88
+ "content",
89
+ "final"
90
+ ])) : createCommentVNode("v-if", true),
91
+ (openBlock(true), createElementBlock(Fragment, null, renderList(__props.message.toolCalls ?? [], (call) => {
92
+ return openBlock(), createBlock(_sfc_main$1, {
93
+ key: call.id,
94
+ "tool-call": call,
95
+ onRespond: (approved) => emit("respond", call.id, approved)
96
+ }, null, 8, ["tool-call", "onRespond"]);
97
+ }), 128)),
98
+ __props.message.status === "error" ? (openBlock(), createElementBlock("div", {
99
+ key: 2,
100
+ class: normalizeClass(unref(cls).e("message-status"))
101
+ }, "生成出错", 2)) : __props.message.status === "aborted" ? (openBlock(), createElementBlock("div", {
102
+ key: 3,
103
+ class: normalizeClass(unref(cls).e("message-status"))
104
+ }, " 已停止生成 ", 2)) : createCommentVNode("v-if", true),
105
+ showActions.value ? (openBlock(), createElementBlock("div", {
106
+ key: 4,
107
+ class: normalizeClass(unref(cls).e("message-actions")),
108
+ onClick: _cache[1] || (_cache[1] = ($event) => emit("regenerate"))
109
+ }, [createVNode(unref(UIcon), { title: "重新生成" }, {
110
+ default: withCtx(() => [createVNode(unref(Refresh))]),
111
+ _: 1
112
+ })], 2)) : createCommentVNode("v-if", true)
113
+ ], 64))], 2);
114
+ };
115
+ }
116
+ });
117
+ //#endregion
118
+ export { _sfc_main as default };
119
+
120
+ //# sourceMappingURL=message-item.js.map