evalstate nsarrazin HF Staff commited on
Commit
367ce02
·
unverified ·
1 Parent(s): 5559ab7

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="bottom-36 right-4 max-md:hidden lg:right-10"
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>