@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
|
@@ -20,7 +20,6 @@ import type {
|
|
|
20
20
|
UserMessage,
|
|
21
21
|
} from "@ag-ui/core";
|
|
22
22
|
import {
|
|
23
|
-
DEFAULT_AGENT_ID,
|
|
24
23
|
commitRowKeyStore,
|
|
25
24
|
createRowKeyStore,
|
|
26
25
|
resolveRowRenderKeysById,
|
|
@@ -29,8 +28,10 @@ import {
|
|
|
29
28
|
ɵdeduplicateMessages,
|
|
30
29
|
ɵcreateMessageViewModel,
|
|
31
30
|
ɵduplicateMessagesWarning,
|
|
31
|
+
ɵgetCustomMessageRenderProps,
|
|
32
32
|
ɵgetToolCallRendererNames,
|
|
33
33
|
ɵresolveActivityRenderer,
|
|
34
|
+
ɵresolveAgentId,
|
|
34
35
|
ɵresolveCustomMessageRenderers,
|
|
35
36
|
} from "@copilotkit/core";
|
|
36
37
|
import type {
|
|
@@ -156,8 +157,8 @@ const stateTick = ref(0);
|
|
|
156
157
|
const interruptState = ref<InterruptSlotProps | null>(null);
|
|
157
158
|
const componentSlots = useSlots() as Record<string, (props?: any) => unknown>;
|
|
158
159
|
const forwardedSlotNames = computed(() => Object.keys(componentSlots));
|
|
159
|
-
const resolvedAgentId = computed(
|
|
160
|
-
(
|
|
160
|
+
const resolvedAgentId = computed(() =>
|
|
161
|
+
ɵresolveAgentId({ chatAgentId: config.value?.agentId }),
|
|
161
162
|
);
|
|
162
163
|
const resolvedThreadAgent = computed(() => {
|
|
163
164
|
// Read the registry so this recomputes when agents are added or replaced.
|
|
@@ -343,71 +344,20 @@ watch(
|
|
|
343
344
|
{ immediate: true },
|
|
344
345
|
);
|
|
345
346
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
): Omit<MessageMetaProps, "position" | "message"> {
|
|
349
|
-
stateTick.value += 0;
|
|
350
|
-
const agentId = resolvedAgentId.value;
|
|
351
|
-
const threadId = config.value?.threadId ?? "";
|
|
352
|
-
const core = copilotkit.value;
|
|
353
|
-
|
|
354
|
-
const resolvedRunId =
|
|
355
|
-
core.getRunIdForMessage(agentId, threadId, message.id) ??
|
|
356
|
-
core.getRunIdsForThread(agentId, threadId).slice(-1)[0];
|
|
357
|
-
const runId = resolvedRunId ?? `missing-run-id:${message.id}`;
|
|
358
|
-
|
|
359
|
-
const agent = resolvedThreadAgent.value ?? core.getAgent(agentId);
|
|
360
|
-
|
|
361
|
-
const messageIdsInRun =
|
|
362
|
-
resolvedRunId && agent
|
|
363
|
-
? agent.messages
|
|
364
|
-
.filter(
|
|
365
|
-
(msg) =>
|
|
366
|
-
core.getRunIdForMessage(agentId, threadId, msg.id) ===
|
|
367
|
-
resolvedRunId,
|
|
368
|
-
)
|
|
369
|
-
.map((msg) => msg.id)
|
|
370
|
-
: [message.id];
|
|
371
|
-
|
|
372
|
-
const rawMessageIndex = agent
|
|
373
|
-
? agent.messages.findIndex((msg) => msg.id === message.id)
|
|
374
|
-
: -1;
|
|
375
|
-
const messageIndex = rawMessageIndex >= 0 ? rawMessageIndex : 0;
|
|
376
|
-
const messageIndexInRun = resolvedRunId
|
|
377
|
-
? Math.max(messageIdsInRun.indexOf(message.id), 0)
|
|
378
|
-
: 0;
|
|
379
|
-
const numberOfMessagesInRun = resolvedRunId ? messageIdsInRun.length : 1;
|
|
380
|
-
|
|
381
|
-
// `getStateByRun` deep-clones the whole run state, so resolve it only when
|
|
382
|
-
// a consumer reads `stateSnapshot`, and at most once per meta object.
|
|
383
|
-
let stateSnapshot: unknown;
|
|
384
|
-
let stateSnapshotResolved = false;
|
|
385
|
-
|
|
386
|
-
return {
|
|
387
|
-
runId,
|
|
388
|
-
messageIndex,
|
|
389
|
-
messageIndexInRun,
|
|
390
|
-
numberOfMessagesInRun,
|
|
391
|
-
agentId,
|
|
392
|
-
get stateSnapshot() {
|
|
393
|
-
if (!stateSnapshotResolved) {
|
|
394
|
-
stateSnapshotResolved = true;
|
|
395
|
-
stateSnapshot = resolvedRunId
|
|
396
|
-
? core.getStateByRun(agentId, threadId, resolvedRunId)
|
|
397
|
-
: undefined;
|
|
398
|
-
}
|
|
399
|
-
return stateSnapshot;
|
|
400
|
-
},
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// `Object.assign` onto the meta object keeps the lazy `stateSnapshot` getter.
|
|
405
|
-
// A spread would call it.
|
|
347
|
+
// Core builds the props with a lazy `stateSnapshot` getter, so slots that
|
|
348
|
+
// never read it do not clone the run state.
|
|
406
349
|
function getMessageSlotProps(
|
|
407
350
|
message: Message,
|
|
408
351
|
position: MessageMetaProps["position"],
|
|
409
352
|
): MessageMetaProps {
|
|
410
|
-
|
|
353
|
+
stateTick.value += 0;
|
|
354
|
+
return ɵgetCustomMessageRenderProps(copilotkit.value, {
|
|
355
|
+
agentId: resolvedAgentId.value,
|
|
356
|
+
threadId: config.value?.threadId ?? "",
|
|
357
|
+
agent: resolvedThreadAgent.value,
|
|
358
|
+
message,
|
|
359
|
+
position,
|
|
360
|
+
});
|
|
411
361
|
}
|
|
412
362
|
|
|
413
363
|
// Calls a `#message-before` / `#message-after` slot with the meta object
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onBeforeUnmount,
|
|
2
|
+
import { computed, onBeforeUnmount, shallowRef, watch } from "vue";
|
|
3
3
|
import type { Message, ReasoningMessage } from "@ag-ui/core";
|
|
4
4
|
import { IconChevronRight } from "../icons";
|
|
5
5
|
import StreamingMarkdownDefault from "./StreamingMarkdownDefault.vue";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ɵgetReasoningHeaderLabel,
|
|
8
|
+
ɵReasoningDisclosure,
|
|
9
|
+
} from "@copilotkit/core";
|
|
7
10
|
import { useChatLabels } from "./use-chat-labels";
|
|
8
11
|
|
|
9
12
|
const props = withDefaults(
|
|
@@ -78,66 +81,36 @@ const isLatest = computed(
|
|
|
78
81
|
);
|
|
79
82
|
const isStreaming = computed(() => !!(props.isRunning && isLatest.value));
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function updateElapsedNow() {
|
|
95
|
-
if (startTimeMs !== null) {
|
|
96
|
-
elapsed.value = (Date.now() - startTimeMs) / 1000;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
watch(
|
|
101
|
-
isStreaming,
|
|
102
|
-
(nextIsStreaming) => {
|
|
103
|
-
if (nextIsStreaming) {
|
|
104
|
-
if (startTimeMs === null) {
|
|
105
|
-
startTimeMs = Date.now();
|
|
106
|
-
}
|
|
107
|
-
clearElapsedInterval();
|
|
108
|
-
elapsedInterval = setInterval(updateElapsedNow, 1000);
|
|
109
|
-
userToggledDuringStreaming.value = false;
|
|
110
|
-
isOpen.value = true;
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
clearElapsedInterval();
|
|
115
|
-
updateElapsedNow();
|
|
116
|
-
if (!userToggledDuringStreaming.value) {
|
|
117
|
-
isOpen.value = false;
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
{ immediate: true },
|
|
121
|
-
);
|
|
122
|
-
|
|
84
|
+
// Open while streaming, auto-collapse when it ends unless the user toggled,
|
|
85
|
+
// and the elapsed time: see core's ɵReasoningDisclosure.
|
|
86
|
+
const disclosure = new ɵReasoningDisclosure({ isStreaming: isStreaming.value });
|
|
87
|
+
const disclosureState = shallowRef(disclosure.getSnapshot());
|
|
88
|
+
const unsubscribe = disclosure.subscribe(() => {
|
|
89
|
+
disclosureState.value = disclosure.getSnapshot();
|
|
90
|
+
});
|
|
91
|
+
watch(isStreaming, (next) => disclosure.setStreaming(next), {
|
|
92
|
+
immediate: true,
|
|
93
|
+
});
|
|
123
94
|
onBeforeUnmount(() => {
|
|
124
|
-
|
|
95
|
+
unsubscribe();
|
|
96
|
+
disclosure.dispose();
|
|
125
97
|
});
|
|
126
98
|
|
|
99
|
+
const elapsed = computed(() => disclosureState.value.elapsedSeconds);
|
|
100
|
+
const isOpen = computed(() => disclosureState.value.isOpen);
|
|
101
|
+
|
|
127
102
|
const labels = useChatLabels();
|
|
128
103
|
/** `labels.reasoningMessageThinkingLabel` while streaming, then "Thought for …". */
|
|
129
104
|
const label = computed(() =>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}),
|
|
105
|
+
ɵgetReasoningHeaderLabel(
|
|
106
|
+
{ isStreaming: isStreaming.value, elapsedSeconds: elapsed.value },
|
|
107
|
+
labels.value,
|
|
108
|
+
),
|
|
135
109
|
);
|
|
136
110
|
|
|
137
111
|
function toggleOpen() {
|
|
138
112
|
if (!hasContent.value) return;
|
|
139
|
-
|
|
140
|
-
isOpen.value = !isOpen.value;
|
|
113
|
+
disclosure.toggle();
|
|
141
114
|
}
|
|
142
115
|
</script>
|
|
143
116
|
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
computed,
|
|
4
|
-
nextTick,
|
|
5
|
-
onBeforeUnmount,
|
|
6
|
-
onMounted,
|
|
7
|
-
ref,
|
|
8
|
-
useSlots,
|
|
9
|
-
watch,
|
|
10
|
-
} from "vue";
|
|
2
|
+
import { computed, ref, useSlots, watch } from "vue";
|
|
11
3
|
import type { Message } from "@ag-ui/core";
|
|
12
4
|
import {
|
|
5
|
+
ɵcreateStickToBottom,
|
|
13
6
|
ɵgetChatViewState,
|
|
14
|
-
|
|
7
|
+
ɵisContentScrolledToBottom,
|
|
15
8
|
ɵnormalizeAutoScroll,
|
|
9
|
+
ɵwatchElementHeight,
|
|
16
10
|
} from "@copilotkit/core";
|
|
17
|
-
import type { Suggestion } from "@copilotkit/core";
|
|
11
|
+
import type { Suggestion, ɵStickToBottom } from "@copilotkit/core";
|
|
18
12
|
import type { Attachment } from "@copilotkit/shared";
|
|
19
13
|
import { useChatLabels } from "./use-chat-labels";
|
|
20
14
|
import { useKeyboardHeight } from "../../hooks/use-keyboard-height";
|
|
@@ -138,6 +132,9 @@ const spacerRef = ref<HTMLElement | null>(null);
|
|
|
138
132
|
const inputContainerRef = ref<HTMLElement | null>(null);
|
|
139
133
|
const inputContainerHeight = ref(0);
|
|
140
134
|
const isAtBottom = ref(true);
|
|
135
|
+
// The end of the messages is in view. Without following ("pin-to-send" and
|
|
136
|
+
// "none"), this, not the scroll height, decides the scroll-to-bottom button.
|
|
137
|
+
const isContentAtBottom = ref(true);
|
|
141
138
|
const isControlledInput = computed(() => props.inputValue !== undefined);
|
|
142
139
|
const localInputValue = ref(props.inputValue ?? "");
|
|
143
140
|
|
|
@@ -212,126 +209,111 @@ const messagePaddingBottom = computed(
|
|
|
212
209
|
() => `${viewState.value.paddingBottom}px`,
|
|
213
210
|
);
|
|
214
211
|
const showScrollToBottomButton = computed(
|
|
215
|
-
() =>
|
|
212
|
+
() =>
|
|
213
|
+
!shouldShowWelcomeScreen.value &&
|
|
214
|
+
!(isPinToBottomMode.value ? isAtBottom.value : isContentAtBottom.value),
|
|
216
215
|
);
|
|
217
216
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
inputResizeObserver = new ResizeObserver((entries) => {
|
|
231
|
-
const wasAtBottom = isAtBottom.value;
|
|
232
|
-
const measured = entries[0]?.contentRect?.height;
|
|
233
|
-
if (typeof measured === "number") {
|
|
234
|
-
inputContainerHeight.value = measured;
|
|
235
|
-
} else {
|
|
236
|
-
syncInputContainerHeight();
|
|
237
|
-
}
|
|
238
|
-
updateIsAtBottom();
|
|
239
|
-
if (isPinToBottomMode.value && wasAtBottom) {
|
|
240
|
-
scrollToBottom("auto");
|
|
217
|
+
// Core keeps the list at the bottom while content arrives in `pin-to-bottom`
|
|
218
|
+
// mode, and tracks the position for the scroll-to-bottom button in every
|
|
219
|
+
// mode. It attaches when the scroll container mounts, also after the welcome
|
|
220
|
+
// screen gives way to the chat.
|
|
221
|
+
let stickToBottom: ɵStickToBottom | null = null;
|
|
222
|
+
watch(
|
|
223
|
+
[scrollContainerRef, scrollContentRef],
|
|
224
|
+
([scrollElement, contentElement], _old, onCleanup) => {
|
|
225
|
+
if (!scrollElement) {
|
|
226
|
+
isAtBottom.value = true;
|
|
227
|
+
return;
|
|
241
228
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
229
|
+
const controller = ɵcreateStickToBottom({
|
|
230
|
+
scrollElement,
|
|
231
|
+
contentElement: contentElement ?? scrollElement,
|
|
232
|
+
follow: isPinToBottomMode.value,
|
|
233
|
+
onChange: (value) => {
|
|
234
|
+
isAtBottom.value = value;
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
stickToBottom = controller;
|
|
238
|
+
isAtBottom.value = controller.isAtBottom;
|
|
239
|
+
onCleanup(() => {
|
|
240
|
+
controller.dispose();
|
|
241
|
+
if (stickToBottom === controller) stickToBottom = null;
|
|
242
|
+
});
|
|
243
|
+
},
|
|
244
|
+
{ flush: "post" },
|
|
245
|
+
);
|
|
254
246
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
247
|
+
watch(isPinToBottomMode, (follow) => stickToBottom?.setFollow(follow));
|
|
248
|
+
|
|
249
|
+
// Without following, the button shows while messages sit below the viewport.
|
|
250
|
+
// That is the end of the messages, not the scroll height: in "pin-to-send"
|
|
251
|
+
// mode an empty spacer under them lets a sent message reach the top, and
|
|
252
|
+
// scrolling into it would only clear the screen (parity with React).
|
|
253
|
+
function checkContentAtBottom() {
|
|
254
|
+
const scrollElement = scrollContainerRef.value;
|
|
255
|
+
const contentElement = scrollContentRef.value;
|
|
256
|
+
isContentAtBottom.value =
|
|
257
|
+
!scrollElement ||
|
|
258
|
+
!contentElement ||
|
|
259
|
+
ɵisContentScrolledToBottom(scrollElement, contentElement);
|
|
263
260
|
}
|
|
264
261
|
|
|
265
|
-
// When the welcome screen disappears and the main chat view (with the input
|
|
266
|
-
// overlay) appears, the template-ref elements become non-null. Re-attach
|
|
267
|
-
// observers that were skipped during the initial mount.
|
|
268
|
-
watch(shouldShowWelcomeScreen, async (show) => {
|
|
269
|
-
if (show) return;
|
|
270
|
-
await nextTick();
|
|
271
|
-
syncInputContainerHeight();
|
|
272
|
-
attachInputOverlayObserver();
|
|
273
|
-
attachScrollContentObserver();
|
|
274
|
-
});
|
|
275
|
-
|
|
276
262
|
watch(
|
|
277
|
-
|
|
278
|
-
(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
263
|
+
[scrollContainerRef, scrollContentRef],
|
|
264
|
+
([scrollElement, contentElement], _old, onCleanup) => {
|
|
265
|
+
checkContentAtBottom();
|
|
266
|
+
if (!scrollElement) return;
|
|
267
|
+
scrollElement.addEventListener("scroll", checkContentAtBottom);
|
|
268
|
+
// A reply that streams past the bottom needs the button before anyone
|
|
269
|
+
// scrolls, so check again when the viewport or the messages resize.
|
|
270
|
+
const resizeObserver =
|
|
271
|
+
typeof ResizeObserver === "undefined"
|
|
272
|
+
? null
|
|
273
|
+
: new ResizeObserver(checkContentAtBottom);
|
|
274
|
+
resizeObserver?.observe(scrollElement);
|
|
275
|
+
if (contentElement) resizeObserver?.observe(contentElement);
|
|
276
|
+
onCleanup(() => {
|
|
277
|
+
scrollElement.removeEventListener("scroll", checkContentAtBottom);
|
|
278
|
+
resizeObserver?.disconnect();
|
|
279
|
+
});
|
|
282
280
|
},
|
|
281
|
+
{ flush: "post" },
|
|
283
282
|
);
|
|
284
283
|
|
|
284
|
+
// The input overlay's height becomes the bottom padding of the messages.
|
|
285
285
|
watch(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (!isPinToBottomMode.value || !wasAtBottom) {
|
|
286
|
+
inputContainerRef,
|
|
287
|
+
(element, _old, onCleanup) => {
|
|
288
|
+
if (!element) {
|
|
289
|
+
inputContainerHeight.value = 0;
|
|
291
290
|
return;
|
|
292
291
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
292
|
+
onCleanup(
|
|
293
|
+
ɵwatchElementHeight(element, ({ height }) => {
|
|
294
|
+
inputContainerHeight.value = height;
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
299
297
|
},
|
|
300
|
-
{
|
|
298
|
+
{ flush: "post" },
|
|
301
299
|
);
|
|
302
300
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function syncInputContainerHeight() {
|
|
314
|
-
const element = inputContainerRef.value;
|
|
315
|
-
inputContainerHeight.value = element?.offsetHeight ?? 0;
|
|
316
|
-
}
|
|
301
|
+
watch(
|
|
302
|
+
() => props.inputValue,
|
|
303
|
+
(next) => {
|
|
304
|
+
if (isControlledInput.value) {
|
|
305
|
+
localInputValue.value = next ?? "";
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
);
|
|
317
309
|
|
|
318
310
|
function scrollToBottom(behavior: ScrollBehavior = "smooth") {
|
|
319
|
-
|
|
320
|
-
if (!element) return;
|
|
321
|
-
|
|
322
|
-
if (typeof element.scrollTo === "function") {
|
|
323
|
-
element.scrollTo({
|
|
324
|
-
top: element.scrollHeight,
|
|
325
|
-
behavior,
|
|
326
|
-
});
|
|
327
|
-
} else {
|
|
328
|
-
element.scrollTop = element.scrollHeight;
|
|
329
|
-
}
|
|
330
|
-
updateIsAtBottom();
|
|
311
|
+
stickToBottom?.scrollToBottom(behavior);
|
|
331
312
|
}
|
|
332
313
|
|
|
333
314
|
function handleScroll() {
|
|
334
|
-
|
|
315
|
+
stickToBottom?.update();
|
|
316
|
+
checkContentAtBottom();
|
|
335
317
|
}
|
|
336
318
|
|
|
337
319
|
function handleInputValueChange(value: string) {
|
|
@@ -416,30 +398,6 @@ const inputEventProps = computed(() => {
|
|
|
416
398
|
const canStop = computed(() => !!props.onStop);
|
|
417
399
|
const canAddFile = computed(() => !!props.onAddFile);
|
|
418
400
|
const canTranscribe = computed(() => !!props.onStartTranscribe);
|
|
419
|
-
|
|
420
|
-
onMounted(async () => {
|
|
421
|
-
await nextTick();
|
|
422
|
-
syncInputContainerHeight();
|
|
423
|
-
updateIsAtBottom();
|
|
424
|
-
if (isPinToBottomMode.value) {
|
|
425
|
-
scrollToBottom("auto");
|
|
426
|
-
if (typeof requestAnimationFrame === "function") {
|
|
427
|
-
requestAnimationFrame(() => {
|
|
428
|
-
scrollToBottom("auto");
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
attachInputOverlayObserver();
|
|
434
|
-
attachScrollContentObserver();
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
onBeforeUnmount(() => {
|
|
438
|
-
inputResizeObserver?.disconnect();
|
|
439
|
-
contentResizeObserver?.disconnect();
|
|
440
|
-
inputResizeObserver = null;
|
|
441
|
-
contentResizeObserver = null;
|
|
442
|
-
});
|
|
443
401
|
</script>
|
|
444
402
|
|
|
445
403
|
<template>
|
|
@@ -583,7 +541,6 @@ onBeforeUnmount(() => {
|
|
|
583
541
|
ref="scrollContainerRef"
|
|
584
542
|
data-testid="copilot-chat-view-scroll"
|
|
585
543
|
class="cpk-chat-view-scroll-area cpk-chat-view-scroll-viewport"
|
|
586
|
-
@scroll="handleScroll"
|
|
587
544
|
>
|
|
588
545
|
<div class="cpk-chat-view-scroll-content">
|
|
589
546
|
<div
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, ref, useAttrs, watch } from "vue";
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
onBeforeUnmount,
|
|
6
|
-
onMounted,
|
|
7
|
-
ref,
|
|
8
|
-
useAttrs,
|
|
9
|
-
watch,
|
|
10
|
-
} from "vue";
|
|
11
|
-
import {
|
|
12
|
-
ɵPOPUP_ANIMATION_MS,
|
|
4
|
+
ɵPOPUP_DEFAULT_HEIGHT,
|
|
5
|
+
ɵPOPUP_DEFAULT_WIDTH,
|
|
13
6
|
ɵbindPopupDismiss,
|
|
7
|
+
ɵcreateExitPresence,
|
|
14
8
|
ɵdimensionToCss,
|
|
15
9
|
} from "@copilotkit/core";
|
|
16
10
|
import { CopilotChatDefaultLabels } from "../../providers/types";
|
|
@@ -35,10 +29,6 @@ import type {
|
|
|
35
29
|
|
|
36
30
|
defineOptions({ inheritAttrs: false });
|
|
37
31
|
|
|
38
|
-
const DEFAULT_POPUP_WIDTH = 420;
|
|
39
|
-
const DEFAULT_POPUP_HEIGHT = 560;
|
|
40
|
-
const POPUP_ANIMATION_MS = ɵPOPUP_ANIMATION_MS;
|
|
41
|
-
|
|
42
32
|
const props = withDefaults(
|
|
43
33
|
defineProps<Omit<CopilotPopupViewProps, "defaultOpen">>(),
|
|
44
34
|
{
|
|
@@ -102,11 +92,19 @@ const attrs = useAttrs();
|
|
|
102
92
|
const config = useCopilotChatConfiguration();
|
|
103
93
|
|
|
104
94
|
const containerRef = ref<HTMLElement | null>(null);
|
|
105
|
-
const isRendered = ref(config.value?.isModalOpen ?? false);
|
|
106
|
-
const isAnimatingOut = ref(false);
|
|
107
|
-
let animationTimer: ReturnType<typeof setTimeout> | undefined;
|
|
108
|
-
|
|
109
95
|
const isPopupOpen = computed(() => config.value?.isModalOpen ?? false);
|
|
96
|
+
|
|
97
|
+
// Core keeps the closed popup rendered while its exit animation plays.
|
|
98
|
+
const presence = ɵcreateExitPresence(isPopupOpen.value);
|
|
99
|
+
const isRendered = ref(presence.getState().isRendered);
|
|
100
|
+
const isAnimatingOut = ref(presence.getState().isClosing);
|
|
101
|
+
presence.subscribe((state) => {
|
|
102
|
+
isRendered.value = state.isRendered;
|
|
103
|
+
isAnimatingOut.value = state.isClosing;
|
|
104
|
+
});
|
|
105
|
+
watch(isPopupOpen, (open) => presence.setOpen(open));
|
|
106
|
+
onBeforeUnmount(() => presence.dispose());
|
|
107
|
+
|
|
110
108
|
const labels = computed(() => config.value?.labels ?? CopilotChatDefaultLabels);
|
|
111
109
|
const headerTitle = computed(() => labels.value.modalHeaderTitle);
|
|
112
110
|
const popupAnimationClass = computed(() =>
|
|
@@ -119,11 +117,11 @@ const popupStyle = computed(
|
|
|
119
117
|
({
|
|
120
118
|
"--copilot-popup-width": ɵdimensionToCss(
|
|
121
119
|
props.width,
|
|
122
|
-
|
|
120
|
+
ɵPOPUP_DEFAULT_WIDTH,
|
|
123
121
|
),
|
|
124
122
|
"--copilot-popup-height": ɵdimensionToCss(
|
|
125
123
|
props.height,
|
|
126
|
-
|
|
124
|
+
ɵPOPUP_DEFAULT_HEIGHT,
|
|
127
125
|
),
|
|
128
126
|
"--copilot-popup-max-width": "calc(100vw - 3rem)",
|
|
129
127
|
"--copilot-popup-max-height": "calc(100dvh - 7.5rem)",
|
|
@@ -212,34 +210,6 @@ const chatViewBindings = computed(() => ({
|
|
|
212
210
|
...chatViewEventProps.value,
|
|
213
211
|
}));
|
|
214
212
|
|
|
215
|
-
watch(
|
|
216
|
-
isPopupOpen,
|
|
217
|
-
(open) => {
|
|
218
|
-
if (animationTimer) {
|
|
219
|
-
clearTimeout(animationTimer);
|
|
220
|
-
animationTimer = undefined;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (open) {
|
|
224
|
-
isRendered.value = true;
|
|
225
|
-
isAnimatingOut.value = false;
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (!isRendered.value) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
isAnimatingOut.value = true;
|
|
234
|
-
animationTimer = setTimeout(() => {
|
|
235
|
-
isRendered.value = false;
|
|
236
|
-
isAnimatingOut.value = false;
|
|
237
|
-
animationTimer = undefined;
|
|
238
|
-
}, POPUP_ANIMATION_MS);
|
|
239
|
-
},
|
|
240
|
-
{ immediate: true },
|
|
241
|
-
);
|
|
242
|
-
|
|
243
213
|
// Escape, a click outside (when enabled), and focus once the popup opens.
|
|
244
214
|
watch(
|
|
245
215
|
[isPopupOpen, () => props.clickOutsideToClose],
|
|
@@ -257,16 +227,6 @@ watch(
|
|
|
257
227
|
},
|
|
258
228
|
{ immediate: true },
|
|
259
229
|
);
|
|
260
|
-
|
|
261
|
-
onMounted(async () => {
|
|
262
|
-
await nextTick();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
onBeforeUnmount(() => {
|
|
266
|
-
if (animationTimer) {
|
|
267
|
-
clearTimeout(animationTimer);
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
230
|
</script>
|
|
271
231
|
|
|
272
232
|
<template>
|