@copilotkit/vue 2.0.0-rc.26 → 2.0.0-rc.28
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/CONTRIBUTOR_GUIDE.md +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{use-render-activity-message-BVZr0Z-G.js → use-render-activity-message-BLHkxK7Q.js} +5686 -5983
- package/dist/use-render-activity-message-BLHkxK7Q.js.map +1 -0
- package/dist/use-render-activity-message-d1Uz5YiB.cjs +113 -0
- package/dist/use-render-activity-message-d1Uz5YiB.cjs.map +1 -0
- package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts +3 -6
- package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatFeedbackCommentPopover.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +1 -16
- package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotThreadsDrawer.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/message-feedback.d.ts +20 -0
- package/dist/v2/components/chat/message-feedback.d.ts.map +1 -1
- package/dist/v2/hooks/use-configure-suggestions.d.ts.map +1 -1
- package/dist/v2/hooks/use-default-render-tool.d.ts +2 -7
- package/dist/v2/hooks/use-default-render-tool.d.ts.map +1 -1
- package/dist/v2/hooks/use-frontend-tool.d.ts +2 -2
- package/dist/v2/hooks/use-frontend-tool.d.ts.map +1 -1
- package/dist/v2/hooks/use-render-activity-message.d.ts.map +1 -1
- package/dist/v2/hooks/use-suggestions.d.ts.map +1 -1
- package/dist/v2/hooks/use-threads.d.ts +6 -15
- package/dist/v2/hooks/use-threads.d.ts.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/lib/vue-core.d.ts +16 -2
- package/dist/v2/lib/vue-core.d.ts.map +1 -1
- package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/__tests__/build-entries.test.ts +3 -1
- package/src/v2/components/a2ui/A2UIBuiltInToolCallRenderer.ts +4 -19
- package/src/v2/components/chat/CopilotChat.vue +50 -218
- package/src/v2/components/chat/CopilotChatAssistantMessage.vue +28 -141
- package/src/v2/components/chat/CopilotChatFeedbackCommentPopover.vue +11 -76
- package/src/v2/components/chat/CopilotChatInput.vue +82 -269
- package/src/v2/components/chat/CopilotChatMessageView.vue +14 -64
- package/src/v2/components/chat/CopilotChatReasoningMessage.vue +25 -52
- package/src/v2/components/chat/CopilotChatView.vue +92 -135
- package/src/v2/components/chat/CopilotPopupViewInternal.vue +18 -58
- package/src/v2/components/chat/CopilotSidebarViewInternal.vue +22 -45
- package/src/v2/components/chat/CopilotThreadsDrawer.vue +6 -3
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +43 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.pinToSend.test.ts +43 -1
- package/src/v2/components/chat/message-feedback.ts +33 -1
- package/src/v2/hooks/use-configure-suggestions.ts +3 -4
- package/src/v2/hooks/use-default-render-tool.ts +7 -68
- package/src/v2/hooks/use-frontend-tool.ts +10 -20
- package/src/v2/hooks/use-render-activity-message.ts +4 -2
- package/src/v2/hooks/use-suggestions.ts +9 -4
- package/src/v2/hooks/use-threads.ts +6 -15
- package/src/v2/lib/vue-core.ts +19 -2
- package/src/v2/providers/CopilotKitProvider.vue +107 -184
- package/src/v2/providers/__tests__/CopilotKitProvider.a2ui.test.ts +18 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.test.ts +35 -2
- package/dist/use-render-activity-message-8p2HwK2y.cjs +0 -114
- package/dist/use-render-activity-message-8p2HwK2y.cjs.map +0 -1
- package/dist/use-render-activity-message-BVZr0Z-G.js.map +0 -1
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
import { computed, nextTick, ref, watch } from "vue";
|
|
13
13
|
import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
|
|
14
14
|
import { CopilotChatDefaultLabels } from "../../providers/types";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
ɵMESSAGE_FEEDBACK_MAX_COMMENT_LENGTH as MESSAGE_FEEDBACK_MAX_COMMENT_LENGTH,
|
|
17
|
+
ɵisOwnFeedbackThumb,
|
|
18
|
+
} from "@copilotkit/core";
|
|
16
19
|
import type { ɵMessageFeedbackRating as MessageFeedbackRating } from "@copilotkit/core";
|
|
17
20
|
import CopilotPopover from "../ui/CopilotPopover.vue";
|
|
18
21
|
import { IconArrowUp } from "../icons";
|
|
@@ -38,60 +41,13 @@ const labels = computed(() => config.value?.labels ?? CopilotChatDefaultLabels);
|
|
|
38
41
|
const popover = ref<{ content: HTMLElement | null } | null>(null);
|
|
39
42
|
const contentElement = () => popover.value?.content ?? null;
|
|
40
43
|
|
|
41
|
-
// Elements Tab stops at.
|
|
42
|
-
const FOCUSABLE = [
|
|
43
|
-
"a[href]",
|
|
44
|
-
"button:not([disabled])",
|
|
45
|
-
"input:not([disabled])",
|
|
46
|
-
"select:not([disabled])",
|
|
47
|
-
"textarea:not([disabled])",
|
|
48
|
-
"[tabindex]",
|
|
49
|
-
]
|
|
50
|
-
.map((selector) => `${selector}:not([tabindex="-1"])`)
|
|
51
|
-
.join(", ");
|
|
52
|
-
|
|
53
|
-
/** Approximately the element Tab reaches after `element`, outside `skip`. */
|
|
54
|
-
function nextFocusable(
|
|
55
|
-
element: Element,
|
|
56
|
-
skip: Element | null,
|
|
57
|
-
): HTMLElement | null {
|
|
58
|
-
const all = Array.from(
|
|
59
|
-
element.ownerDocument.querySelectorAll<HTMLElement>(FOCUSABLE),
|
|
60
|
-
).filter(
|
|
61
|
-
(candidate) =>
|
|
62
|
-
candidate === element ||
|
|
63
|
-
(!skip?.contains(candidate) && !candidate.closest("[hidden], [inert]")),
|
|
64
|
-
);
|
|
65
|
-
const index = all.indexOf(element as HTMLElement);
|
|
66
|
-
return index === -1 ? null : (all[index + 1] ?? null);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const focusInside = () =>
|
|
70
|
-
contentElement()?.contains(document.activeElement) ?? false;
|
|
71
|
-
|
|
72
|
-
/** Whether `target` is a thumb of this message, whose click decides itself. */
|
|
73
|
-
const isOwnThumb = (target: EventTarget | null) => {
|
|
74
|
-
const thumb =
|
|
75
|
-
target instanceof Element
|
|
76
|
-
? target.closest("[data-copilotkit-rating]")
|
|
77
|
-
: null;
|
|
78
|
-
return (
|
|
79
|
-
thumb?.closest("[data-message-id]")?.getAttribute("data-message-id") ===
|
|
80
|
-
props.messageId
|
|
81
|
-
);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
44
|
// A keyboard rating moves focus into the field, as the popover opens or, when
|
|
85
45
|
// it was open already, as the field for the new rating renders.
|
|
86
|
-
const focusField = () => {
|
|
87
|
-
const input = props.comment.input.value;
|
|
88
|
-
if (input && props.comment.takeFocusRequest()) input.focus();
|
|
89
|
-
};
|
|
90
46
|
watch(
|
|
91
47
|
() => [props.comment.focusRequest.value, props.comment.open.value],
|
|
92
48
|
async () => {
|
|
93
49
|
await nextTick();
|
|
94
|
-
focusField();
|
|
50
|
+
props.comment.focusField();
|
|
95
51
|
},
|
|
96
52
|
// Immediate: the popover mounts as it opens, after the request was made.
|
|
97
53
|
{ flush: "post", immediate: true },
|
|
@@ -107,43 +63,25 @@ const changed = computed(
|
|
|
107
63
|
);
|
|
108
64
|
|
|
109
65
|
function onEscape(event: KeyboardEvent) {
|
|
110
|
-
const returnFocus = focusInside();
|
|
111
66
|
// Escape in the comment closes only the comment, not a chat popup around
|
|
112
67
|
// it; with focus elsewhere it also reaches that control.
|
|
113
|
-
if (
|
|
68
|
+
if (props.comment.popoverEscape(contentElement())) event.stopPropagation();
|
|
114
69
|
event.preventDefault();
|
|
115
|
-
props.comment.close({ save: false });
|
|
116
|
-
if (returnFocus) props.comment.returnFocus(props.anchor);
|
|
117
70
|
}
|
|
118
71
|
|
|
119
72
|
function onInteractOutside(event: Event) {
|
|
120
73
|
// A click on a thumb removes or switches the rating, so closing must not
|
|
121
74
|
// first save the draft as the answer to the rating it replaces.
|
|
122
|
-
if (
|
|
75
|
+
if (ɵisOwnFeedbackThumb(event.target, props.messageId))
|
|
76
|
+
event.preventDefault();
|
|
123
77
|
}
|
|
124
78
|
|
|
125
79
|
function onKeyDown(event: KeyboardEvent) {
|
|
126
|
-
|
|
127
|
-
const focusables = Array.from(
|
|
128
|
-
contentElement()?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [],
|
|
129
|
-
);
|
|
130
|
-
const leaving = event.shiftKey
|
|
131
|
-
? document.activeElement === focusables[0]
|
|
132
|
-
: document.activeElement === focusables[focusables.length - 1];
|
|
133
|
-
const anchor = props.anchor;
|
|
134
|
-
if (!leaving || !anchor?.isConnected) return;
|
|
135
|
-
event.preventDefault();
|
|
136
|
-
const target = event.shiftKey
|
|
137
|
-
? anchor
|
|
138
|
-
: (nextFocusable(anchor, contentElement()) ?? anchor);
|
|
139
|
-
props.comment.close();
|
|
140
|
-
if (target === anchor) props.comment.returnFocus(target);
|
|
141
|
-
else target.focus();
|
|
80
|
+
props.comment.popoverKeyDown(event, contentElement());
|
|
142
81
|
}
|
|
143
82
|
|
|
144
83
|
function onSubmit() {
|
|
145
|
-
props.comment.
|
|
146
|
-
props.comment.returnFocus(props.anchor);
|
|
84
|
+
props.comment.submit();
|
|
147
85
|
}
|
|
148
86
|
|
|
149
87
|
function onEditingChange(editing: boolean) {
|
|
@@ -151,10 +89,7 @@ function onEditingChange(editing: boolean) {
|
|
|
151
89
|
}
|
|
152
90
|
|
|
153
91
|
function onLeave(next: Element | null) {
|
|
154
|
-
|
|
155
|
-
// still on, does not close it.
|
|
156
|
-
if (next && (contentElement()?.contains(next) || isOwnThumb(next))) return;
|
|
157
|
-
props.comment.leave();
|
|
92
|
+
props.comment.popoverFocusLeave(next, contentElement(), props.messageId);
|
|
158
93
|
}
|
|
159
94
|
|
|
160
95
|
function onFormFocusOut(event: FocusEvent) {
|
|
@@ -10,14 +10,13 @@ import {
|
|
|
10
10
|
watch,
|
|
11
11
|
} from "vue";
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
ɵComposerController,
|
|
14
14
|
ɵcomposerButtonState,
|
|
15
15
|
ɵcreateComposerAutosize,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
ɵresolveSlashKey,
|
|
16
|
+
ɵflattenToolsMenuForDisplay,
|
|
17
|
+
ɵnormalizeToolsMenu,
|
|
18
|
+
ɵshowsComposerDisclaimer,
|
|
19
|
+
ɵsyncRecorderToMode,
|
|
21
20
|
} from "@copilotkit/core";
|
|
22
21
|
import {
|
|
23
22
|
IconArrowUp,
|
|
@@ -43,16 +42,6 @@ import { useChatLabels } from "./use-chat-labels";
|
|
|
43
42
|
defineOptions({ inheritAttrs: false });
|
|
44
43
|
|
|
45
44
|
type MenuEntry = ToolsMenuItem | "-";
|
|
46
|
-
type MenuDisplayEntry =
|
|
47
|
-
| { type: "separator"; key: string }
|
|
48
|
-
| { type: "label"; key: string; label: string; depth: number }
|
|
49
|
-
| {
|
|
50
|
-
type: "item";
|
|
51
|
-
key: string;
|
|
52
|
-
label: string;
|
|
53
|
-
depth: number;
|
|
54
|
-
action: () => void;
|
|
55
|
-
};
|
|
56
45
|
|
|
57
46
|
const props = withDefaults(
|
|
58
47
|
defineProps<{
|
|
@@ -124,8 +113,17 @@ const addButtonContainerRef = ref<HTMLElement | null>(null);
|
|
|
124
113
|
const actionsContainerRef = ref<HTMLElement | null>(null);
|
|
125
114
|
const slashMenuRef = ref<HTMLElement | null>(null);
|
|
126
115
|
const audioRecorderRef = ref<CopilotChatAudioRecorderRef | null>(null);
|
|
127
|
-
const localValue = ref(props.modelValue ?? "");
|
|
128
116
|
const isComposing = ref(false);
|
|
117
|
+
// The text, controlled or not, and the slash-command menu: see core's
|
|
118
|
+
// ɵComposerController.
|
|
119
|
+
const composer = new ɵComposerController<ToolsMenuItem>({
|
|
120
|
+
initialValue: props.modelValue ?? "",
|
|
121
|
+
onChange: (value) => emit("update:modelValue", value),
|
|
122
|
+
});
|
|
123
|
+
const composerState = shallowRef(composer.getSnapshot());
|
|
124
|
+
composer.subscribe(() => {
|
|
125
|
+
composerState.value = composer.getSnapshot();
|
|
126
|
+
});
|
|
129
127
|
// Sizes the text box and picks the compact or expanded layout.
|
|
130
128
|
const autosize = ɵcreateComposerAutosize({
|
|
131
129
|
getElements: () => ({
|
|
@@ -140,9 +138,6 @@ const layout = shallowRef(autosize.getSnapshot());
|
|
|
140
138
|
autosize.subscribe(() => {
|
|
141
139
|
layout.value = autosize.getSnapshot();
|
|
142
140
|
});
|
|
143
|
-
const commandQuery = ref<string | null>(null);
|
|
144
|
-
const slashHighlightIndex = ref(0);
|
|
145
|
-
const previousCommandQuery = ref<string | null>(null);
|
|
146
141
|
const addMenuOpen = ref(false);
|
|
147
142
|
// Where focus goes as the add menu opens: its first or last item for the
|
|
148
143
|
// keyboard, the menu itself for a pointer, as Radix's DropdownMenu does.
|
|
@@ -151,9 +146,13 @@ const addMenuAnchor = ref<HTMLElement | null>(null);
|
|
|
151
146
|
const addMenuId = useCopilotId("cpk-add-menu");
|
|
152
147
|
const addMenuTriggerId = `${addMenuId}-trigger`;
|
|
153
148
|
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
149
|
+
const inputValue = computed(() => composerState.value.value);
|
|
150
|
+
const filteredCommands = computed(() => composerState.value.slashCommands);
|
|
151
|
+
const slashHighlightIndex = computed(
|
|
152
|
+
() => composerState.value.slashHighlightIndex,
|
|
153
|
+
);
|
|
154
|
+
const slashMenuVisible = computed(
|
|
155
|
+
() => composerState.value.slashQuery !== null,
|
|
157
156
|
);
|
|
158
157
|
const labels = useChatLabels();
|
|
159
158
|
const resolvedPlaceholder = computed(
|
|
@@ -162,8 +161,11 @@ const resolvedPlaceholder = computed(
|
|
|
162
161
|
const isExpanded = computed(
|
|
163
162
|
() => props.mode === "input" && layout.value === "expanded",
|
|
164
163
|
);
|
|
165
|
-
const shouldShowDisclaimer = computed(
|
|
166
|
-
(
|
|
164
|
+
const shouldShowDisclaimer = computed(() =>
|
|
165
|
+
ɵshowsComposerDisclaimer({
|
|
166
|
+
showDisclaimer: props.showDisclaimer,
|
|
167
|
+
positioning: props.positioning,
|
|
168
|
+
}),
|
|
167
169
|
);
|
|
168
170
|
const hasSubmitAction = computed(
|
|
169
171
|
() => typeof props.onSubmitMessage === "function",
|
|
@@ -208,150 +210,58 @@ const rootAttrs = computed(() => {
|
|
|
208
210
|
return rest;
|
|
209
211
|
});
|
|
210
212
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (!hasAddFileAction.value) {
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
return {
|
|
222
|
-
label: labels.value.chatInputToolbarAddButtonLabel,
|
|
223
|
-
action: () => emit("add-file"),
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function normalizeMenuItems() {
|
|
228
|
-
const items: MenuEntry[] = [];
|
|
229
|
-
const addItem = createDefaultAddItem();
|
|
230
|
-
if (addItem) {
|
|
231
|
-
items.push(addItem);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (props.toolsMenu.length > 0) {
|
|
235
|
-
if (items.length > 0) {
|
|
236
|
-
items.push("-");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
for (const menuEntry of props.toolsMenu) {
|
|
240
|
-
if (menuEntry === "-") {
|
|
241
|
-
if (items.length === 0 || items[items.length - 1] === "-") {
|
|
242
|
-
continue;
|
|
213
|
+
const menuItems = computed(() =>
|
|
214
|
+
ɵnormalizeToolsMenu<ToolsMenuItem>(
|
|
215
|
+
props.toolsMenu,
|
|
216
|
+
hasAddFileAction.value
|
|
217
|
+
? {
|
|
218
|
+
label: labels.value.chatInputToolbarAddButtonLabel,
|
|
219
|
+
action: () => emit("add-file"),
|
|
243
220
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
items.push(menuEntry);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
while (items.length > 0 && items[items.length - 1] === "-") {
|
|
253
|
-
items.pop();
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return items;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const menuItems = computed(() => normalizeMenuItems());
|
|
221
|
+
: undefined,
|
|
222
|
+
),
|
|
223
|
+
);
|
|
260
224
|
const hasMenuItems = computed(() => menuItems.value.length > 0);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
function flattenMenuDisplay(items: MenuEntry[], depth = 0, prefix = "root") {
|
|
265
|
-
const entries: MenuDisplayEntry[] = [];
|
|
266
|
-
for (const [index, item] of items.entries()) {
|
|
267
|
-
const key = `${prefix}-${index}`;
|
|
268
|
-
if (item === "-") {
|
|
269
|
-
entries.push({ type: "separator", key: `sep-${key}` });
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if (isMenuGroup(item) && item.items.length > 0) {
|
|
273
|
-
entries.push({
|
|
274
|
-
type: "label",
|
|
275
|
-
key: `label-${key}`,
|
|
276
|
-
label: item.label,
|
|
277
|
-
depth,
|
|
278
|
-
});
|
|
279
|
-
entries.push(...flattenMenuDisplay(item.items, depth + 1, key));
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
if (item.action) {
|
|
283
|
-
entries.push({
|
|
284
|
-
type: "item",
|
|
285
|
-
key: `item-${key}`,
|
|
286
|
-
label: item.label,
|
|
287
|
-
depth,
|
|
288
|
-
action: item.action,
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
return entries;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const menuDisplayItems = computed(() => flattenMenuDisplay(menuItems.value));
|
|
296
|
-
|
|
297
|
-
const filteredCommands = computed(() =>
|
|
298
|
-
ɵfilterCommands(commandItems.value, commandQuery.value),
|
|
225
|
+
const menuDisplayItems = computed(() =>
|
|
226
|
+
ɵflattenToolsMenuForDisplay(menuItems.value),
|
|
299
227
|
);
|
|
300
228
|
|
|
301
|
-
|
|
302
|
-
() =>
|
|
229
|
+
watch(
|
|
230
|
+
[() => props.modelValue, () => props.mode, menuItems],
|
|
231
|
+
([value, mode, menu]) => composer.setInputs({ value, mode, menu }),
|
|
232
|
+
{ immediate: true },
|
|
303
233
|
);
|
|
304
234
|
|
|
305
235
|
function updateInputValue(nextValue: string) {
|
|
306
|
-
|
|
307
|
-
localValue.value = nextValue;
|
|
308
|
-
}
|
|
309
|
-
emit("update:modelValue", nextValue);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function clearInputValue() {
|
|
313
|
-
updateInputValue("");
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function updateSlashState(value: string) {
|
|
317
|
-
commandQuery.value = ɵparseSlashQuery(value, commandItems.value.length > 0);
|
|
236
|
+
composer.setValue(nextValue);
|
|
318
237
|
}
|
|
319
238
|
|
|
320
|
-
function
|
|
321
|
-
clearInputValue();
|
|
322
|
-
command.action?.();
|
|
323
|
-
commandQuery.value = null;
|
|
324
|
-
slashHighlightIndex.value = 0;
|
|
239
|
+
function focusTextareaSoon() {
|
|
325
240
|
requestAnimationFrame(() => {
|
|
326
241
|
textareaRef.value?.focus();
|
|
327
242
|
});
|
|
328
243
|
}
|
|
329
244
|
|
|
245
|
+
function runCommand(command: ToolsMenuItem) {
|
|
246
|
+
composer.runCommand(command);
|
|
247
|
+
focusTextareaSoon();
|
|
248
|
+
}
|
|
249
|
+
|
|
330
250
|
function submit() {
|
|
331
251
|
if (props.mode !== "input" || props.disabled || !hasSubmitAction.value) {
|
|
332
252
|
return;
|
|
333
253
|
}
|
|
334
254
|
// In controlled mode, parent-updated modelValue can lag one tick behind
|
|
335
255
|
// the actual textarea value during intense runtime/connect churn.
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
emit("submit-message", trimmed);
|
|
343
|
-
|
|
344
|
-
if (props.clearOnSubmit) {
|
|
345
|
-
clearInputValue();
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
textareaRef.value?.focus();
|
|
256
|
+
const sent = composer.submit((text) => emit("submit-message", text), {
|
|
257
|
+
value: textareaRef.value?.value ?? inputValue.value,
|
|
258
|
+
clear: props.clearOnSubmit,
|
|
259
|
+
});
|
|
260
|
+
if (sent) textareaRef.value?.focus();
|
|
349
261
|
}
|
|
350
262
|
|
|
351
263
|
function handleInput(event: Event) {
|
|
352
|
-
|
|
353
|
-
updateInputValue(nextValue);
|
|
354
|
-
updateSlashState(nextValue);
|
|
264
|
+
updateInputValue((event.target as HTMLTextAreaElement).value);
|
|
355
265
|
}
|
|
356
266
|
|
|
357
267
|
// The button stops a run whatever the input holds. Enter differs: see
|
|
@@ -367,48 +277,28 @@ function handleSendButtonClick() {
|
|
|
367
277
|
}
|
|
368
278
|
|
|
369
279
|
function handleKeydown(event: KeyboardEvent) {
|
|
370
|
-
if (props.disabled) {
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (isComposing.value || event.isComposing || event.keyCode === 229) {
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const slash = ɵresolveSlashKey(event, {
|
|
379
|
-
query: props.mode === "input" ? commandQuery.value : null,
|
|
380
|
-
commands: filteredCommands.value,
|
|
381
|
-
highlightIndex: slashHighlightIndex.value,
|
|
382
|
-
});
|
|
383
|
-
if (slash.type !== "none") event.preventDefault();
|
|
384
|
-
switch (slash.type) {
|
|
385
|
-
case "highlight":
|
|
386
|
-
slashHighlightIndex.value = slash.index;
|
|
387
|
-
return;
|
|
388
|
-
case "run":
|
|
389
|
-
runCommand(slash.command);
|
|
390
|
-
return;
|
|
391
|
-
case "close":
|
|
392
|
-
commandQuery.value = null;
|
|
393
|
-
return;
|
|
394
|
-
case "none":
|
|
395
|
-
break;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (props.mode !== "input") {
|
|
280
|
+
if (props.disabled || props.mode !== "input") {
|
|
399
281
|
return;
|
|
400
282
|
}
|
|
401
283
|
|
|
402
284
|
// Enter with text sends, also while a run is going. Enter in an empty input
|
|
403
285
|
// stops the run. Read the textarea: a controlled modelValue can lag a tick.
|
|
404
|
-
const action =
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
286
|
+
const { preventDefault, action } = composer.keyDown(
|
|
287
|
+
{
|
|
288
|
+
key: event.key,
|
|
289
|
+
shiftKey: event.shiftKey,
|
|
290
|
+
isComposing: isComposing.value || event.isComposing,
|
|
291
|
+
keyCode: event.keyCode,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
...composerInput.value,
|
|
295
|
+
value: textareaRef.value?.value ?? inputValue.value,
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
if (preventDefault) event.preventDefault();
|
|
299
|
+
if (action === "command") {
|
|
300
|
+
focusTextareaSoon();
|
|
301
|
+
} else if (action === "send") {
|
|
412
302
|
submit();
|
|
413
303
|
} else if (action === "stop") {
|
|
414
304
|
emit("stop");
|
|
@@ -523,19 +413,6 @@ function handleContainerClick(event: MouseEvent) {
|
|
|
523
413
|
textareaRef.value?.focus();
|
|
524
414
|
}
|
|
525
415
|
|
|
526
|
-
watch(
|
|
527
|
-
() => props.modelValue,
|
|
528
|
-
(next) => {
|
|
529
|
-
if (isControlled.value) {
|
|
530
|
-
localValue.value = next ?? "";
|
|
531
|
-
}
|
|
532
|
-
},
|
|
533
|
-
);
|
|
534
|
-
|
|
535
|
-
watch(inputValue, (value) => {
|
|
536
|
-
updateSlashState(value);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
416
|
watch(
|
|
540
417
|
[inputValue, () => props.mode],
|
|
541
418
|
([value, mode]) => {
|
|
@@ -551,88 +428,24 @@ watch(
|
|
|
551
428
|
},
|
|
552
429
|
);
|
|
553
430
|
|
|
554
|
-
watch(commandItems, () => {
|
|
555
|
-
updateSlashState(inputValue.value);
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
watch(
|
|
559
|
-
[commandQuery, () => filteredCommands.value.length],
|
|
560
|
-
([query, filteredCount]) => {
|
|
561
|
-
if (
|
|
562
|
-
query !== null &&
|
|
563
|
-
query !== previousCommandQuery.value &&
|
|
564
|
-
filteredCount > 0
|
|
565
|
-
) {
|
|
566
|
-
slashHighlightIndex.value = 0;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
previousCommandQuery.value = query;
|
|
570
|
-
},
|
|
571
|
-
{ immediate: true },
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
watch(
|
|
575
|
-
[commandQuery, filteredCommands],
|
|
576
|
-
() => {
|
|
577
|
-
slashHighlightIndex.value =
|
|
578
|
-
commandQuery.value === null
|
|
579
|
-
? 0
|
|
580
|
-
: ɵclampSlashIndex(
|
|
581
|
-
slashHighlightIndex.value,
|
|
582
|
-
filteredCommands.value.length,
|
|
583
|
-
);
|
|
584
|
-
},
|
|
585
|
-
{ immediate: true },
|
|
586
|
-
);
|
|
587
|
-
|
|
588
431
|
watch(
|
|
589
432
|
() => props.mode,
|
|
590
|
-
|
|
433
|
+
(mode) => {
|
|
591
434
|
if (mode !== "input") {
|
|
592
|
-
commandQuery.value = null;
|
|
593
435
|
closeAddMenu();
|
|
594
436
|
}
|
|
595
|
-
|
|
596
|
-
const recorder = audioRecorderRef.value;
|
|
597
|
-
if (!recorder) {
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
if (mode === "transcribe") {
|
|
602
|
-
try {
|
|
603
|
-
await recorder.start();
|
|
604
|
-
} catch (error) {
|
|
605
|
-
console.error(error);
|
|
606
|
-
}
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
if (recorder.state === "recording") {
|
|
611
|
-
try {
|
|
612
|
-
await recorder.stop();
|
|
613
|
-
} catch {
|
|
614
|
-
// ignore transition stop failures
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
437
|
},
|
|
618
|
-
{ immediate: true },
|
|
619
438
|
);
|
|
620
439
|
|
|
440
|
+
// Recording follows the mode: it starts in transcribe mode and stops when the
|
|
441
|
+
// mode changes. The recorder renders only in transcribe mode, so also sync
|
|
442
|
+
// when it appears.
|
|
621
443
|
watch(
|
|
622
|
-
audioRecorderRef,
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
if (recorder.state === "idle") {
|
|
628
|
-
try {
|
|
629
|
-
await recorder.start();
|
|
630
|
-
} catch (error) {
|
|
631
|
-
console.error(error);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
444
|
+
[audioRecorderRef, () => props.mode],
|
|
445
|
+
([recorder, mode]) => {
|
|
446
|
+
void ɵsyncRecorderToMode(recorder, mode);
|
|
634
447
|
},
|
|
635
|
-
{ immediate: true },
|
|
448
|
+
{ immediate: true, flush: "post" },
|
|
636
449
|
);
|
|
637
450
|
|
|
638
451
|
watch(slashHighlightIndex, async (index) => {
|
|
@@ -846,7 +659,7 @@ onBeforeUnmount(() => {
|
|
|
846
659
|
index === slashHighlightIndex ? 'true' : undefined
|
|
847
660
|
"
|
|
848
661
|
class="cpk-chat-input-slash-menu-item"
|
|
849
|
-
@mouseenter="
|
|
662
|
+
@mouseenter="composer.highlight(index)"
|
|
850
663
|
@mousedown.prevent="runCommand(command)"
|
|
851
664
|
>
|
|
852
665
|
{{ command.label }}
|