Spaces:
Runtime error
Runtime error
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 <sarrazin.nathan@gmail.com>
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>
|