Spaces:
Running
Running
Feature - Scroll to Previous Message (#1440)
Browse files* Added "Scroll to Previous Message" button.
* update mobile positioning to 50%
* fix linting issues in ScrollToPreviousBtn
---------
Co-authored-by: Nathan Sarrazin <[email protected]>
src/lib/components/ScrollToPreviousBtn.svelte
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { fade } from "svelte/transition";
|
3 |
+
import { onDestroy } from "svelte";
|
4 |
+
import IconChevron from "./icons/IconChevron.svelte";
|
5 |
+
|
6 |
+
export let scrollNode: HTMLElement;
|
7 |
+
export { className as class };
|
8 |
+
|
9 |
+
let visible = false;
|
10 |
+
let className = "";
|
11 |
+
let observer: ResizeObserver | null = null;
|
12 |
+
|
13 |
+
$: if (scrollNode) {
|
14 |
+
destroy();
|
15 |
+
|
16 |
+
if (window.ResizeObserver) {
|
17 |
+
observer = new ResizeObserver(() => {
|
18 |
+
updateVisibility();
|
19 |
+
});
|
20 |
+
observer.observe(scrollNode);
|
21 |
+
}
|
22 |
+
scrollNode.addEventListener("scroll", updateVisibility);
|
23 |
+
}
|
24 |
+
|
25 |
+
function updateVisibility() {
|
26 |
+
if (!scrollNode) return;
|
27 |
+
visible =
|
28 |
+
Math.ceil(scrollNode.scrollTop) + 200 < scrollNode.scrollHeight - scrollNode.clientHeight &&
|
29 |
+
scrollNode.scrollTop > 200;
|
30 |
+
}
|
31 |
+
|
32 |
+
function scrollToPrevious() {
|
33 |
+
if (!scrollNode) return;
|
34 |
+
const messages = scrollNode.querySelectorAll('[id^="message-"]');
|
35 |
+
const scrollTop = scrollNode.scrollTop;
|
36 |
+
let previousMessage: Element | null = null;
|
37 |
+
|
38 |
+
for (let i = messages.length - 1; i >= 0; i--) {
|
39 |
+
const messageTop =
|
40 |
+
messages[i].getBoundingClientRect().top +
|
41 |
+
scrollTop -
|
42 |
+
scrollNode.getBoundingClientRect().top;
|
43 |
+
if (messageTop < scrollTop - 1) {
|
44 |
+
previousMessage = messages[i];
|
45 |
+
break;
|
46 |
+
}
|
47 |
+
}
|
48 |
+
|
49 |
+
if (previousMessage) {
|
50 |
+
previousMessage.scrollIntoView({ behavior: "smooth", block: "start" });
|
51 |
+
}
|
52 |
+
}
|
53 |
+
|
54 |
+
function destroy() {
|
55 |
+
observer?.disconnect();
|
56 |
+
scrollNode?.removeEventListener("scroll", updateVisibility);
|
57 |
+
}
|
58 |
+
|
59 |
+
onDestroy(destroy);
|
60 |
+
</script>
|
61 |
+
|
62 |
+
{#if visible}
|
63 |
+
<button
|
64 |
+
transition:fade={{ duration: 150 }}
|
65 |
+
on:click={scrollToPrevious}
|
66 |
+
class="btn absolute flex h-[41px] w-[41px] rounded-full border bg-white shadow-md transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:shadow-gray-950 dark:hover:bg-gray-600 {className}"
|
67 |
+
>
|
68 |
+
<IconChevron classNames="rotate-180 mt-[2px]" />
|
69 |
+
</button>
|
70 |
+
{/if}
|
src/lib/components/chat/ChatMessage.svelte
CHANGED
@@ -230,6 +230,7 @@
|
|
230 |
{#if message.from === "assistant"}
|
231 |
<div
|
232 |
class="group relative -mb-4 flex items-start justify-start gap-4 pb-4 leading-relaxed"
|
|
|
233 |
role="presentation"
|
234 |
on:click={() => (isTapped = !isTapped)}
|
235 |
on:keydown={() => (isTapped = !isTapped)}
|
@@ -372,6 +373,7 @@
|
|
372 |
{#if message.from === "user"}
|
373 |
<div
|
374 |
class="group relative w-full items-start justify-start gap-4 max-sm:text-sm"
|
|
|
375 |
role="presentation"
|
376 |
on:click={() => (isTapped = !isTapped)}
|
377 |
on:keydown={() => (isTapped = !isTapped)}
|
|
|
230 |
{#if message.from === "assistant"}
|
231 |
<div
|
232 |
class="group relative -mb-4 flex items-start justify-start gap-4 pb-4 leading-relaxed"
|
233 |
+
id="message-assistant-{message.id}"
|
234 |
role="presentation"
|
235 |
on:click={() => (isTapped = !isTapped)}
|
236 |
on:keydown={() => (isTapped = !isTapped)}
|
|
|
373 |
{#if message.from === "user"}
|
374 |
<div
|
375 |
class="group relative w-full items-start justify-start gap-4 max-sm:text-sm"
|
376 |
+
id="message-user-{message.id}"
|
377 |
role="presentation"
|
378 |
on:click={() => (isTapped = !isTapped)}
|
379 |
on:keydown={() => (isTapped = !isTapped)}
|
src/lib/components/chat/ChatWindow.svelte
CHANGED
@@ -27,6 +27,7 @@
|
|
27 |
import AssistantIntroduction from "./AssistantIntroduction.svelte";
|
28 |
import ChatMessage from "./ChatMessage.svelte";
|
29 |
import ScrollToBottomBtn from "../ScrollToBottomBtn.svelte";
|
|
|
30 |
import { browser } from "$app/environment";
|
31 |
import { snapScrollToBottom } from "$lib/actions/snapScrollToBottom";
|
32 |
import SystemPromptModal from "../SystemPromptModal.svelte";
|
@@ -328,8 +329,14 @@
|
|
328 |
/>
|
329 |
{/if}
|
330 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
331 |
<ScrollToBottomBtn
|
332 |
-
class="
|
333 |
scrollNode={chatContainer}
|
334 |
/>
|
335 |
</div>
|
|
|
27 |
import AssistantIntroduction from "./AssistantIntroduction.svelte";
|
28 |
import ChatMessage from "./ChatMessage.svelte";
|
29 |
import ScrollToBottomBtn from "../ScrollToBottomBtn.svelte";
|
30 |
+
import ScrollToPreviousBtn from "../ScrollToPreviousBtn.svelte";
|
31 |
import { browser } from "$app/environment";
|
32 |
import { snapScrollToBottom } from "$lib/actions/snapScrollToBottom";
|
33 |
import SystemPromptModal from "../SystemPromptModal.svelte";
|
|
|
329 |
/>
|
330 |
{/if}
|
331 |
</div>
|
332 |
+
|
333 |
+
<ScrollToPreviousBtn
|
334 |
+
class="fixed right-4 max-md:bottom-[calc(50%+26px)] md:bottom-48 lg:right-10"
|
335 |
+
scrollNode={chatContainer}
|
336 |
+
/>
|
337 |
+
|
338 |
<ScrollToBottomBtn
|
339 |
+
class="fixed right-4 max-md:bottom-[calc(50%-26px)] md:bottom-36 lg:right-10"
|
340 |
scrollNode={chatContainer}
|
341 |
/>
|
342 |
</div>
|