@veltra/ai 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/index.d.ts +2 -2
- package/dist/chat/transports/openai.js +1 -1
- package/dist/chat/transports/openai.js.map +1 -1
- package/dist/chat/types.d.ts +37 -2
- package/dist/chat/use-chat.d.ts +23 -1
- package/dist/chat/use-chat.js +83 -8
- package/dist/chat/use-chat.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js +207 -46
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/chat-input.js +37 -12
- package/dist/components/ai-chat/chat-input.js.map +1 -1
- package/dist/components/ai-chat/di.js.map +1 -1
- package/dist/components/ai-chat/message-item.js +101 -23
- package/dist/components/ai-chat/message-item.js.map +1 -1
- package/dist/components/ai-chat/message-list.js +217 -32
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/model-picker.js +69 -46
- package/dist/components/ai-chat/model-picker.js.map +1 -1
- package/dist/components/ai-chat/queue-list.js +70 -0
- package/dist/components/ai-chat/queue-list.js.map +1 -0
- package/dist/components/ai-chat/side-panel.js +96 -0
- package/dist/components/ai-chat/side-panel.js.map +1 -0
- package/dist/components/ai-chat/style.d.ts +4 -0
- package/dist/components/ai-chat/style.js +4 -0
- package/dist/components/ai-chat/style2.css +457 -74
- package/dist/components/ai-chat/tool-call.js +60 -26
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/components/ai-orb/ai-orb.js +94 -0
- package/dist/components/ai-orb/ai-orb.js.map +1 -0
- package/dist/components/ai-orb/orb-renderer.js +411 -0
- package/dist/components/ai-orb/orb-renderer.js.map +1 -0
- package/dist/components/ai-orb/style.d.ts +1 -0
- package/dist/components/ai-orb/style.js +1 -0
- package/dist/components/ai-orb/style2.css +23 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/style.js +1 -0
- package/dist/types/ai-chat.d.ts +21 -7
- package/dist/types/ai-orb.d.ts +27 -0
- package/dist/types/index.d.ts +2 -1
- package/package.json +5 -5
- package/src/chat/__test__/use-chat.test.ts +224 -9
- package/src/chat/transports/__test__/openai.test.ts +78 -0
- package/src/chat/transports/openai.ts +2 -2
- package/src/chat/types.ts +37 -1
- package/src/chat/use-chat.ts +112 -9
- package/src/components/ai-chat/__test__/ai-chat.test.ts +588 -11
- package/src/components/ai-chat/ai-chat.vue +227 -31
- package/src/components/ai-chat/chat-input.vue +41 -11
- package/src/components/ai-chat/di.ts +2 -0
- package/src/components/ai-chat/message-item.vue +98 -11
- package/src/components/ai-chat/message-list.vue +261 -21
- package/src/components/ai-chat/model-picker.vue +110 -59
- package/src/components/ai-chat/queue-list.vue +63 -0
- package/src/components/ai-chat/side-panel.vue +76 -0
- package/src/components/ai-chat/style.scss +527 -74
- package/src/components/ai-chat/style.ts +4 -0
- package/src/components/ai-chat/tool-call.vue +86 -42
- package/src/components/ai-orb/__test__/ai-orb.test.ts +251 -0
- package/src/components/ai-orb/ai-orb.vue +107 -0
- package/src/components/ai-orb/index.ts +1 -0
- package/src/components/ai-orb/orb-renderer.ts +505 -0
- package/src/components/ai-orb/style.scss +10 -0
- package/src/components/ai-orb/style.ts +1 -0
- package/src/components/index.ts +1 -0
- package/src/style.ts +1 -0
- package/src/tools/ask-question/__test__/ask-question.test.ts +1 -1
- package/src/types/ai-chat.ts +21 -6
- package/src/types/ai-orb.ts +27 -0
- package/src/types/index.ts +1 -0
|
@@ -3,9 +3,17 @@ import { createBuiltinTools } from "../../tools/index.js";
|
|
|
3
3
|
import { useChat } from "../../chat/use-chat.js";
|
|
4
4
|
import _sfc_main$1 from "./chat-input.js";
|
|
5
5
|
import _sfc_main$2 from "./message-list.js";
|
|
6
|
-
import
|
|
6
|
+
import _sfc_main$3 from "./queue-list.js";
|
|
7
|
+
import _sfc_main$4 from "./side-panel.js";
|
|
8
|
+
import { useResizeObserver } from "@veltra/compositions";
|
|
9
|
+
import { computed, createBlock, createCommentVNode, createElementVNode, createSlots, createVNode, defineComponent, isRef, normalizeClass, openBlock, provide, ref, renderSlot, unref, useSlots, useTemplateRef, watch, withCtx } from "vue";
|
|
10
|
+
import { ULayout } from "@veltra/desktop";
|
|
7
11
|
import { bem } from "@veltra/utils";
|
|
8
12
|
//#region src/components/ai-chat/ai-chat.vue
|
|
13
|
+
const PANEL_MIN_WIDTH = 320;
|
|
14
|
+
const MAIN_MIN_WIDTH = 360;
|
|
15
|
+
/** 面板打开时默认给会话区留的宽度(即面板默认尽可能大) */
|
|
16
|
+
const MAIN_DEFAULT_WIDTH = 860;
|
|
9
17
|
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
10
18
|
name: "UAiChat",
|
|
11
19
|
__name: "ai-chat",
|
|
@@ -13,6 +21,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
13
21
|
transport: { type: Function },
|
|
14
22
|
tools: {},
|
|
15
23
|
systemPrompt: {},
|
|
24
|
+
maxToolRounds: {},
|
|
16
25
|
messages: {},
|
|
17
26
|
models: {},
|
|
18
27
|
model: {},
|
|
@@ -37,68 +46,220 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
37
46
|
const emit = __emit;
|
|
38
47
|
const cls = bem("ai-chat");
|
|
39
48
|
const slots = useSlots();
|
|
40
|
-
/** 内置 + 用户工具(同名内置优先),供工具卡片解析 icon/label/render/autoCollapse */
|
|
49
|
+
/** 内置 + 用户工具(同名内置优先),供工具卡片解析 icon/label/render/renderTo/autoCollapse */
|
|
41
50
|
const toolMap = computed(() => {
|
|
42
51
|
const builtins = createBuiltinTools();
|
|
43
52
|
const names = new Set(builtins.map((t) => t.name));
|
|
44
53
|
const tools = [...builtins, ...(props.tools ?? []).filter((t) => !names.has(t.name))];
|
|
45
54
|
return Object.fromEntries(tools.map((t) => [t.name, t]));
|
|
46
55
|
});
|
|
56
|
+
const { messages, model, reasoningLevel, running, queue, send, abort, regenerate, clear, respondToolCall, enqueue, startQueued, removeQueued } = useChat({
|
|
57
|
+
props,
|
|
58
|
+
emit
|
|
59
|
+
});
|
|
60
|
+
/** 空会话(无可见消息):输入区垂直居中,欢迎区(orb)贴于输入区上方并与之左对齐 */
|
|
61
|
+
const isEmpty = computed(() => !messages.value.some((msg) => msg.role !== "tool"));
|
|
62
|
+
/** 所有 renderTo: 'panel' 的工具调用(按消息顺序,面板内容的来源) */
|
|
63
|
+
const panelCalls = computed(() => {
|
|
64
|
+
const calls = [];
|
|
65
|
+
for (const msg of messages.value) {
|
|
66
|
+
if (msg.role !== "assistant" || !msg.toolCalls) continue;
|
|
67
|
+
for (const call of msg.toolCalls) if (toolMap.value[call.name]?.renderTo === "panel") calls.push(call);
|
|
68
|
+
}
|
|
69
|
+
return calls;
|
|
70
|
+
});
|
|
71
|
+
/** 当前展示在右侧面板的工具调用 id;null 表示面板关闭 */
|
|
72
|
+
const activePanelCallId = ref(null);
|
|
73
|
+
const activePanelCall = computed(() => {
|
|
74
|
+
if (!activePanelCallId.value) return null;
|
|
75
|
+
return panelCalls.value.find((call) => call.id === activePanelCallId.value) ?? null;
|
|
76
|
+
});
|
|
77
|
+
const panelOpen = computed(() => activePanelCall.value != null);
|
|
78
|
+
watch(() => panelCalls.value[panelCalls.value.length - 1]?.id, (id) => {
|
|
79
|
+
if (id) activePanelCallId.value = id;
|
|
80
|
+
});
|
|
81
|
+
watch(panelCalls, (calls) => {
|
|
82
|
+
const id = activePanelCallId.value;
|
|
83
|
+
if (id && !calls.some((call) => call.id === id)) activePanelCallId.value = calls[calls.length - 1]?.id ?? null;
|
|
84
|
+
});
|
|
85
|
+
/** 打开面板并聚焦到指定工具调用(工具卡片「查看面板」入口,经 DI 下发) */
|
|
86
|
+
const openPanel = (toolCallId) => {
|
|
87
|
+
if (panelCalls.value.some((call) => call.id === toolCallId)) activePanelCallId.value = toolCallId;
|
|
88
|
+
};
|
|
89
|
+
const closePanel = () => {
|
|
90
|
+
activePanelCallId.value = null;
|
|
91
|
+
};
|
|
47
92
|
provide(AiChatDIKey, {
|
|
48
93
|
cls,
|
|
49
94
|
slots,
|
|
50
|
-
tools: toolMap
|
|
95
|
+
tools: toolMap,
|
|
96
|
+
openPanel
|
|
51
97
|
});
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
98
|
+
/**
|
|
99
|
+
* 面板宽度:经 ULayout 列轨施加(cols = 1fr + 面板宽度),
|
|
100
|
+
* 拖拽手柄由 ULayout 提供(colMinSizes 约束会话区 / 面板最小宽度)。
|
|
101
|
+
*/
|
|
102
|
+
const layoutRef = useTemplateRef("layoutRef");
|
|
103
|
+
const panelRef = useTemplateRef("panelRef");
|
|
104
|
+
const panelWidth = ref(PANEL_MIN_WIDTH);
|
|
105
|
+
const resizing = ref(false);
|
|
106
|
+
const layoutCols = computed(() => {
|
|
107
|
+
return panelOpen.value ? ["1fr", `${panelWidth.value}px`] : ["1fr"];
|
|
108
|
+
});
|
|
109
|
+
const containerWidth = () => (layoutRef.value?.$el)?.clientWidth ?? 0;
|
|
110
|
+
/** 面板默认宽度:容器宽减去会话区默认保留宽度(缺省给会话区留 MAIN_DEFAULT_WIDTH) */
|
|
111
|
+
const defaultPanelWidth = () => {
|
|
112
|
+
const width = containerWidth();
|
|
113
|
+
return width ? width - MAIN_DEFAULT_WIDTH : 420;
|
|
114
|
+
};
|
|
115
|
+
/** 钳位面板宽度在 [PANEL_MIN_WIDTH, 容器宽 - MAIN_MIN_WIDTH] 区间 */
|
|
116
|
+
const clampPanelWidth = (width) => {
|
|
117
|
+
const width0 = containerWidth();
|
|
118
|
+
const maxWidth = width0 ? Math.max(PANEL_MIN_WIDTH, width0 - MAIN_MIN_WIDTH) : Number.MAX_SAFE_INTEGER;
|
|
119
|
+
return Math.min(Math.max(width, PANEL_MIN_WIDTH), maxWidth);
|
|
120
|
+
};
|
|
121
|
+
watch(activePanelCall, (call, prevCall) => {
|
|
122
|
+
if (!call) return;
|
|
123
|
+
const width = toolMap.value[call.name]?.panelWidth ?? (prevCall ? void 0 : defaultPanelWidth());
|
|
124
|
+
if (typeof width !== "number") return;
|
|
125
|
+
panelWidth.value = clampPanelWidth(width);
|
|
55
126
|
});
|
|
127
|
+
useResizeObserver({
|
|
128
|
+
targets: computed(() => panelRef.value?.$el ?? void 0),
|
|
129
|
+
onResize(entries) {
|
|
130
|
+
const inlineSize = entries[0]?.borderBoxSize?.[0]?.inlineSize;
|
|
131
|
+
if (!inlineSize) return;
|
|
132
|
+
const width = Math.round(inlineSize);
|
|
133
|
+
if (Math.abs(width - panelWidth.value) < 2) return;
|
|
134
|
+
panelWidth.value = width;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
const chatInputRef = useTemplateRef("chatInputRef");
|
|
138
|
+
/**
|
|
139
|
+
* 编辑中的队列项原后继 id(编辑态标记):
|
|
140
|
+
* undefined 表示无编辑;null 表示原位置在队尾;否则重新提交时插回该后继之前,
|
|
141
|
+
* 使前后项顺序不受编辑影响(前项已执行则该项自然成为最前待执行项)。
|
|
142
|
+
*/
|
|
143
|
+
const editingSuccessor = ref(void 0);
|
|
144
|
+
/** 编辑队列项:取回输入框重新编辑;已有未完成编辑时先按锚点还原,避免内容丢失 */
|
|
145
|
+
const handleQueueEdit = (id) => {
|
|
146
|
+
restoreEditing();
|
|
147
|
+
const index = queue.value.findIndex((item) => item.id === id);
|
|
148
|
+
if (index === -1) return;
|
|
149
|
+
editingSuccessor.value = queue.value[index + 1]?.id ?? null;
|
|
150
|
+
const item = removeQueued(id);
|
|
151
|
+
if (item) chatInputRef.value?.setContent(item.content);
|
|
152
|
+
};
|
|
153
|
+
/** 将输入框中未提交的编辑内容按原锚点插回队列 */
|
|
154
|
+
const restoreEditing = () => {
|
|
155
|
+
if (editingSuccessor.value === void 0) return;
|
|
156
|
+
const content = chatInputRef.value?.getContent().trim();
|
|
157
|
+
if (content) enqueue(content, void 0, editingSuccessor.value ?? void 0);
|
|
158
|
+
editingSuccessor.value = void 0;
|
|
159
|
+
};
|
|
160
|
+
const handleSend = (content, attachments) => {
|
|
161
|
+
if (editingSuccessor.value !== void 0) {
|
|
162
|
+
enqueue(content, attachments, editingSuccessor.value ?? void 0);
|
|
163
|
+
editingSuccessor.value = void 0;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
send(content, attachments);
|
|
167
|
+
};
|
|
56
168
|
__expose({
|
|
57
169
|
send,
|
|
58
170
|
abort,
|
|
59
171
|
regenerate,
|
|
60
|
-
clear
|
|
172
|
+
clear,
|
|
173
|
+
queue,
|
|
174
|
+
startQueued,
|
|
175
|
+
removeQueued,
|
|
176
|
+
enqueue
|
|
61
177
|
});
|
|
62
178
|
return (_ctx, _cache) => {
|
|
63
|
-
return openBlock(),
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
179
|
+
return openBlock(), createBlock(unref(ULayout), {
|
|
180
|
+
ref_key: "layoutRef",
|
|
181
|
+
ref: layoutRef,
|
|
182
|
+
class: normalizeClass([unref(cls).b, unref(bem).is("resizing", resizing.value)]),
|
|
183
|
+
cols: layoutCols.value,
|
|
184
|
+
rows: "1fr",
|
|
185
|
+
resizable: panelOpen.value,
|
|
186
|
+
"col-min-sizes": panelOpen.value ? [MAIN_MIN_WIDTH, PANEL_MIN_WIDTH] : void 0,
|
|
187
|
+
onResizeStart: _cache[3] || (_cache[3] = ($event) => resizing.value = true),
|
|
188
|
+
onResizeEnd: _cache[4] || (_cache[4] = ($event) => resizing.value = false)
|
|
189
|
+
}, {
|
|
190
|
+
default: withCtx(() => [
|
|
191
|
+
createElementVNode("div", { class: normalizeClass([unref(cls).e("main"), unref(bem).is("empty", isEmpty.value)]) }, [
|
|
192
|
+
createVNode(_sfc_main$2, {
|
|
193
|
+
messages: unref(messages),
|
|
194
|
+
welcome: __props.welcome,
|
|
195
|
+
running: unref(running),
|
|
196
|
+
"renderer-props": __props.rendererProps,
|
|
197
|
+
onRespond: unref(respondToolCall),
|
|
198
|
+
onRegenerate: unref(regenerate),
|
|
199
|
+
onWelcomeClick: _cache[0] || (_cache[0] = (text) => handleSend(text, []))
|
|
200
|
+
}, createSlots({ _: 2 }, [_ctx.$slots.welcome ? {
|
|
201
|
+
name: "welcome",
|
|
202
|
+
fn: withCtx(() => [renderSlot(_ctx.$slots, "welcome")]),
|
|
203
|
+
key: "0"
|
|
204
|
+
} : void 0]), 1032, [
|
|
205
|
+
"messages",
|
|
206
|
+
"welcome",
|
|
207
|
+
"running",
|
|
208
|
+
"renderer-props",
|
|
209
|
+
"onRespond",
|
|
210
|
+
"onRegenerate"
|
|
211
|
+
]),
|
|
212
|
+
createVNode(_sfc_main$3, {
|
|
213
|
+
queue: unref(queue),
|
|
214
|
+
onStartNow: unref(startQueued),
|
|
215
|
+
onEdit: handleQueueEdit,
|
|
216
|
+
onRemove: unref(removeQueued)
|
|
217
|
+
}, null, 8, [
|
|
218
|
+
"queue",
|
|
219
|
+
"onStartNow",
|
|
220
|
+
"onRemove"
|
|
221
|
+
]),
|
|
222
|
+
createVNode(_sfc_main$1, {
|
|
223
|
+
ref_key: "chatInputRef",
|
|
224
|
+
ref: chatInputRef,
|
|
225
|
+
model: unref(model),
|
|
226
|
+
"onUpdate:model": _cache[1] || (_cache[1] = ($event) => isRef(model) ? model.value = $event : null),
|
|
227
|
+
"reasoning-level": unref(reasoningLevel),
|
|
228
|
+
"onUpdate:reasoningLevel": _cache[2] || (_cache[2] = ($event) => isRef(reasoningLevel) ? reasoningLevel.value = $event : null),
|
|
229
|
+
running: unref(running),
|
|
230
|
+
models: __props.models,
|
|
231
|
+
placeholder: __props.placeholder,
|
|
232
|
+
accept: __props.accept,
|
|
233
|
+
"max-attachment-size": __props.maxAttachmentSize,
|
|
234
|
+
onSend: handleSend,
|
|
235
|
+
onAbort: unref(abort)
|
|
236
|
+
}, null, 8, [
|
|
237
|
+
"model",
|
|
238
|
+
"reasoning-level",
|
|
239
|
+
"running",
|
|
240
|
+
"models",
|
|
241
|
+
"placeholder",
|
|
242
|
+
"accept",
|
|
243
|
+
"max-attachment-size",
|
|
244
|
+
"onAbort"
|
|
245
|
+
])
|
|
246
|
+
], 2),
|
|
247
|
+
createCommentVNode(" 侧边面板(renderTo: 'panel' 的工具渲染区),与会话区宽度经 ULayout 拖拽手柄调节 "),
|
|
248
|
+
activePanelCall.value ? (openBlock(), createBlock(_sfc_main$4, {
|
|
249
|
+
key: 0,
|
|
250
|
+
ref_key: "panelRef",
|
|
251
|
+
ref: panelRef,
|
|
252
|
+
"tool-call": activePanelCall.value,
|
|
253
|
+
onClose: closePanel
|
|
254
|
+
}, null, 8, ["tool-call"])) : createCommentVNode("v-if", true)
|
|
255
|
+
]),
|
|
256
|
+
_: 3
|
|
257
|
+
}, 8, [
|
|
258
|
+
"class",
|
|
259
|
+
"cols",
|
|
260
|
+
"resizable",
|
|
261
|
+
"col-min-sizes"
|
|
262
|
+
]);
|
|
102
263
|
};
|
|
103
264
|
}
|
|
104
265
|
});
|
|
@@ -1 +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 v-model:model=\"model\"\n v-model:reasoning-level=\"reasoningLevel\"\n :running=\"running\"\n :models=\"models\"\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 { ChatToolCall } from '../../chat/types'\nimport { useChat } from '../../chat/use-chat'\nimport { createBuiltinTools } from '../../tools'\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/** 内置 + 用户工具(同名内置优先),供工具卡片解析 icon/label/render/autoCollapse */\nconst toolMap = computed(() => {\n const builtins = createBuiltinTools()\n const names = new Set(builtins.map((t) => t.name))\n const tools = [...builtins, ...(props.tools ?? []).filter((t) => !names.has(t.name))]\n return Object.fromEntries(tools.map((t) => [t.name, t]))\n})\n\nprovide(AiChatDIKey, { cls, slots, tools: toolMap })\n\nconst {\n messages,\n model,\n reasoningLevel,\n running,\n send,\n abort,\n regenerate,\n clear,\n respondToolCall\n} = useChat({ props, emit })\n\ndefineExpose<_AiChatExposed>({ send, abort, regenerate, clear })\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0CA,MAAM,QAAQ;EAEd,MAAM,OAAO;EASb,MAAM,MAAM,IAAI,SAAS;EAEzB,MAAM,QAAQ,SAAS;;EAGvB,MAAM,UAAU,eAAe;GAC7B,MAAM,WAAW,mBAAmB;GACpC,MAAM,QAAQ,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,IAAI,CAAC;GACjD,MAAM,QAAQ,CAAC,GAAG,UAAU,IAAI,MAAM,SAAS,CAAC,EAAA,CAAG,QAAQ,MAAM,CAAC,MAAM,IAAI,EAAE,IAAI,CAAC,CAAC;GACpF,OAAO,OAAO,YAAY,MAAM,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;EACzD,CAAC;EAED,QAAQ,aAAa;GAAE;GAAK;GAAO,OAAO;EAAQ,CAAC;EAEnD,MAAM,EACJ,UACA,OACA,gBACA,SACA,MACA,OACA,YACA,OACA,oBACE,QAAQ;GAAE;GAAO;EAAK,CAAC;EAE3B,SAA6B;GAAE;GAAM;GAAO;GAAY;EAAM,CAAC;;GA9E7D,OAAA,UAAA,GAAA,mBAwBM,OAAA,EAxBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,CAAC,EAAA,GAChB,CAAA,YAUc,aAAA;IATX,UAAU,MAAA,QAAA;IACV,SAAS,QAAA;IACT,kBAAgB,QAAA;IAChB,WAAS,MAAA,eAAA;IACT,cAAY,MAAA,UAAA;GAEGA,GAAAA,YAAAA,EAAAA,GAAAA,EAAAA,GAAAA,CAAAA,KAAAA,OAAO,UAAA;IAAU,MAAA;IACR,IAAA,cAAA,CAAvB,WAAuB,KAAA,QAAA,SAAA,CAAA,CAAA;;;;;;;;GAI3B,CAAA,GAAA,YAUE,aAAA;IATQ,OAAO,MAAA,KAAA;IAAA,kBAAA,OAAA,OAAA,OAAA,MAAA,WAAA,MAAA,KAAA,IAAA,MAAK,QAAA,SAAA;IACZ,mBAAiB,MAAA,cAAA;IAAA,2BAAA,OAAA,OAAA,OAAA,MAAA,WAAA,MAAA,cAAA,IAAA,eAAc,QAAA,SAAA;IACtC,SAAS,MAAA,OAAA;IACT,QAAQ,QAAA;IACR,aAAa,QAAA;IACb,QAAQ,QAAA;IACR,uBAAqB,QAAA;IACrB,QAAM,MAAA,IAAA;IACN,SAAO,MAAA,KAAA"}
|
|
1
|
+
{"version":3,"file":"ai-chat.js","names":["$slots"],"sources":["../../../src/components/ai-chat/ai-chat.vue"],"sourcesContent":["<template>\n <ULayout\n ref=\"layoutRef\"\n :class=\"[cls.b, bem.is('resizing', resizing)]\"\n :cols=\"layoutCols\"\n rows=\"1fr\"\n :resizable=\"panelOpen\"\n :col-min-sizes=\"panelOpen ? [MAIN_MIN_WIDTH, PANEL_MIN_WIDTH] : undefined\"\n @resize-start=\"resizing = true\"\n @resize-end=\"resizing = false\"\n >\n <div :class=\"[cls.e('main'), bem.is('empty', isEmpty)]\">\n <MessageList\n :messages=\"messages\"\n :welcome=\"welcome\"\n :running=\"running\"\n :renderer-props=\"rendererProps\"\n @respond=\"respondToolCall\"\n @regenerate=\"regenerate\"\n @welcome-click=\"(text) => handleSend(text, [])\"\n >\n <template v-if=\"$slots.welcome\" #welcome>\n <slot name=\"welcome\" />\n </template>\n </MessageList>\n\n <QueueList\n :queue=\"queue\"\n @start-now=\"startQueued\"\n @edit=\"handleQueueEdit\"\n @remove=\"removeQueued\"\n />\n\n <ChatInput\n ref=\"chatInputRef\"\n v-model:model=\"model\"\n v-model:reasoning-level=\"reasoningLevel\"\n :running=\"running\"\n :models=\"models\"\n :placeholder=\"placeholder\"\n :accept=\"accept\"\n :max-attachment-size=\"maxAttachmentSize\"\n @send=\"handleSend\"\n @abort=\"abort\"\n />\n </div>\n\n <!-- 侧边面板(renderTo: 'panel' 的工具渲染区),与会话区宽度经 ULayout 拖拽手柄调节 -->\n <SidePanel\n v-if=\"activePanelCall\"\n ref=\"panelRef\"\n :tool-call=\"activePanelCall\"\n @close=\"closePanel\"\n />\n </ULayout>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useResizeObserver } from '@veltra/compositions'\nimport { ULayout } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, provide, ref, useSlots, useTemplateRef, watch } from 'vue'\n\nimport type { ChatAttachment, ChatToolCall } from '../../chat/types'\nimport { useChat } from '../../chat/use-chat'\nimport { createBuiltinTools } from '../../tools'\nimport type { _AiChatExposed, AiChatEmits, AiChatProps } from '../../types'\nimport ChatInput from './chat-input.vue'\nimport { AiChatDIKey } from './di'\nimport MessageList from './message-list.vue'\nimport QueueList from './queue-list.vue'\nimport SidePanel from './side-panel.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/** 内置 + 用户工具(同名内置优先),供工具卡片解析 icon/label/render/renderTo/autoCollapse */\nconst toolMap = computed(() => {\n const builtins = createBuiltinTools()\n const names = new Set(builtins.map((t) => t.name))\n const tools = [...builtins, ...(props.tools ?? []).filter((t) => !names.has(t.name))]\n return Object.fromEntries(tools.map((t) => [t.name, t]))\n})\n\nconst {\n messages,\n model,\n reasoningLevel,\n running,\n queue,\n send,\n abort,\n regenerate,\n clear,\n respondToolCall,\n enqueue,\n startQueued,\n removeQueued\n} = useChat({ props, emit })\n\n/** 空会话(无可见消息):输入区垂直居中,欢迎区(orb)贴于输入区上方并与之左对齐 */\nconst isEmpty = computed(() => !messages.value.some((msg) => msg.role !== 'tool'))\n\n/** 所有 renderTo: 'panel' 的工具调用(按消息顺序,面板内容的来源) */\nconst panelCalls = computed(() => {\n const calls: ChatToolCall[] = []\n for (const msg of messages.value) {\n if (msg.role !== 'assistant' || !msg.toolCalls) continue\n for (const call of msg.toolCalls) {\n if (toolMap.value[call.name]?.renderTo === 'panel') calls.push(call)\n }\n }\n return calls\n})\n\n/** 当前展示在右侧面板的工具调用 id;null 表示面板关闭 */\nconst activePanelCallId = ref<string | null>(null)\n\nconst activePanelCall = computed(() => {\n if (!activePanelCallId.value) return null\n return panelCalls.value.find((call) => call.id === activePanelCallId.value) ?? null\n})\n\nconst panelOpen = computed(() => activePanelCall.value != null)\n\n// 出现新的面板工具调用时自动打开并聚焦到它\nwatch(\n () => panelCalls.value[panelCalls.value.length - 1]?.id,\n (id) => {\n if (id) activePanelCallId.value = id\n }\n)\n\n// 聚焦的调用消失(清空消息 / 重新生成)时,回落到最近一次面板调用或关闭\nwatch(panelCalls, (calls) => {\n const id = activePanelCallId.value\n if (id && !calls.some((call) => call.id === id)) {\n activePanelCallId.value = calls[calls.length - 1]?.id ?? null\n }\n})\n\n/** 打开面板并聚焦到指定工具调用(工具卡片「查看面板」入口,经 DI 下发) */\nconst openPanel = (toolCallId: string) => {\n if (panelCalls.value.some((call) => call.id === toolCallId)) {\n activePanelCallId.value = toolCallId\n }\n}\n\nconst closePanel = () => {\n activePanelCallId.value = null\n}\n\nprovide(AiChatDIKey, { cls, slots, tools: toolMap, openPanel })\n\n/**\n * 面板宽度:经 ULayout 列轨施加(cols = 1fr + 面板宽度),\n * 拖拽手柄由 ULayout 提供(colMinSizes 约束会话区 / 面板最小宽度)。\n */\nconst PANEL_MIN_WIDTH = 320\nconst MAIN_MIN_WIDTH = 360\n/** 面板打开时默认给会话区留的宽度(即面板默认尽可能大) */\nconst MAIN_DEFAULT_WIDTH = 860\n\nconst layoutRef = useTemplateRef<InstanceType<typeof ULayout>>('layoutRef')\nconst panelRef = useTemplateRef<InstanceType<typeof SidePanel>>('panelRef')\nconst panelWidth = ref(PANEL_MIN_WIDTH)\nconst resizing = ref(false)\n\nconst layoutCols = computed(() => {\n return panelOpen.value ? ['1fr', `${panelWidth.value}px`] : ['1fr']\n})\n\nconst containerWidth = () => (layoutRef.value?.$el as HTMLElement | undefined)?.clientWidth ?? 0\n\n/** 面板默认宽度:容器宽减去会话区默认保留宽度(缺省给会话区留 MAIN_DEFAULT_WIDTH) */\nconst defaultPanelWidth = () => {\n const width = containerWidth()\n return width ? width - MAIN_DEFAULT_WIDTH : 420\n}\n\n/** 钳位面板宽度在 [PANEL_MIN_WIDTH, 容器宽 - MAIN_MIN_WIDTH] 区间 */\nconst clampPanelWidth = (width: number) => {\n const width0 = containerWidth()\n const maxWidth = width0\n ? Math.max(PANEL_MIN_WIDTH, width0 - MAIN_MIN_WIDTH)\n : Number.MAX_SAFE_INTEGER\n return Math.min(Math.max(width, PANEL_MIN_WIDTH), maxWidth)\n}\n\n// 聚焦到指定了 panelWidth 的工具调用时应用其宽度;面板从关闭到打开且工具未指定宽度时\n// 应用默认宽(会话区留 860px);面板已打开时切换聚焦保持当前宽度(含用户拖拽结果)\nwatch(activePanelCall, (call, prevCall) => {\n if (!call) return\n const toolWidth = toolMap.value[call.name]?.panelWidth\n const width = toolWidth ?? (prevCall ? undefined : defaultPanelWidth())\n if (typeof width !== 'number') return\n panelWidth.value = clampPanelWidth(width)\n})\n\n// 跟踪面板实际宽度(含用户拖拽结果):面板关闭重开、切换到未指定宽度的工具时保持\nuseResizeObserver({\n targets: computed(() => (panelRef.value?.$el as HTMLElement | undefined) ?? undefined),\n onResize(entries) {\n // borderBoxSize 随通知携带(含边框、零布局开销),不读 offsetWidth/clientWidth\n const inlineSize = entries[0]?.borderBoxSize?.[0]?.inlineSize\n if (!inlineSize) return\n const width = Math.round(inlineSize)\n // 滞回:忽略亚像素/边框级的微小偏差(< 2px),阻断任何「观测→回写→再观测」的微缩反馈回路\n if (Math.abs(width - panelWidth.value) < 2) return\n panelWidth.value = width\n }\n})\n\nconst chatInputRef = useTemplateRef<InstanceType<typeof ChatInput>>('chatInputRef')\n\n/**\n * 编辑中的队列项原后继 id(编辑态标记):\n * undefined 表示无编辑;null 表示原位置在队尾;否则重新提交时插回该后继之前,\n * 使前后项顺序不受编辑影响(前项已执行则该项自然成为最前待执行项)。\n */\nconst editingSuccessor = ref<string | null | undefined>(undefined)\n\n/** 编辑队列项:取回输入框重新编辑;已有未完成编辑时先按锚点还原,避免内容丢失 */\nconst handleQueueEdit = (id: string) => {\n restoreEditing()\n\n const index = queue.value.findIndex((item) => item.id === id)\n if (index === -1) return\n editingSuccessor.value = queue.value[index + 1]?.id ?? null\n\n const item = removeQueued(id)\n if (item) chatInputRef.value?.setContent(item.content)\n}\n\n/** 将输入框中未提交的编辑内容按原锚点插回队列 */\nconst restoreEditing = () => {\n if (editingSuccessor.value === undefined) return\n const content = chatInputRef.value?.getContent().trim()\n if (content) enqueue(content, undefined, editingSuccessor.value ?? undefined)\n editingSuccessor.value = undefined\n}\n\nconst handleSend = (content: string, attachments: ChatAttachment[]) => {\n // 编辑态提交:插回原锚点位置(会话进行中则留在队列,空闲时由 enqueue 自动消耗队首)\n if (editingSuccessor.value !== undefined) {\n enqueue(content, attachments, editingSuccessor.value ?? undefined)\n editingSuccessor.value = undefined\n return\n }\n send(content, attachments)\n}\n\ndefineExpose<_AiChatExposed>({\n send,\n abort,\n regenerate,\n clear,\n queue,\n startQueued,\n removeQueued,\n enqueue\n})\n</script>\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AiChatDIKey } from "./di.js";
|
|
2
2
|
import _sfc_main$1 from "./model-picker.js";
|
|
3
|
-
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeModels, normalizeClass, openBlock, ref, renderList, shallowRef, unref, useModel, vModelText, withCtx, withDirectives, withKeys, withModifiers } from "vue";
|
|
3
|
+
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeModels, nextTick, normalizeClass, openBlock, ref, renderList, shallowRef, unref, useModel, vModelText, withCtx, withDirectives, withKeys, withModifiers } from "vue";
|
|
4
4
|
import { Attach, Close, Up } from "@veltra/icons/normal";
|
|
5
5
|
import { UButton, UFilePicker, UIcon } from "@veltra/desktop";
|
|
6
6
|
import { bem } from "@veltra/utils";
|
|
@@ -23,7 +23,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
23
23
|
"reasoningLevelModifiers": {}
|
|
24
24
|
}),
|
|
25
25
|
emits: /*@__PURE__*/ mergeModels(["send", "abort"], ["update:model", "update:reasoningLevel"]),
|
|
26
|
-
setup(__props, { emit: __emit }) {
|
|
26
|
+
setup(__props, { expose: __expose, emit: __emit }) {
|
|
27
27
|
const props = __props;
|
|
28
28
|
const model = useModel(__props, "model");
|
|
29
29
|
const reasoningLevel = useModel(__props, "reasoningLevel");
|
|
@@ -32,8 +32,13 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
32
32
|
const text = ref("");
|
|
33
33
|
const attachments = ref([]);
|
|
34
34
|
const textareaRef = shallowRef();
|
|
35
|
-
const
|
|
36
|
-
|
|
35
|
+
const hasContent = computed(() => !!text.value.trim() || attachments.value.length > 0);
|
|
36
|
+
/** 生成中且输入为空时显示停止;有内容则显示发送(入队) */
|
|
37
|
+
const showStop = computed(() => props.running && !hasContent.value);
|
|
38
|
+
/** 生成中提示用户消息将进入队列 */
|
|
39
|
+
const placeholderText = computed(() => {
|
|
40
|
+
if (props.running) return "会话进行中,发送的消息将进入待发送队列";
|
|
41
|
+
return props.placeholder ?? "输入消息,Enter 发送,Shift + Enter 换行";
|
|
37
42
|
});
|
|
38
43
|
/** 多行自适应高度,上限 160px */
|
|
39
44
|
const autoResize = () => {
|
|
@@ -43,12 +48,26 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
43
48
|
el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
|
|
44
49
|
};
|
|
45
50
|
const handleSend = () => {
|
|
46
|
-
if (!
|
|
51
|
+
if (!hasContent.value) return;
|
|
47
52
|
emit("send", text.value.trim(), attachments.value);
|
|
48
53
|
text.value = "";
|
|
49
54
|
attachments.value = [];
|
|
50
55
|
autoResize();
|
|
51
56
|
};
|
|
57
|
+
/** 取回内容到输入框(队列编辑场景),并聚焦 */
|
|
58
|
+
const setContent = (content) => {
|
|
59
|
+
text.value = content;
|
|
60
|
+
nextTick(() => {
|
|
61
|
+
autoResize();
|
|
62
|
+
textareaRef.value?.focus();
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
/** 读取当前输入框内容 */
|
|
66
|
+
const getContent = () => text.value;
|
|
67
|
+
__expose({
|
|
68
|
+
setContent,
|
|
69
|
+
getContent
|
|
70
|
+
});
|
|
52
71
|
/** 将拾取的文件转为 dataUrl 附件(超限忽略) */
|
|
53
72
|
const handlePick = (files) => {
|
|
54
73
|
const maxSize = props.maxAttachmentSize ?? 10485760;
|
|
@@ -94,7 +113,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
94
113
|
ref: textareaRef,
|
|
95
114
|
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => text.value = $event),
|
|
96
115
|
class: normalizeClass(unref(cls).e("input")),
|
|
97
|
-
placeholder:
|
|
116
|
+
placeholder: placeholderText.value,
|
|
98
117
|
rows: "1",
|
|
99
118
|
onKeydown: withKeys(withModifiers(handleSend, ["exact", "prevent"]), ["enter"]),
|
|
100
119
|
onInput: autoResize
|
|
@@ -114,7 +133,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
114
133
|
}, 8, ["class"])]),
|
|
115
134
|
_: 1
|
|
116
135
|
}, 8, ["accept"])], 2),
|
|
117
|
-
createCommentVNode(" 右簇:模型/推理选择 →
|
|
136
|
+
createCommentVNode(" 右簇:模型/推理选择 → 发送或停止(互斥) "),
|
|
118
137
|
createElementVNode("div", { class: normalizeClass(unref(cls).e("input-toolbar-right")) }, [__props.models?.length ? (openBlock(), createBlock(_sfc_main$1, {
|
|
119
138
|
key: 0,
|
|
120
139
|
model: model.value,
|
|
@@ -126,24 +145,30 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
126
145
|
"model",
|
|
127
146
|
"reasoning-level",
|
|
128
147
|
"models"
|
|
129
|
-
])) : createCommentVNode("v-if", true),
|
|
130
|
-
key:
|
|
148
|
+
])) : createCommentVNode("v-if", true), showStop.value ? (openBlock(), createBlock(unref(UButton), {
|
|
149
|
+
key: "stop",
|
|
131
150
|
size: "small",
|
|
132
151
|
type: "danger",
|
|
133
152
|
circle: "",
|
|
153
|
+
title: "停止生成",
|
|
134
154
|
onClick: _cache[3] || (_cache[3] = ($event) => emit("abort"))
|
|
135
155
|
}, {
|
|
136
156
|
default: withCtx(() => [createElementVNode("span", { class: normalizeClass(unref(cls).e("input-stop")) }, null, 2)]),
|
|
137
157
|
_: 1
|
|
138
158
|
})) : (openBlock(), createBlock(unref(UButton), {
|
|
139
|
-
key:
|
|
159
|
+
key: "send",
|
|
140
160
|
size: "small",
|
|
141
161
|
type: "primary",
|
|
142
162
|
circle: "",
|
|
143
|
-
disabled: !
|
|
163
|
+
disabled: !hasContent.value,
|
|
144
164
|
icon: unref(Up),
|
|
165
|
+
title: __props.running ? "加入待发送队列" : void 0,
|
|
145
166
|
onClick: handleSend
|
|
146
|
-
}, null, 8, [
|
|
167
|
+
}, null, 8, [
|
|
168
|
+
"disabled",
|
|
169
|
+
"icon",
|
|
170
|
+
"title"
|
|
171
|
+
]))], 2)
|
|
147
172
|
], 2)
|
|
148
173
|
], 2);
|
|
149
174
|
};
|
|
@@ -1 +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=\"
|
|
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=\"placeholderText\"\n rows=\"1\"\n @keydown.enter.exact.prevent=\"handleSend\"\n @input=\"autoResize\"\n />\n\n <div :class=\"cls.e('input-toolbar')\">\n <div :class=\"cls.e('input-toolbar-left')\">\n <UFilePicker multiple :accept=\"accept ?? 'image/*'\" @pick=\"handlePick\">\n <UIcon :class=\"cls.e('input-attach')\" title=\"添加图片\">\n <Attach />\n </UIcon>\n </UFilePicker>\n </div>\n\n <!-- 右簇:模型/推理选择 → 发送或停止(互斥) -->\n <div :class=\"cls.e('input-toolbar-right')\">\n <ModelPicker\n v-if=\"models?.length\"\n v-model:model=\"model\"\n v-model:reasoning-level=\"reasoningLevel\"\n :models=\"models\"\n />\n\n <UButton\n v-if=\"showStop\"\n key=\"stop\"\n size=\"small\"\n type=\"danger\"\n circle\n title=\"停止生成\"\n @click=\"emit('abort')\"\n >\n <span :class=\"cls.e('input-stop')\" />\n </UButton>\n <UButton\n v-else\n key=\"send\"\n size=\"small\"\n type=\"primary\"\n circle\n :disabled=\"!hasContent\"\n :icon=\"Up\"\n :title=\"running ? '加入待发送队列' : undefined\"\n @click=\"handleSend\"\n />\n </div>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UFilePicker, UIcon } from '@veltra/desktop'\nimport { Attach, Close, Up } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, ref, shallowRef } from 'vue'\n\nimport type { ChatAttachment } from '../../chat/types'\nimport type { ChatModelOption } from '../../providers'\nimport { AiChatDIKey } from './di'\nimport ModelPicker from './model-picker.vue'\n\ndefineOptions({ name: 'UAiChatInput' })\n\nconst props = defineProps<{\n /** 是否生成中(空输入显示停止,有内容则发送入队) */\n running: boolean\n /** 可选模型列表;有值则显示选择器 */\n models?: ChatModelOption[]\n /** 输入框占位文本 */\n placeholder?: string\n /** 附件 accept 类型 */\n accept?: string\n /** 单个附件最大字节数 */\n maxAttachmentSize?: number\n}>()\n\nconst model = defineModel<string>('model')\nconst reasoningLevel = defineModel<string>('reasoningLevel')\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>()\n\nconst hasContent = computed(() => !!text.value.trim() || attachments.value.length > 0)\n\n/** 生成中且输入为空时显示停止;有内容则显示发送(入队) */\nconst showStop = computed(() => props.running && !hasContent.value)\n\n/** 生成中提示用户消息将进入队列 */\nconst placeholderText = computed(() => {\n if (props.running) return '会话进行中,发送的消息将进入待发送队列'\n return props.placeholder ?? '输入消息,Enter 发送,Shift + Enter 换行'\n})\n\n/** 多行自适应高度,上限 160px */\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 (!hasContent.value) return\n emit('send', text.value.trim(), attachments.value)\n text.value = ''\n attachments.value = []\n autoResize()\n}\n\n/** 取回内容到输入框(队列编辑场景),并聚焦 */\nconst setContent = (content: string) => {\n text.value = content\n void nextTick(() => {\n autoResize()\n textareaRef.value?.focus()\n })\n}\n\n/** 读取当前输入框内容 */\nconst getContent = () => text.value\n\ndefineExpose({ setContent, getContent })\n\n/** 将拾取的文件转为 dataUrl 附件(超限忽略) */\nconst handlePick = (files: File[]) => {\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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"}
|
|
@@ -1 +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":";
|
|
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 /** 打开右侧面板并聚焦到指定工具调用(renderTo: 'panel' 工具的「查看面板」入口) */\n openPanel: (toolCallId: string) => void\n}\n\nexport const AiChatDIKey: InjectionKey<AiChatContext> = Symbol('AiChatDIKey')\n"],"mappings":";AAgBA,MAAa,cAA2C,OAAO,aAAa"}
|