@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.
- package/dist/chat/index.d.ts +5 -0
- package/dist/chat/transports/index.d.ts +2 -0
- package/dist/chat/transports/openai.d.ts +23 -0
- package/dist/chat/transports/openai.js +176 -0
- package/dist/chat/transports/openai.js.map +1 -0
- package/dist/chat/types.d.ts +125 -0
- package/dist/chat/use-chat.d.ts +25 -0
- package/dist/chat/use-chat.js +214 -0
- package/dist/chat/use-chat.js.map +1 -0
- package/dist/components/ai-chat/ai-chat.js +88 -0
- package/dist/components/ai-chat/ai-chat.js.map +1 -0
- package/dist/components/ai-chat/ai-chat.vue.d.ts +36 -0
- package/dist/components/ai-chat/ask-question.js +187 -0
- package/dist/components/ai-chat/ask-question.js.map +1 -0
- package/dist/components/ai-chat/chat-input.js +143 -0
- package/dist/components/ai-chat/chat-input.js.map +1 -0
- package/dist/components/ai-chat/di.js +6 -0
- package/dist/components/ai-chat/di.js.map +1 -0
- package/dist/components/ai-chat/index.d.ts +2 -0
- package/dist/components/ai-chat/message-item.js +120 -0
- package/dist/components/ai-chat/message-item.js.map +1 -0
- package/dist/components/ai-chat/message-list.js +75 -0
- package/dist/components/ai-chat/message-list.js.map +1 -0
- package/dist/components/ai-chat/style.d.ts +5 -0
- package/dist/components/ai-chat/style.js +6 -0
- package/dist/components/ai-chat/style2.css +416 -0
- package/dist/components/ai-chat/tool-call.js +151 -0
- package/dist/components/ai-chat/tool-call.js.map +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +5 -0
- package/dist/style.d.ts +1 -0
- package/dist/style.js +1 -0
- package/dist/tools/ask-question/ask-question.d.ts +45 -0
- package/dist/tools/ask-question/ask-question.js +65 -0
- package/dist/tools/ask-question/ask-question.js.map +1 -0
- package/dist/tools/ask-question/deferred.js +22 -0
- package/dist/tools/ask-question/deferred.js.map +1 -0
- package/dist/tools/ask-question/index.d.ts +2 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/types/ai-chat.d.ts +49 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +40 -0
- package/src/chat/__test__/use-chat.test.ts +246 -0
- package/src/chat/index.ts +3 -0
- package/src/chat/transports/index.ts +1 -0
- package/src/chat/transports/openai.ts +237 -0
- package/src/chat/types.ts +136 -0
- package/src/chat/use-chat.ts +245 -0
- package/src/components/ai-chat/__test__/ai-chat.test.ts +316 -0
- package/src/components/ai-chat/ai-chat.vue +67 -0
- package/src/components/ai-chat/ask-question.vue +232 -0
- package/src/components/ai-chat/chat-input.vue +127 -0
- package/src/components/ai-chat/di.ts +15 -0
- package/src/components/ai-chat/index.ts +1 -0
- package/src/components/ai-chat/message-item.vue +120 -0
- package/src/components/ai-chat/message-list.vue +81 -0
- package/src/components/ai-chat/style.scss +439 -0
- package/src/components/ai-chat/style.ts +6 -0
- package/src/components/ai-chat/tool-call.vue +147 -0
- package/src/components/index.ts +1 -0
- package/src/env.d.ts +12 -0
- package/src/index.ts +4 -0
- package/src/style.ts +1 -0
- package/src/tools/ask-question/__test__/ask-question.test.ts +198 -0
- package/src/tools/ask-question/ask-question.ts +114 -0
- package/src/tools/ask-question/deferred.ts +27 -0
- package/src/tools/ask-question/index.ts +1 -0
- package/src/tools/index.ts +1 -0
- package/src/types/ai-chat.ts +56 -0
- 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":";;;;;;;;;;;;;EA2HA,MAAM,QAAQ;EAGd,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;;EAGpC,MAAM,YAAY,eAAkC;GAClD,IAAI;IACF,MAAM,OAAO,KAAK,MAAM,MAAM,SAAS,aAAa,IAAI;IACxD,OAAO,MAAM,QAAQ,KAAK,SAAS,IAAI,KAAK,YAAY,CAAC;GAC3D,QAAQ;IACN,OAAO,CAAC;GACV;EACF,CAAC;;EAGD,MAAM,YAAY,eAAe;GAC/B,OACE,MAAM,SAAS,UAAU,SACxB,MAAM,SAAS,WAAW,aAAa,MAAM,SAAS,WAAW;EAEtE,CAAC;;EAGD,MAAM,UAAU,IAAI,CAAC;;EAErB,MAAM,aAAa,IAAuB,CAAC,CAAC;;EAE5C,MAAM,UAAU,IAAc,CAAC,CAAC;EAEhC,MACE,YACC,SAAS;GACR,IAAI,MAAM,SAAS,UAAU,MAAM;GACnC,WAAW,QAAQ,KAAK,UAAU,IAAI;GACtC,QAAQ,QAAQ,KAAK,UAAU,EAAE;EACnC,GACA,EAAE,WAAW,KAAK,CACpB;EAEA,MAAM,kBAAkB,eAAe,UAAU,MAAM,QAAQ,MAAM;;EAGrE,MAAM,cAAc,UAAkB;GACpC,OAAO,QAAQ,QAAQ,MAAM,MAAM,EAAE,KAAK,KAAK,WAAW,MAAM,MAAM;EACxE;EAEA,MAAM,cAAc,eAAe;GACjC,OAAO,UAAU,MAAM,SAAS,KAAK,UAAU,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,CAAC;EACpF,CAAC;EAED,MAAM,QAAQ,UAAkB;GAC9B,IAAI,QAAQ,KAAK,SAAS,UAAU,MAAM,QAAQ;GAClD,QAAQ,QAAQ;EAClB;;EAGA,MAAM,cAAc,WAAmB;GACrC,MAAM,IAAI,QAAQ;GAClB,MAAM,WAAW,WAAW,MAAM,OAAO,SAAS,OAAO;GACzD,WAAW,MAAM,KAAK;GACtB,IAAI,UAAU,QAAQ,MAAM,KAAK;EACnC;;EAGA,MAAM,iBAAiB,UAAkB;GACvC,QAAQ,MAAM,QAAQ,SAAS;GAC/B,IAAI,OAAO,WAAW,MAAM,QAAQ,SAAS;EAC/C;EAEA,MAAM,qBAAqB;GACzB,MAAM,kBAAkB,UAAU,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;GAC1E,IAAI,mBAAmB,GAAG;IACxB,QAAQ,QAAQ;IAChB;GACF;GACA,MAAM,UAAU,UAAU,MAAM,KAAK,MAAM,OAAO;IAChD,UAAU,KAAK;IACf,QAAQ,QAAQ,MAAM,EAAE,EAAE,KAAK,KAAK,WAAW,MAAM,MAAM;GAC7D,EAAE;GACF,mBAAmB,MAAM,SAAS,IAAI,EAAE,QAAQ,CAAC;EACnD;;EAGA,MAAM,cAAc,eAAe;GACjC,IAAI,MAAM,SAAS,WAAW,aAAa,CAAC,MAAM,SAAS,QAAQ,OAAO,CAAC;GAC3E,IAAI;GACJ,IAAI;IACF,SAAS,KAAK,MAAM,MAAM,SAAS,MAAM;GAC3C,QAAQ;IACN,OAAO,CAAC;GACV;GACA,IAAI,CAAC,MAAM,QAAQ,QAAQ,OAAO,GAAG,OAAO,CAAC;GAC7C,OAAO,OAAO,QAAQ,KAAK,UAAU;IACnC,GAAG;;IAEH,UAAU,UAAU,MAAM,MACvB,MAAM,EAAE,aAAa,KAAK,YAAY,EAAE,SAAS,SAAS,KAAK,MAAM,CACxE;GACF,EAAE;EACJ,CAAC;EAED,MAAM,aAAa,eAAe;GAChC,IAAI,MAAM,SAAS,WAAW,SAAS,OAAO;GAC9C,MAAM,MAAM,MAAM,SAAS,SAAS;GACpC,IAAI,IAAI,SAAS,OAAO,KAAK,IAAI,SAAS,IAAI,GAAG,OAAO;GACxD,OAAO,OAAO;EAChB,CAAC;;uBArOC,mBAuGM,OAAA,EAvGA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA,EAAA,GAAA,CAChB,mBAAA,YAAA,GACgB,UAAA,SAAA,UAAA,GAAhB,mBA6EW,UAAA,EAAA,KAAA,EAAA,GAAA;IA5EE,UAAA,MAAU,SAAM,KAAA,UAAA,GAA3B,mBAcM,OAAA;;KAd4B,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,uBAAA,CAAA;QAC5C,mBAA4F,QAAA,EAArF,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,oBAAA,CAAA,EAAA,GAAA,gBAA2B,QAAA,QAAO,CAAA,IAAO,QAAG,gBAAG,UAAA,MAAU,MAAM,GAAA,CAAA,GAClF,mBAWO,QAAA,EAXA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA,EAAA,GAAA,EAAA,UAAA,IAAA,GACjB,mBASE,UAAA,MAAA,WARiB,UAAA,QAAT,GAAG,MAAC;yBADd,mBASE,QAAA;MAPC,KAAK;MACL,OAAK,eAAA;OAAkB,MAAA,GAAA,CAAG,CAAC,EAAC,kBAAA;OAAoC,MAAA,GAAA,CAAG,CAAC,GAAE,WAAY,MAAM,QAAA,KAAO;OAAiB,MAAA,GAAA,CAAG,CAAC,GAAE,YAAa,WAAW,CAAC,CAAA;;MAK/I,UAAK,WAAE,KAAK,CAAC;;;IAKpB,mBAKM,OAAA,EALA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,gBAAA,CAAA,EAAA,GAAA,CACJ,UAAA,MAAU,SAAM,KAAA,UAAA,GAA5B,mBAEO,QAAA;;KAF4B,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,iBAAA,CAAA;KAAqB,eAAY;uBAC3E,QAAA,QAAO,CAAA,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,GAAA,gBACL,MACP,gBAAG,gBAAA,OAAiB,QAAQ,GAAA,CAAA,CAAA,GAAA,CAAA;IAItB,gBAAA,OAAiB,SAAS,UAAA,UAAA,GADlC,mBAuBM,OAAA;;KArBH,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,sBAAA,CAAA;KACb,MAAK;0BAEL,mBAiBS,UAAA,MAAA,WAhBU,gBAAA,MAAgB,UAA1B,WAAM;yBADf,mBAiBS,UAAA;MAfN,KAAK;MACN,MAAK;MACL,MAAK;MACJ,gBAAc,WAAA,MAAW,QAAA,WAAa;MACtC,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,kBAAA,GAAsB,MAAA,GAAA,CAAG,CAAC,GAAE,YAAa,WAAA,MAAW,QAAA,WAAa,MAAM,CAAA,CAAA;MACpF,UAAK,WAAE,WAAW,MAAM;SAEzB,mBAME,QAAA;MALC,OAAK,eAAA,CAAkB,MAAA,GAAA,CAAG,CAAC,EAAC,oBAAA,GAAsC,MAAA,GAAA,CAAG,CAAC,GAAE,YAAa,WAAA,MAAW,QAAA,WAAa,MAAM,CAAA,CAAA;MAIpH,eAAY;kBAEd,mBAAiE,QAAA,EAA1D,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,uBAAA,CAAA,EAAA,GAAA,gBAA8B,MAAM,GAAA,CAAA,CAAA,GAAA,IAAA,UAAA;;IAI3D,YAIE,MAAA,MAAA,GAAA;KAHC,eAAa,QAAA,MAAQ,QAAA;KACrB,aAAa,gBAAA,OAAiB,eAAW;KACzC,uBAAoB;;IAGvB,mBAqBS,UAAA,EArBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,sBAAA,CAAA,EAAA,GAAA,CAEX,UAAA,MAAU,SAAM,KAAA,UAAA,GADxB,YAQU,MAAA,OAAA,GAAA;;KANR,MAAK;KACL,MAAA;KACC,UAAU,QAAA,UAAO;KACjB,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,KAAK,QAAA,QAAO,CAAA;;4BAGtB,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAAA,gBAFC,SAED,EAAA,CAAA,EAAA,CAAA;;6DAEQ,QAAA,QAAU,UAAA,MAAU,SAAM,KAAA,UAAA,GADlC,YAOU,MAAA,OAAA,GAAA;;KALR,MAAK;KACL,MAAK;KACJ,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,KAAK,QAAA,QAAO,CAAA;;4BAGtB,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAAA,gBAFC,SAED,EAAA,CAAA,EAAA,CAAA;;wBACA,YAEU,MAAA,OAAA,GAAA;;KAFM,MAAK;KAAQ,MAAK;KAAW,UAAQ,CAAG,YAAA;KAAc,SAAO;;4BAE7E,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAAA,gBAF2F,QAE3F,EAAA,CAAA,EAAA,CAAA;;;aAKiB,QAAA,SAAS,WAAM,aAAA,UAAA,GAApC,mBAiBW,UAAA,EAAA,KAAA,EAAA,GAAA,CAlBX,mBAAA,YAAA,IAAA,UAAA,IAAA,GAEE,mBAeM,UAAA,MAAA,WAfmB,YAAA,QAAZ,MAAM,MAAC;wBAApB,mBAeM,OAAA;KAfiC,KAAK;KAAI,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,qBAAA,CAAA;QAC1D,mBASM,OAAA,EATA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,uBAAA,CAAA,EAAA,GAAA,CAER,YAAA,MAAY,SAAM,KAAA,UAAA,GAD1B,mBAMO,QAAA;;KAJJ,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,wBAAA,CAAA;KACb,eAAY;uBAET,IAAC,CAAA,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,GAAA,gBACC,MACP,gBAAG,KAAK,QAAQ,GAAA,CAAA,CAAA,GAAA,CAAA,GAElB,mBAGM,OAAA,EAHA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,uBAAA,CAAA,EAAA,GAAA,CACJ,KAAK,YAAA,UAAA,GAAjB,mBAAuF,QAAA;;KAA3D,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA;uBAA0B,KAAK,MAAM,GAAA,CAAA,MAAA,UAAA,GAC7E,mBAA6C,UAAA,EAAA,KAAA,EAAA,GAAA,CAAA,gBAAA,gBAAzB,KAAK,MAAM,GAAA,CAAA,CAAA,GAAA,EAAA,EAAA,GAAA,CAAA,CAAA,GAAA,CAAA;oCAMrC,mBAAwE,UAAA,EAAA,KAAA,EAAA,GAAA,CADxE,mBAAA,WAAA,GACA,mBAAwE,OAAA,EAA3D,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,qBAAA,CAAA,EAAA,GAAA,gBAA4B,WAAA,KAAU,GAAA,CAAA,CAAA,GAAA,IAAA,EAAA,GAAA,CAAA"}
|
|
@@ -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":"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"}
|
|
@@ -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,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
|