nsarrazin HF Staff commited on
Commit
9d36148
·
unverified ·
1 Parent(s): e7c09f7

feat: lazy stream conversations load function (#1553)

Browse files

* feat: lazy stream conversations load function

* fix: lint

* feat: add animation

* feat: skip db call if no convs

src/lib/components/MobileNav.svelte CHANGED
@@ -10,7 +10,7 @@
10
  import IconNew from "$lib/components/icons/IconNew.svelte";
11
 
12
  export let isOpen = false;
13
- export let title: string | undefined;
14
 
15
  $: title = title ?? "New Chat";
16
 
@@ -40,7 +40,11 @@
40
  aria-label="Open menu"
41
  bind:this={openEl}><CarbonTextAlignJustify /></button
42
  >
43
- <span class="truncate px-4">{title}</span>
 
 
 
 
44
  <a
45
  class:invisible={!$page.params.id}
46
  href="{base}/"
 
10
  import IconNew from "$lib/components/icons/IconNew.svelte";
11
 
12
  export let isOpen = false;
13
+ export let title: Promise<string | undefined> | string;
14
 
15
  $: title = title ?? "New Chat";
16
 
 
40
  aria-label="Open menu"
41
  bind:this={openEl}><CarbonTextAlignJustify /></button
42
  >
43
+ {#await title}
44
+ <div class="flex h-full items-center justify-center" />
45
+ {:then title}
46
+ <span class="truncate px-4">{title ?? ""}</span>
47
+ {/await}
48
  <a
49
  class:invisible={!$page.params.id}
50
  href="{base}/"
src/lib/components/NavConversationItem.svelte CHANGED
@@ -34,9 +34,9 @@
34
  {#if confirmDelete}
35
  <span class="mr-1 font-semibold"> Delete </span>
36
  {/if}
37
- {#if conv.avatarHash}
38
  <img
39
- src="{base}/settings/assistants/{conv.assistantId}/avatar.jpg?hash={conv.avatarHash}"
40
  alt="Assistant avatar"
41
  class="mr-1.5 inline size-4 flex-none rounded-full object-cover"
42
  />
 
34
  {#if confirmDelete}
35
  <span class="mr-1 font-semibold"> Delete </span>
36
  {/if}
37
+ {#if conv.avatarUrl}
38
  <img
39
+ src="{base}{conv.avatarUrl}"
40
  alt="Assistant avatar"
41
  class="mr-1.5 inline size-4 flex-none rounded-full object-cover"
42
  />
src/lib/components/NavMenu.svelte CHANGED
@@ -11,7 +11,8 @@
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
 
@@ -25,16 +26,16 @@
25
  new Date().setMonth(new Date().getMonth() - 1),
26
  ];
27
 
28
- $: groupedConversations = {
29
- today: conversations.filter(({ updatedAt }) => updatedAt.getTime() > dateRanges[0]),
30
- week: conversations.filter(
31
  ({ updatedAt }) => updatedAt.getTime() > dateRanges[1] && updatedAt.getTime() < dateRanges[0]
32
  ),
33
- month: conversations.filter(
34
  ({ updatedAt }) => updatedAt.getTime() > dateRanges[2] && updatedAt.getTime() < dateRanges[1]
35
  ),
36
- older: conversations.filter(({ updatedAt }) => updatedAt.getTime() < dateRanges[2]),
37
- };
38
 
39
  const titles: { [key: string]: string } = {
40
  today: "Today",
@@ -65,16 +66,31 @@
65
  <div
66
  class="scrollbar-custom flex flex-col gap-1 overflow-y-auto rounded-r-xl from-gray-50 px-3 pb-3 pt-2 text-[.9rem] dark:from-gray-800/30 max-sm:bg-gradient-to-t md:bg-gradient-to-l"
67
  >
68
- {#each Object.entries(groupedConversations) as [group, convs]}
69
- {#if convs.length}
70
- <h4 class="mb-1.5 mt-4 pl-0.5 text-sm text-gray-400 first:mt-0 dark:text-gray-500">
71
- {titles[group]}
72
- </h4>
73
- {#each convs as conv}
74
- <NavConversationItem on:editConversationTitle on:deleteConversation {conv} />
75
- {/each}
 
 
76
  {/if}
77
- {/each}
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  </div>
79
  <div
80
  class="mt-0.5 flex flex-col gap-1 rounded-r-xl p-3 text-sm md:bg-gradient-to-l md:from-gray-50 md:dark:from-gray-800/30"
 
11
  import type { Model } from "$lib/types/Model";
12
  import { page } from "$app/stores";
13
 
14
+ import { fade } from "svelte/transition";
15
+ export let conversations: Promise<ConvSidebar[]>;
16
  export let canLogin: boolean;
17
  export let user: LayoutData["user"];
18
 
 
26
  new Date().setMonth(new Date().getMonth() - 1),
27
  ];
28
 
29
+ $: groupedConversations = conversations.then((convs) => ({
30
+ today: convs.filter(({ updatedAt }) => updatedAt.getTime() > dateRanges[0]),
31
+ week: convs.filter(
32
  ({ updatedAt }) => updatedAt.getTime() > dateRanges[1] && updatedAt.getTime() < dateRanges[0]
33
  ),
34
+ month: convs.filter(
35
  ({ updatedAt }) => updatedAt.getTime() > dateRanges[2] && updatedAt.getTime() < dateRanges[1]
36
  ),
37
+ older: convs.filter(({ updatedAt }) => updatedAt.getTime() < dateRanges[2]),
38
+ }));
39
 
40
  const titles: { [key: string]: string } = {
41
  today: "Today",
 
66
  <div
67
  class="scrollbar-custom flex flex-col gap-1 overflow-y-auto rounded-r-xl from-gray-50 px-3 pb-3 pt-2 text-[.9rem] dark:from-gray-800/30 max-sm:bg-gradient-to-t md:bg-gradient-to-l"
68
  >
69
+ {#await groupedConversations}
70
+ {#if $page.data.nConversations > 0}
71
+ <div class="overflow-y-hidden">
72
+ <div class="flex animate-pulse flex-col gap-4">
73
+ <div class="h-4 w-24 rounded bg-gray-200 dark:bg-gray-700" />
74
+ {#each Array(100) as _}
75
+ <div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700" />
76
+ {/each}
77
+ </div>
78
+ </div>
79
  {/if}
80
+ {:then groupedConversations}
81
+ <div transition:fade class="flex flex-col gap-1">
82
+ {#each Object.entries(groupedConversations) as [group, convs]}
83
+ {#if convs.length}
84
+ <h4 class="mb-1.5 mt-4 pl-0.5 text-sm text-gray-400 first:mt-0 dark:text-gray-500">
85
+ {titles[group]}
86
+ </h4>
87
+ {#each convs as conv}
88
+ <NavConversationItem on:editConversationTitle on:deleteConversation {conv} />
89
+ {/each}
90
+ {/if}
91
+ {/each}
92
+ </div>
93
+ {/await}
94
  </div>
95
  <div
96
  class="mt-0.5 flex flex-col gap-1 rounded-r-xl p-3 text-sm md:bg-gradient-to-l md:from-gray-50 md:dark:from-gray-800/30"
src/lib/types/ConvSidebar.ts CHANGED
@@ -4,5 +4,5 @@ export interface ConvSidebar {
4
  updatedAt: Date;
5
  model?: string;
6
  assistantId?: string;
7
- avatarHash?: string;
8
  }
 
4
  updatedAt: Date;
5
  model?: string;
6
  assistantId?: string;
7
+ avatarUrl?: string;
8
  }
src/routes/+layout.server.ts CHANGED
@@ -51,38 +51,52 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
51
  })
52
  : null;
53
 
54
- const conversations = await collections.conversations
55
- .find(authCondition(locals))
56
- .sort({ updatedAt: -1 })
57
- .project<
58
- Pick<Conversation, "title" | "model" | "_id" | "updatedAt" | "createdAt" | "assistantId">
59
- >({
60
- title: 1,
61
- model: 1,
62
- _id: 1,
63
- updatedAt: 1,
64
- createdAt: 1,
65
- assistantId: 1,
66
- })
67
- .limit(300)
68
- .toArray();
 
 
 
 
 
 
 
 
69
 
70
  const userAssistants = settings?.assistants?.map((assistantId) => assistantId.toString()) ?? [];
71
  const userAssistantsSet = new Set(userAssistants);
72
 
73
- const assistantIds = [
74
- ...userAssistants.map((el) => new ObjectId(el)),
75
- ...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]),
76
- ];
77
-
78
- const assistants = await collections.assistants.find({ _id: { $in: assistantIds } }).toArray();
 
 
 
 
 
 
79
 
80
  const messagesBeforeLogin = env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0;
81
 
82
  let loginRequired = false;
83
 
84
  if (requiresUser && !locals.user && messagesBeforeLogin) {
85
- if (conversations.length > messagesBeforeLogin) {
86
  loginRequired = true;
87
  } else {
88
  // get the number of messages where `from === "assistant"` across all conversations.
@@ -129,25 +143,42 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
129
  );
130
 
131
  return {
132
- conversations: conversations.map((conv) => {
133
- if (settings?.hideEmojiOnSidebar) {
134
- conv.title = conv.title.replace(/\p{Emoji}/gu, "");
135
- }
136
-
137
- // remove invalid unicode and trim whitespaces
138
- conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart();
139
-
140
- return {
141
- id: conv._id.toString(),
142
- title: conv.title,
143
- model: conv.model ?? defaultModel,
144
- updatedAt: conv.updatedAt,
145
- assistantId: conv.assistantId?.toString(),
146
- avatarHash:
147
- conv.assistantId &&
148
- assistants.find((a) => a._id.toString() === conv.assistantId?.toString())?.avatar,
149
- };
150
- }) satisfies ConvSidebar[],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  settings: {
152
  searchEnabled: !!(
153
  env.SERPAPI_KEY ||
@@ -223,15 +254,17 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
223
  type: "community",
224
  review: ReviewStatus.APPROVED,
225
  }),
226
- assistants: assistants
227
- .filter((el) => userAssistantsSet.has(el._id.toString()))
228
- .map((el) => ({
229
- ...el,
230
- _id: el._id.toString(),
231
- createdById: undefined,
232
- createdByMe:
233
- el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(),
234
- })),
 
 
235
  user: locals.user && {
236
  id: locals.user._id.toString(),
237
  username: locals.user.username,
 
51
  })
52
  : null;
53
 
54
+ const nConversations = await collections.conversations.countDocuments(authCondition(locals));
55
+
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);
80
 
81
+ const assistants = conversations.then((conversations) =>
82
+ collections.assistants
83
+ .find({
84
+ _id: {
85
+ $in: [
86
+ ...userAssistants.map((el) => new ObjectId(el)),
87
+ ...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]),
88
+ ],
89
+ },
90
+ })
91
+ .toArray()
92
+ );
93
 
94
  const messagesBeforeLogin = env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0;
95
 
96
  let loginRequired = false;
97
 
98
  if (requiresUser && !locals.user && messagesBeforeLogin) {
99
+ if (nConversations > messagesBeforeLogin) {
100
  loginRequired = true;
101
  } else {
102
  // get the number of messages where `from === "assistant"` across all conversations.
 
143
  );
144
 
145
  return {
146
+ nConversations,
147
+ conversations: conversations.then(
148
+ async (convs) =>
149
+ await Promise.all(
150
+ convs.map(async (conv) => {
151
+ if (settings?.hideEmojiOnSidebar) {
152
+ conv.title = conv.title.replace(/\p{Emoji}/gu, "");
153
+ }
154
+
155
+ // remove invalid unicode and trim whitespaces
156
+ conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart();
157
+
158
+ let avatarUrl: string | undefined = undefined;
159
+
160
+ if (conv.assistantId) {
161
+ const hash = (
162
+ await collections.assistants.findOne({
163
+ _id: new ObjectId(conv.assistantId),
164
+ })
165
+ )?.avatar;
166
+ if (hash) {
167
+ avatarUrl = `/settings/assistants/${conv.assistantId}/avatar.jpg?hash=${hash}`;
168
+ }
169
+ }
170
+
171
+ return {
172
+ id: conv._id.toString(),
173
+ title: conv.title,
174
+ model: conv.model ?? defaultModel,
175
+ updatedAt: conv.updatedAt,
176
+ assistantId: conv.assistantId?.toString(),
177
+ avatarUrl,
178
+ } satisfies ConvSidebar;
179
+ })
180
+ )
181
+ ),
182
  settings: {
183
  searchEnabled: !!(
184
  env.SERPAPI_KEY ||
 
254
  type: "community",
255
  review: ReviewStatus.APPROVED,
256
  }),
257
+ assistants: assistants.then((assistants) =>
258
+ assistants
259
+ .filter((el) => userAssistantsSet.has(el._id.toString()))
260
+ .map((el) => ({
261
+ ...el,
262
+ _id: el._id.toString(),
263
+ createdById: undefined,
264
+ createdByMe:
265
+ el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(),
266
+ }))
267
+ ),
268
  user: locals.user && {
269
  id: locals.user._id.toString(),
270
  username: locals.user.username,
src/routes/+layout.svelte CHANGED
@@ -100,15 +100,17 @@
100
  $: if ($error) onError();
101
 
102
  $: if ($titleUpdate) {
103
- const convIdx = data.conversations.findIndex(({ id }) => id === $titleUpdate?.convId);
 
104
 
105
- if (convIdx != -1) {
106
- data.conversations[convIdx].title = $titleUpdate?.title ?? data.conversations[convIdx].title;
107
- }
108
- // update data.conversations
109
- data.conversations = [...data.conversations];
110
 
111
- $titleUpdate = null;
 
112
  }
113
 
114
  const settings = createSettingsStore(data.settings);
@@ -147,7 +149,7 @@
147
 
148
  $: mobileNavTitle = ["/models", "/assistants", "/privacy"].includes($page.route.id ?? "")
149
  ? ""
150
- : data.conversations.find((conv) => conv.id === $page.params.id)?.title;
151
  </script>
152
 
153
  <svelte:head>
 
100
  $: if ($error) onError();
101
 
102
  $: if ($titleUpdate) {
103
+ data.conversations.then((convs) => {
104
+ const convIdx = convs.findIndex(({ id }) => id === $titleUpdate?.convId);
105
 
106
+ if (convIdx != -1) {
107
+ convs[convIdx].title = $titleUpdate?.title ?? convs[convIdx].title;
108
+ }
109
+ // update data.conversations
110
+ data.conversations = Promise.resolve([...convs]);
111
 
112
+ $titleUpdate = null;
113
+ });
114
  }
115
 
116
  const settings = createSettingsStore(data.settings);
 
149
 
150
  $: mobileNavTitle = ["/models", "/assistants", "/privacy"].includes($page.route.id ?? "")
151
  ? ""
152
+ : data.conversations.then((convs) => convs.find((conv) => conv.id === $page.params.id)?.title);
153
  </script>
154
 
155
  <svelte:head>
src/routes/conversation/[id]/+page.svelte CHANGED
@@ -4,7 +4,7 @@
4
  import { isAborted } from "$lib/stores/isAborted";
5
  import { onMount } from "svelte";
6
  import { page } from "$app/stores";
7
- import { goto, invalidateAll } from "$app/navigation";
8
  import { base } from "$app/paths";
9
  import { shareConversation } from "$lib/shareConversation";
10
  import { ERROR_MESSAGES, error } from "$lib/stores/errors";
@@ -24,6 +24,7 @@
24
  import { createConvTreeStore } from "$lib/stores/convTree";
25
  import type { v4 } from "uuid";
26
  import { useSettingsStore } from "$lib/stores/settings.js";
 
27
 
28
  export let data;
29
 
@@ -247,7 +248,9 @@
247
  ) {
248
  $error = update.message ?? "An error has occurred";
249
  } else if (update.type === MessageUpdateType.Title) {
250
- const convInData = data.conversations.find(({ id }) => id === $page.params.id);
 
 
251
  if (convInData) {
252
  convInData.title = update.title;
253
 
@@ -280,7 +283,7 @@
280
  } finally {
281
  loading = false;
282
  pending = false;
283
- await invalidateAll();
284
  }
285
  }
286
 
@@ -376,14 +379,18 @@
376
  }
377
 
378
  $: $page.params.id, (($isAborted = true), (loading = false), ($convTreeStore.editing = null));
379
- $: title = data.conversations.find((conv) => conv.id === $page.params.id)?.title ?? data.title;
 
 
380
 
381
  const convTreeStore = createConvTreeStore();
382
  const settings = useSettingsStore();
383
  </script>
384
 
385
  <svelte:head>
386
- <title>{title}</title>
 
 
387
  <link
388
  rel="stylesheet"
389
  href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
 
4
  import { isAborted } from "$lib/stores/isAborted";
5
  import { onMount } from "svelte";
6
  import { page } from "$app/stores";
7
+ import { goto, invalidate } from "$app/navigation";
8
  import { base } from "$app/paths";
9
  import { shareConversation } from "$lib/shareConversation";
10
  import { ERROR_MESSAGES, error } from "$lib/stores/errors";
 
24
  import { createConvTreeStore } from "$lib/stores/convTree";
25
  import type { v4 } from "uuid";
26
  import { useSettingsStore } from "$lib/stores/settings.js";
27
+ import { UrlDependency } from "$lib/types/UrlDependency.js";
28
 
29
  export let data;
30
 
 
248
  ) {
249
  $error = update.message ?? "An error has occurred";
250
  } else if (update.type === MessageUpdateType.Title) {
251
+ const convInData = await data.conversations.then((convs) =>
252
+ convs.find(({ id }) => id === $page.params.id)
253
+ );
254
  if (convInData) {
255
  convInData.title = update.title;
256
 
 
283
  } finally {
284
  loading = false;
285
  pending = false;
286
+ await invalidate(UrlDependency.Conversation);
287
  }
288
  }
289
 
 
379
  }
380
 
381
  $: $page.params.id, (($isAborted = true), (loading = false), ($convTreeStore.editing = null));
382
+ $: title = data.conversations.then(
383
+ (convs) => convs.find((conv) => conv.id === $page.params.id)?.title ?? data.title
384
+ );
385
 
386
  const convTreeStore = createConvTreeStore();
387
  const settings = useSettingsStore();
388
  </script>
389
 
390
  <svelte:head>
391
+ {#await title then title}
392
+ <title>{title}</title>
393
+ {/await}
394
  <link
395
  rel="stylesheet"
396
  href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte CHANGED
@@ -103,7 +103,7 @@
103
  >
104
  <div class="w-full sm:w-auto">
105
  <button
106
- class="mx-auto my-2 flex w-full w-min items-center justify-center rounded-full bg-black px-3 py-1 text-base !text-white"
107
  name="Activate model"
108
  on:click|stopPropagation={() => {
109
  settings.instantSet({
 
103
  >
104
  <div class="w-full sm:w-auto">
105
  <button
106
+ class="mx-auto my-2 flex w-min items-center justify-center rounded-full bg-black px-3 py-1 text-base !text-white"
107
  name="Activate model"
108
  on:click|stopPropagation={() => {
109
  settings.instantSet({
src/routes/settings/+layout.server.ts CHANGED
@@ -18,7 +18,7 @@ export const load = (async ({ locals, parent }) => {
18
  }
19
 
20
  return {
21
- assistants: assistants.map((el) => ({
22
  ...el,
23
  reported: reportsByUser.includes(el._id),
24
  })),
 
18
  }
19
 
20
  return {
21
+ assistants: (await assistants).map((el) => ({
22
  ...el,
23
  reported: reportsByUser.includes(el._id),
24
  })),