nsarrazin HF Staff commited on
Commit
5290cbb
·
unverified ·
1 Parent(s): 14ef8d0

feat: infinite scrolling list of conversations (#1564)

Browse files

* wip

* wip: infinite scrolling continues

* fix: merge regression

* fix: infinite scrolling

* fix: unify use of base over APP_BASE

* fix: show loader properly when invalidating after reaching end of page

src/lib/components/InfiniteScroll.svelte ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount, createEventDispatcher } from "svelte";
3
+
4
+ const dispatch = createEventDispatcher();
5
+ let loader: HTMLDivElement;
6
+ let observer: IntersectionObserver;
7
+ let intervalId: ReturnType<typeof setInterval> | undefined;
8
+
9
+ onMount(() => {
10
+ observer = new IntersectionObserver((entries) => {
11
+ entries.forEach((entry) => {
12
+ if (entry.isIntersecting) {
13
+ // Clear any existing interval
14
+ if (intervalId) {
15
+ clearInterval(intervalId);
16
+ }
17
+ // Start new interval that dispatches every 250ms
18
+ intervalId = setInterval(() => {
19
+ dispatch("visible");
20
+ }, 250);
21
+ } else {
22
+ // Clear interval when not intersecting
23
+ if (intervalId) {
24
+ clearInterval(intervalId);
25
+ intervalId = undefined;
26
+ }
27
+ }
28
+ });
29
+ });
30
+
31
+ observer.observe(loader);
32
+
33
+ return () => {
34
+ observer.disconnect();
35
+ if (intervalId) {
36
+ clearInterval(intervalId);
37
+ }
38
+ };
39
+ });
40
+ </script>
41
+
42
+ <div bind:this={loader} class="flex animate-pulse flex-col gap-4">
43
+ <div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700" />
44
+ <div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700" />
45
+ <div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700" />
46
+ </div>
src/lib/components/NavMenu.svelte CHANGED
@@ -10,11 +10,18 @@
10
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
11
  import type { Model } from "$lib/types/Model";
12
  import { page } from "$app/stores";
 
 
 
13
 
14
  export let conversations: ConvSidebar[];
15
  export let canLogin: boolean;
16
  export let user: LayoutData["user"];
17
 
 
 
 
 
18
  function handleNewChatClick() {
19
  isAborted.set(true);
20
  }
@@ -44,6 +51,34 @@
44
  } as const;
45
 
46
  const nModels: number = $page.data.models.filter((el: Model) => !el.unlisted).length;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  </script>
48
 
49
  <div class="sticky top-0 flex flex-none items-center justify-between px-1.5 py-3.5 max-sm:pt-0">
@@ -89,6 +124,9 @@
89
  {/if}
90
  {/each}
91
  </div>
 
 
 
92
  {/await}
93
  </div>
94
  <div
 
10
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
11
  import type { Model } from "$lib/types/Model";
12
  import { page } from "$app/stores";
13
+ import InfiniteScroll from "./InfiniteScroll.svelte";
14
+ import type { Conversation } from "$lib/types/Conversation";
15
+ import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
16
 
17
  export let conversations: ConvSidebar[];
18
  export let canLogin: boolean;
19
  export let user: LayoutData["user"];
20
 
21
+ export let p = 0;
22
+
23
+ let hasMore = true;
24
+
25
  function handleNewChatClick() {
26
  isAborted.set(true);
27
  }
 
51
  } as const;
52
 
53
  const nModels: number = $page.data.models.filter((el: Model) => !el.unlisted).length;
54
+
55
+ async function handleVisible() {
56
+ p++;
57
+ const newConvs = await fetch(`${base}/api/conversations?p=${p}`)
58
+ .then((res) => res.json())
59
+ .then((convs) =>
60
+ convs.map(
61
+ (conv: Pick<Conversation, "_id" | "title" | "updatedAt" | "model" | "assistantId">) => ({
62
+ ...conv,
63
+ updatedAt: new Date(conv.updatedAt),
64
+ })
65
+ )
66
+ )
67
+ .catch(() => []);
68
+
69
+ if (newConvs.length === 0) {
70
+ hasMore = false;
71
+ }
72
+
73
+ conversations = [...conversations, ...newConvs];
74
+ }
75
+
76
+ $: if (conversations.length <= CONV_NUM_PER_PAGE) {
77
+ // reset p to 0 if there's only one page of content
78
+ // that would be caused by a data loading invalidation
79
+ p = 0;
80
+ hasMore = true;
81
+ }
82
  </script>
83
 
84
  <div class="sticky top-0 flex flex-none items-center justify-between px-1.5 py-3.5 max-sm:pt-0">
 
124
  {/if}
125
  {/each}
126
  </div>
127
+ {#if hasMore}
128
+ <InfiniteScroll on:visible={handleVisible} />
129
+ {/if}
130
  {/await}
131
  </div>
132
  <div
src/lib/constants/pagination.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ export const CONV_NUM_PER_PAGE = 30;
src/routes/+layout.server.ts CHANGED
@@ -12,8 +12,9 @@ import { toolFromConfigs } from "$lib/server/tools";
12
  import { MetricsServer } from "$lib/server/metrics";
13
  import type { ToolFront, ToolInputFile } from "$lib/types/Tool";
14
  import { ReviewStatus } from "$lib/types/Review";
 
15
 
16
- export const load: LayoutServerLoad = async ({ locals, depends }) => {
17
  depends(UrlDependency.ConversationList);
18
 
19
  const settings = await collections.settings.findOne(authCondition(locals));
@@ -56,24 +57,17 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
56
  const conversations =
57
  nConversations === 0
58
  ? Promise.resolve([])
59
- : collections.conversations
60
- .find(authCondition(locals))
61
- .sort({ updatedAt: -1 })
62
- .project<
63
- Pick<
64
- Conversation,
65
- "title" | "model" | "_id" | "updatedAt" | "createdAt" | "assistantId"
66
- >
67
- >({
68
- title: 1,
69
- model: 1,
70
- _id: 1,
71
- updatedAt: 1,
72
- createdAt: 1,
73
- assistantId: 1,
74
- })
75
- .limit(300)
76
- .toArray();
77
 
78
  const userAssistants = settings?.assistants?.map((assistantId) => assistantId.toString()) ?? [];
79
  const userAssistantsSet = new Set(userAssistants);
 
12
  import { MetricsServer } from "$lib/server/metrics";
13
  import type { ToolFront, ToolInputFile } from "$lib/types/Tool";
14
  import { ReviewStatus } from "$lib/types/Review";
15
+ import { base } from "$app/paths";
16
 
17
+ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => {
18
  depends(UrlDependency.ConversationList);
19
 
20
  const settings = await collections.settings.findOne(authCondition(locals));
 
57
  const conversations =
58
  nConversations === 0
59
  ? Promise.resolve([])
60
+ : fetch(`${base}/api/conversations`)
61
+ .then((res) => res.json())
62
+ .then(
63
+ (
64
+ convs: Pick<Conversation, "_id" | "title" | "updatedAt" | "model" | "assistantId">[]
65
+ ) =>
66
+ convs.map((conv) => ({
67
+ ...conv,
68
+ updatedAt: new Date(conv.updatedAt),
69
+ }))
70
+ );
 
 
 
 
 
 
 
71
 
72
  const userAssistants = settings?.assistants?.map((assistantId) => assistantId.toString()) ?? [];
73
  const userAssistantsSet = new Set(userAssistants);
src/routes/api/conversations/+server.ts CHANGED
@@ -2,8 +2,7 @@ import { collections } from "$lib/server/database";
2
  import { models } from "$lib/server/models";
3
  import { authCondition } from "$lib/server/auth";
4
  import type { Conversation } from "$lib/types/Conversation";
5
-
6
- const NUM_PER_PAGE = 300;
7
 
8
  export async function GET({ locals, url }) {
9
  const p = parseInt(url.searchParams.get("p") ?? "0");
@@ -20,19 +19,24 @@ export async function GET({ locals, url }) {
20
  assistantId: 1,
21
  })
22
  .sort({ updatedAt: -1 })
23
- .skip(p * NUM_PER_PAGE)
24
- .limit(NUM_PER_PAGE)
25
  .toArray();
26
 
 
 
 
 
27
  const res = convs.map((conv) => ({
28
- id: conv._id,
 
29
  title: conv.title,
30
  updatedAt: conv.updatedAt,
31
- modelId: conv.model,
 
32
  assistantId: conv.assistantId,
33
  modelTools: models.find((m) => m.id == conv.model)?.tools ?? false,
34
  }));
35
-
36
  return Response.json(res);
37
  } else {
38
  return Response.json({ message: "Must have session cookie" }, { status: 401 });
 
2
  import { models } from "$lib/server/models";
3
  import { authCondition } from "$lib/server/auth";
4
  import type { Conversation } from "$lib/types/Conversation";
5
+ import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
 
6
 
7
  export async function GET({ locals, url }) {
8
  const p = parseInt(url.searchParams.get("p") ?? "0");
 
19
  assistantId: 1,
20
  })
21
  .sort({ updatedAt: -1 })
22
+ .skip(p * CONV_NUM_PER_PAGE)
23
+ .limit(CONV_NUM_PER_PAGE)
24
  .toArray();
25
 
26
+ if (convs.length === 0) {
27
+ return Response.json([]);
28
+ }
29
+
30
  const res = convs.map((conv) => ({
31
+ _id: conv._id,
32
+ id: conv._id, // legacy param iOS
33
  title: conv.title,
34
  updatedAt: conv.updatedAt,
35
+ model: conv.model,
36
+ modelId: conv.model, // legacy param iOS
37
  assistantId: conv.assistantId,
38
  modelTools: models.find((m) => m.id == conv.model)?.tools ?? false,
39
  }));
 
40
  return Response.json(res);
41
  } else {
42
  return Response.json({ message: "Must have session cookie" }, { status: 401 });