Adrien Denat commited on
Commit
b37570d
1 Parent(s): 9c0276d

Chat snap scroll bottom (#10)

Browse files

* only snap scroll to bottom if user didn't scroll up

* add scroll to bottom button when user scrolls up in the chat

* remove unecessary export

* rename scrollToBottom action to snapScrollToBottom + remove anim for now as it's stuttering

src/lib/actions/snapScrollToBottom.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @param node element to snap scroll to bottom
3
+ * @param dependency pass in a dependency to update scroll on changes.
4
+ */
5
+ export const snapScrollToBottom = (node: HTMLElement, dependency: any) => {
6
+ let prevScrollValue = node.scrollTop;
7
+ let isDetached = false;
8
+
9
+ const handleScroll = () => {
10
+ // if user scrolled up, we detach
11
+ if (node.scrollTop < prevScrollValue) {
12
+ isDetached = true;
13
+ }
14
+
15
+ // if user scrolled back to bottom, we reattach
16
+ if (node.scrollTop === node.scrollHeight - node.clientHeight) {
17
+ isDetached = false;
18
+ }
19
+
20
+ prevScrollValue = node.scrollTop;
21
+ };
22
+
23
+ const updateScroll = (_options: { force?: boolean } = {}) => {
24
+ const defaultOptions = { force: false };
25
+ const options = { ...defaultOptions, ..._options };
26
+ const { force } = options;
27
+
28
+ if (!force && isDetached) return;
29
+
30
+ node.scroll({
31
+ top: node.scrollHeight
32
+ });
33
+ };
34
+
35
+ node.addEventListener('scroll', handleScroll);
36
+
37
+ updateScroll({ force: true });
38
+
39
+ return {
40
+ update: updateScroll,
41
+ destroy: () => {
42
+ node.removeEventListener('scroll', handleScroll);
43
+ }
44
+ };
45
+ };
src/lib/components/ScrollToBottomBtn.svelte ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { fade } from 'svelte/transition';
3
+ import Chevron from './icons/Chevron.svelte';
4
+ import { onDestroy } from 'svelte';
5
+
6
+ export let scrollNode: HTMLElement;
7
+ export { className as class };
8
+
9
+ let visible: boolean = false;
10
+ let className = '';
11
+
12
+ $: if (scrollNode) {
13
+ scrollNode.addEventListener('scroll', onScroll);
14
+ }
15
+
16
+ function onScroll() {
17
+ visible =
18
+ Math.ceil(scrollNode.scrollTop) + 200 < scrollNode.scrollHeight - scrollNode.clientHeight;
19
+ }
20
+
21
+ onDestroy(() => {
22
+ if (!scrollNode) return;
23
+ scrollNode.removeEventListener('scroll', onScroll);
24
+ });
25
+ </script>
26
+
27
+ {#if visible}
28
+ <button
29
+ transition:fade={{ duration: 150 }}
30
+ on:click={() => scrollNode.scrollTo({ top: scrollNode.scrollHeight, behavior: 'smooth' })}
31
+ class="absolute flex rounded-full border w-10 h-10 items-center justify-center shadow bg-white dark:bg-gray-700 dark:border-gray-600 {className}"
32
+ ><Chevron /></button
33
+ >
34
+ {/if}
src/lib/components/icons/Chevron.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames: string = '';
3
+ </script>
4
+
5
+ <svg
6
+ width="15"
7
+ height="8"
8
+ viewBox="0 0 15 8"
9
+ class={classNames}
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <path
14
+ d="M1.67236 1L7.67236 7L13.6724 1"
15
+ stroke="currentColor"
16
+ stroke-width="2"
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"
19
+ />
20
+ </svg>
src/routes/+page.svelte CHANGED
@@ -1,7 +1,6 @@
1
  <script lang="ts">
2
  import type { Message } from '$lib/Types';
3
 
4
- import { afterUpdate } from 'svelte';
5
  import { HfInference } from '@huggingface/inference';
6
 
7
  import ChatMessage from '$lib/components/chat/ChatMessage.svelte';
@@ -21,18 +20,15 @@
21
  const userToken = PUBLIC_USER_MESSAGE_TOKEN || '<|prompter|>';
22
  const assistantToken = PUBLIC_ASSISTANT_MESSAGE_TOKEN || '<|assistant|>';
23
  const sepToken = PUBLIC_SEP_TOKEN || '<|endoftext|>';
 
 
24
 
25
  const hf = new HfInference();
26
  const model = hf.endpoint(`${$page.url.origin}/api/conversation`);
27
 
28
  let messages: Message[] = [];
29
  let message = '';
30
-
31
- let messagesContainer: HTMLElement;
32
-
33
- afterUpdate(() => {
34
- messagesContainer.scrollTo(0, messagesContainer.scrollHeight);
35
- });
36
 
37
  function switchTheme() {
38
  const { classList } = document.querySelector('html') as HTMLElement;
@@ -156,7 +152,7 @@
156
  <button>New Chat</button>
157
  <button>+</button>
158
  </nav>
159
- <div class="overflow-y-auto h-full" bind:this={messagesContainer}>
160
  <div class="max-w-3xl xl:max-w-4xl mx-auto px-5 pt-6 flex flex-col gap-8 h-full">
161
  {#each messages as message}
162
  <ChatMessage {message} />
@@ -165,6 +161,7 @@
165
  {/each}
166
  <div class="h-32 flex-none" />
167
  </div>
 
168
  </div>
169
  <div
170
  class="flex max-md:border-t dark:border-gray-800 items-center max-md:dark:bg-gray-900 max-md:bg-white bg-gradient-to-t from-white dark:from-gray-900 to-transparent justify-center absolute inset-x-0 max-w-3xl xl:max-w-4xl mx-auto px-5 bottom-0 py-4 md:py-8 w-full"
 
1
  <script lang="ts">
2
  import type { Message } from '$lib/Types';
3
 
 
4
  import { HfInference } from '@huggingface/inference';
5
 
6
  import ChatMessage from '$lib/components/chat/ChatMessage.svelte';
 
20
  const userToken = PUBLIC_USER_MESSAGE_TOKEN || '<|prompter|>';
21
  const assistantToken = PUBLIC_ASSISTANT_MESSAGE_TOKEN || '<|assistant|>';
22
  const sepToken = PUBLIC_SEP_TOKEN || '<|endoftext|>';
23
+ import { snapScrollToBottom } from '$lib/actions/snapScrollToBottom';
24
+ import ScrollToBottomBtn from '$lib/components/ScrollToBottomBtn.svelte';
25
 
26
  const hf = new HfInference();
27
  const model = hf.endpoint(`${$page.url.origin}/api/conversation`);
28
 
29
  let messages: Message[] = [];
30
  let message = '';
31
+ let chatContainer: HTMLElement;
 
 
 
 
 
32
 
33
  function switchTheme() {
34
  const { classList } = document.querySelector('html') as HTMLElement;
 
152
  <button>New Chat</button>
153
  <button>+</button>
154
  </nav>
155
+ <div class="overflow-y-auto h-full" use:snapScrollToBottom={messages} bind:this={chatContainer}>
156
  <div class="max-w-3xl xl:max-w-4xl mx-auto px-5 pt-6 flex flex-col gap-8 h-full">
157
  {#each messages as message}
158
  <ChatMessage {message} />
 
161
  {/each}
162
  <div class="h-32 flex-none" />
163
  </div>
164
+ <ScrollToBottomBtn class="bottom-10 right-12" scrollNode={chatContainer} />
165
  </div>
166
  <div
167
  class="flex max-md:border-t dark:border-gray-800 items-center max-md:dark:bg-gray-900 max-md:bg-white bg-gradient-to-t from-white dark:from-gray-900 to-transparent justify-center absolute inset-x-0 max-w-3xl xl:max-w-4xl mx-auto px-5 bottom-0 py-4 md:py-8 w-full"