@meistrari/chat-nuxt 4.2.0-rc.7 → 4.2.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/module.json +1 -1
- package/dist/runtime/embed/components/chat-embed-inner.vue +2 -0
- package/dist/runtime/messages/components/chat-message-list.d.vue.ts +4 -2
- package/dist/runtime/messages/components/chat-message-list.vue +18 -5
- package/dist/runtime/messages/components/chat-message-list.vue.d.ts +4 -2
- package/dist/runtime/messages/composables/use-stick-to-bottom.d.ts +28 -0
- package/dist/runtime/messages/composables/use-stick-to-bottom.js +136 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -1100,6 +1100,7 @@ async function handleDelete() {
|
|
|
1100
1100
|
</template>
|
|
1101
1101
|
|
|
1102
1102
|
<ChatMessageList
|
|
1103
|
+
:key="currentConversation?.id ?? 'draft'"
|
|
1103
1104
|
:messages="messages"
|
|
1104
1105
|
:events="displayConversationEvents"
|
|
1105
1106
|
:streaming-active-message-id="streamingActiveMessageId"
|
|
@@ -1314,6 +1315,7 @@ async function handleDelete() {
|
|
|
1314
1315
|
:variant="compact ? 'widget' : 'page'"
|
|
1315
1316
|
>
|
|
1316
1317
|
<ChatMessageList
|
|
1318
|
+
:key="currentConversation?.id ?? 'draft'"
|
|
1317
1319
|
:messages="messages"
|
|
1318
1320
|
:events="displayConversationEvents"
|
|
1319
1321
|
:streaming-active-message-id="streamingActiveMessageId"
|
|
@@ -21,8 +21,10 @@ type __VLS_Slots = {
|
|
|
21
21
|
}) => unknown;
|
|
22
22
|
};
|
|
23
23
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
24
|
-
isPinned:
|
|
25
|
-
scrollToBottom:
|
|
24
|
+
isPinned: import("vue").Ref<boolean, boolean>;
|
|
25
|
+
scrollToBottom: (options?: {
|
|
26
|
+
smooth?: boolean;
|
|
27
|
+
}) => void;
|
|
26
28
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
27
29
|
retry: (messageId: string) => any;
|
|
28
30
|
openConversation: (conversationId: string) => any;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { useStickToBottom } from "
|
|
2
|
+
import { useStickToBottom } from "../composables/use-stick-to-bottom";
|
|
3
3
|
import { mergeConversationTimeline } from "../conversation-timeline";
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
messages: { type: Array, required: true },
|
|
@@ -32,6 +32,8 @@ const scrollRoot = computed(() => conversationElements.value?.scroll ?? null);
|
|
|
32
32
|
const contentRoot = computed(() => conversationElements.value?.content ?? null);
|
|
33
33
|
const {
|
|
34
34
|
bottomPinned: isPinned,
|
|
35
|
+
hasRoomBelow,
|
|
36
|
+
detach,
|
|
35
37
|
scheduleScrollToBottom,
|
|
36
38
|
scrollToBottom
|
|
37
39
|
} = useStickToBottom(scrollRoot, contentRoot);
|
|
@@ -61,6 +63,17 @@ watch(latestOutputSignal, async () => {
|
|
|
61
63
|
await nextTick();
|
|
62
64
|
scheduleScrollToBottom();
|
|
63
65
|
}, { flush: "post" });
|
|
66
|
+
watch(() => props.streamingActiveMessageId, async (messageId, previousMessageId) => {
|
|
67
|
+
if (!messageId || messageId === previousMessageId)
|
|
68
|
+
return;
|
|
69
|
+
const streamingMessage = props.messages.find((message) => message.id === messageId);
|
|
70
|
+
if (!streamingMessage || streamingMessage.content)
|
|
71
|
+
return;
|
|
72
|
+
await nextTick();
|
|
73
|
+
if (isPinned.value)
|
|
74
|
+
scrollToBottom();
|
|
75
|
+
detach();
|
|
76
|
+
}, { flush: "post" });
|
|
64
77
|
defineExpose({
|
|
65
78
|
isPinned,
|
|
66
79
|
scrollToBottom
|
|
@@ -134,14 +147,14 @@ defineExpose({
|
|
|
134
147
|
|
|
135
148
|
<Transition name="jump-to-latest">
|
|
136
149
|
<button
|
|
137
|
-
v-if="!isPinned"
|
|
150
|
+
v-if="!isPinned && hasRoomBelow"
|
|
138
151
|
type="button"
|
|
139
152
|
class="chat-message-list__jump"
|
|
140
|
-
bg
|
|
153
|
+
bg="white/55"
|
|
141
154
|
b=".5px border-default"
|
|
142
155
|
aria-label="Ir para a mensagem mais recente"
|
|
143
156
|
title="Ir para a mensagem mais recente"
|
|
144
|
-
@click="scrollToBottom"
|
|
157
|
+
@click="scrollToBottom({ smooth: true })"
|
|
145
158
|
>
|
|
146
159
|
<TelaIcon name="i-ph-arrow-down" size="16px" color="icon-secondary" />
|
|
147
160
|
</button>
|
|
@@ -150,5 +163,5 @@ defineExpose({
|
|
|
150
163
|
</template>
|
|
151
164
|
|
|
152
165
|
<style scoped>
|
|
153
|
-
.chat-message-list>:deep([data-chat-conversation]){flex:1 1 0%;min-height:0;overflow-y:auto}.chat-message-list__jump{align-items:center;border-radius:999px;bottom:
|
|
166
|
+
.chat-message-list>:deep([data-chat-conversation]){flex:1 1 0%;min-height:0;overflow-y:auto}.chat-message-list__jump{align-items:center;backdrop-filter:blur(12px) saturate(1.4);-webkit-backdrop-filter:blur(12px) saturate(1.4);border-radius:999px;bottom:calc(var(--chat-input-height, 0px) + 40px);box-shadow:0 3px 12px rgba(0,0,0,.1);cursor:pointer;display:inline-flex;height:32px;justify-content:center;left:50%;position:absolute;transform:translateX(-50%);width:32px;z-index:2}.jump-to-latest-enter-active,.jump-to-latest-leave-active{transition:opacity .12s ease,transform .12s ease}.jump-to-latest-enter-from,.jump-to-latest-leave-to{opacity:0;transform:translateX(-50%) translateY(4px)}
|
|
154
167
|
</style>
|
|
@@ -21,8 +21,10 @@ type __VLS_Slots = {
|
|
|
21
21
|
}) => unknown;
|
|
22
22
|
};
|
|
23
23
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
24
|
-
isPinned:
|
|
25
|
-
scrollToBottom:
|
|
24
|
+
isPinned: import("vue").Ref<boolean, boolean>;
|
|
25
|
+
scrollToBottom: (options?: {
|
|
26
|
+
smooth?: boolean;
|
|
27
|
+
}) => void;
|
|
26
28
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
27
29
|
retry: (messageId: string) => any;
|
|
28
30
|
openConversation: (conversationId: string) => any;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
interface ScrollMetrics {
|
|
3
|
+
scrollTop: number;
|
|
4
|
+
scrollHeight: number;
|
|
5
|
+
clientHeight: number;
|
|
6
|
+
}
|
|
7
|
+
interface ScrollSnapshot {
|
|
8
|
+
scrollTop: number;
|
|
9
|
+
scrollHeight: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Only the user changes the pinned state: moving up detaches, moving down into
|
|
13
|
+
* the bottom zone reattaches. Layout changes that shift the bottom under a
|
|
14
|
+
* stationary viewport leave the state untouched. A position drop that comes
|
|
15
|
+
* with a shorter document is the browser clamping after content shrank, not the
|
|
16
|
+
* user reading upwards, so it keeps whatever state was in place.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolvePinnedAfterScroll(pinned: boolean, previous: ScrollSnapshot, { scrollTop, scrollHeight, clientHeight }: ScrollMetrics, threshold: number): boolean;
|
|
19
|
+
export declare function useStickToBottom(scrollRoot: Ref<HTMLElement | null>, contentRoot: Ref<HTMLElement | null>): {
|
|
20
|
+
bottomPinned: Ref<boolean, boolean>;
|
|
21
|
+
hasRoomBelow: Ref<boolean, boolean>;
|
|
22
|
+
detach: () => void;
|
|
23
|
+
scheduleScrollToBottom: () => void;
|
|
24
|
+
scrollToBottom: (options?: {
|
|
25
|
+
smooth?: boolean;
|
|
26
|
+
}) => void;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { getCurrentScope, onMounted, onScopeDispose, ref, watch } from "vue";
|
|
2
|
+
const PIN_THRESHOLD_PX = 64;
|
|
3
|
+
const AT_BOTTOM_EPSILON_PX = 1;
|
|
4
|
+
const ROOM_BELOW_MIN_PX = 30;
|
|
5
|
+
export function resolvePinnedAfterScroll(pinned, previous, { scrollTop, scrollHeight, clientHeight }, threshold) {
|
|
6
|
+
const distanceToBottom = scrollHeight - clientHeight - scrollTop;
|
|
7
|
+
const movedDown = scrollTop > previous.scrollTop + AT_BOTTOM_EPSILON_PX;
|
|
8
|
+
const movedUp = scrollTop < previous.scrollTop - AT_BOTTOM_EPSILON_PX;
|
|
9
|
+
if (distanceToBottom <= AT_BOTTOM_EPSILON_PX)
|
|
10
|
+
return pinned || movedDown;
|
|
11
|
+
if (movedUp)
|
|
12
|
+
return scrollHeight < previous.scrollHeight ? pinned : false;
|
|
13
|
+
if (movedDown && distanceToBottom <= threshold)
|
|
14
|
+
return true;
|
|
15
|
+
return pinned;
|
|
16
|
+
}
|
|
17
|
+
export function useStickToBottom(scrollRoot, contentRoot) {
|
|
18
|
+
const bottomPinned = ref(true);
|
|
19
|
+
const hasRoomBelow = ref(false);
|
|
20
|
+
let last = { scrollTop: 0, scrollHeight: 0 };
|
|
21
|
+
let frame = 0;
|
|
22
|
+
let touchStartY = null;
|
|
23
|
+
let resizeObserver = null;
|
|
24
|
+
let unbindScroll = null;
|
|
25
|
+
function scrollToBottom(options = {}) {
|
|
26
|
+
const element = scrollRoot.value;
|
|
27
|
+
if (!element)
|
|
28
|
+
return;
|
|
29
|
+
bottomPinned.value = true;
|
|
30
|
+
element.scrollTo({ top: element.scrollHeight, behavior: options.smooth ? "smooth" : "auto" });
|
|
31
|
+
remember(element);
|
|
32
|
+
}
|
|
33
|
+
function scheduleScrollToBottom() {
|
|
34
|
+
if (!bottomPinned.value || frame || typeof requestAnimationFrame === "undefined")
|
|
35
|
+
return;
|
|
36
|
+
frame = requestAnimationFrame(() => {
|
|
37
|
+
frame = 0;
|
|
38
|
+
if (bottomPinned.value)
|
|
39
|
+
scrollToBottom();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function unpin() {
|
|
43
|
+
bottomPinned.value = false;
|
|
44
|
+
}
|
|
45
|
+
function remember(element) {
|
|
46
|
+
last = { scrollTop: element.scrollTop, scrollHeight: element.scrollHeight };
|
|
47
|
+
hasRoomBelow.value = element.scrollHeight - element.clientHeight - element.scrollTop >= ROOM_BELOW_MIN_PX;
|
|
48
|
+
}
|
|
49
|
+
function handleContentResize() {
|
|
50
|
+
const element = scrollRoot.value;
|
|
51
|
+
if (element)
|
|
52
|
+
remember(element);
|
|
53
|
+
scheduleScrollToBottom();
|
|
54
|
+
}
|
|
55
|
+
function handleScroll() {
|
|
56
|
+
const element = scrollRoot.value;
|
|
57
|
+
if (!element)
|
|
58
|
+
return;
|
|
59
|
+
bottomPinned.value = resolvePinnedAfterScroll(bottomPinned.value, last, element, PIN_THRESHOLD_PX);
|
|
60
|
+
remember(element);
|
|
61
|
+
}
|
|
62
|
+
function handleWheel(event) {
|
|
63
|
+
if (event.deltaY < 0 && (scrollRoot.value?.scrollTop ?? 0) > 0)
|
|
64
|
+
unpin();
|
|
65
|
+
}
|
|
66
|
+
function handleKeyDown(event) {
|
|
67
|
+
const target = event.target;
|
|
68
|
+
if (event.defaultPrevented)
|
|
69
|
+
return;
|
|
70
|
+
if (target instanceof Element && target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])'))
|
|
71
|
+
return;
|
|
72
|
+
if (event.key === "ArrowUp" || event.key === "PageUp" || event.key === "Home")
|
|
73
|
+
unpin();
|
|
74
|
+
}
|
|
75
|
+
function handleTouchStart(event) {
|
|
76
|
+
touchStartY = event.touches[0]?.clientY ?? null;
|
|
77
|
+
}
|
|
78
|
+
function handleTouchMove(event) {
|
|
79
|
+
const currentY = event.touches[0]?.clientY ?? null;
|
|
80
|
+
if (touchStartY != null && currentY != null && currentY - touchStartY >= 6)
|
|
81
|
+
unpin();
|
|
82
|
+
}
|
|
83
|
+
function handleTouchEnd() {
|
|
84
|
+
touchStartY = null;
|
|
85
|
+
}
|
|
86
|
+
function bindScroll(element) {
|
|
87
|
+
unbindScroll?.();
|
|
88
|
+
unbindScroll = null;
|
|
89
|
+
if (!element)
|
|
90
|
+
return;
|
|
91
|
+
remember(element);
|
|
92
|
+
element.addEventListener("scroll", handleScroll, { passive: true });
|
|
93
|
+
element.addEventListener("wheel", handleWheel, { passive: true });
|
|
94
|
+
element.addEventListener("keydown", handleKeyDown);
|
|
95
|
+
element.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
96
|
+
element.addEventListener("touchmove", handleTouchMove, { passive: true });
|
|
97
|
+
element.addEventListener("touchend", handleTouchEnd, { passive: true });
|
|
98
|
+
unbindScroll = () => {
|
|
99
|
+
element.removeEventListener("scroll", handleScroll);
|
|
100
|
+
element.removeEventListener("wheel", handleWheel);
|
|
101
|
+
element.removeEventListener("keydown", handleKeyDown);
|
|
102
|
+
element.removeEventListener("touchstart", handleTouchStart);
|
|
103
|
+
element.removeEventListener("touchmove", handleTouchMove);
|
|
104
|
+
element.removeEventListener("touchend", handleTouchEnd);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function observeContent(element) {
|
|
108
|
+
resizeObserver?.disconnect();
|
|
109
|
+
resizeObserver = null;
|
|
110
|
+
if (!element || typeof ResizeObserver === "undefined")
|
|
111
|
+
return;
|
|
112
|
+
resizeObserver = new ResizeObserver(handleContentResize);
|
|
113
|
+
resizeObserver.observe(element);
|
|
114
|
+
}
|
|
115
|
+
onMounted(() => {
|
|
116
|
+
bindScroll(scrollRoot.value);
|
|
117
|
+
observeContent(contentRoot.value);
|
|
118
|
+
scheduleScrollToBottom();
|
|
119
|
+
});
|
|
120
|
+
const stopScrollWatch = watch(scrollRoot, bindScroll);
|
|
121
|
+
const stopContentWatch = watch(contentRoot, observeContent);
|
|
122
|
+
if (getCurrentScope()) {
|
|
123
|
+
onScopeDispose(() => {
|
|
124
|
+
stopScrollWatch();
|
|
125
|
+
stopContentWatch();
|
|
126
|
+
if (frame && typeof cancelAnimationFrame !== "undefined")
|
|
127
|
+
cancelAnimationFrame(frame);
|
|
128
|
+
frame = 0;
|
|
129
|
+
resizeObserver?.disconnect();
|
|
130
|
+
resizeObserver = null;
|
|
131
|
+
unbindScroll?.();
|
|
132
|
+
unbindScroll = null;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return { bottomPinned, hasRoomBelow, detach: unpin, scheduleScrollToBottom, scrollToBottom };
|
|
136
|
+
}
|