Skip to content

Commit

Permalink
feat: lazy stream conversations load function (#1553)
Browse files Browse the repository at this point in the history
* feat: lazy stream conversations load function

* fix: lint

* feat: add animation

* feat: skip db call if no convs
  • Loading branch information
nsarrazin authored Nov 5, 2024
1 parent c9703f4 commit 0cbb862
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 86 deletions.
8 changes: 6 additions & 2 deletions src/lib/components/MobileNav.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import IconNew from "$lib/components/icons/IconNew.svelte";
export let isOpen = false;
export let title: string | undefined;
export let title: Promise<string | undefined> | string;
$: title = title ?? "New Chat";
Expand Down Expand Up @@ -40,7 +40,11 @@
aria-label="Open menu"
bind:this={openEl}><CarbonTextAlignJustify /></button
>
<span class="truncate px-4">{title}</span>
{#await title}
<div class="flex h-full items-center justify-center" />
{:then title}
<span class="truncate px-4">{title ?? ""}</span>
{/await}
<a
class:invisible={!$page.params.id}
href="{base}/"
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/NavConversationItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
{#if confirmDelete}
<span class="mr-1 font-semibold"> Delete </span>
{/if}
{#if conv.avatarHash}
{#if conv.avatarUrl}
<img
src="{base}/settings/assistants/{conv.assistantId}/avatar.jpg?hash={conv.avatarHash}"
src="{base}{conv.avatarUrl}"
alt="Assistant avatar"
class="mr-1.5 inline size-4 flex-none rounded-full object-cover"
/>
Expand Down
48 changes: 32 additions & 16 deletions src/lib/components/NavMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
import type { Model } from "$lib/types/Model";
import { page } from "$app/stores";
export let conversations: ConvSidebar[] = [];
import { fade } from "svelte/transition";
export let conversations: Promise<ConvSidebar[]>;
export let canLogin: boolean;
export let user: LayoutData["user"];
Expand All @@ -25,16 +26,16 @@
new Date().setMonth(new Date().getMonth() - 1),
];
$: groupedConversations = {
today: conversations.filter(({ updatedAt }) => updatedAt.getTime() > dateRanges[0]),
week: conversations.filter(
$: groupedConversations = conversations.then((convs) => ({
today: convs.filter(({ updatedAt }) => updatedAt.getTime() > dateRanges[0]),
week: convs.filter(
({ updatedAt }) => updatedAt.getTime() > dateRanges[1] && updatedAt.getTime() < dateRanges[0]
),
month: conversations.filter(
month: convs.filter(
({ updatedAt }) => updatedAt.getTime() > dateRanges[2] && updatedAt.getTime() < dateRanges[1]
),
older: conversations.filter(({ updatedAt }) => updatedAt.getTime() < dateRanges[2]),
};
older: convs.filter(({ updatedAt }) => updatedAt.getTime() < dateRanges[2]),
}));
const titles: { [key: string]: string } = {
today: "Today",
Expand Down Expand Up @@ -65,16 +66,31 @@
<div
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"
>
{#each Object.entries(groupedConversations) as [group, convs]}
{#if convs.length}
<h4 class="mb-1.5 mt-4 pl-0.5 text-sm text-gray-400 first:mt-0 dark:text-gray-500">
{titles[group]}
</h4>
{#each convs as conv}
<NavConversationItem on:editConversationTitle on:deleteConversation {conv} />
{/each}
{#await groupedConversations}
{#if $page.data.nConversations > 0}
<div class="overflow-y-hidden">
<div class="flex animate-pulse flex-col gap-4">
<div class="h-4 w-24 rounded bg-gray-200 dark:bg-gray-700" />
{#each Array(100) as _}
<div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700" />
{/each}
</div>
</div>
{/if}
{/each}
{:then groupedConversations}
<div transition:fade class="flex flex-col gap-1">
{#each Object.entries(groupedConversations) as [group, convs]}
{#if convs.length}
<h4 class="mb-1.5 mt-4 pl-0.5 text-sm text-gray-400 first:mt-0 dark:text-gray-500">
{titles[group]}
</h4>
{#each convs as conv}
<NavConversationItem on:editConversationTitle on:deleteConversation {conv} />
{/each}
{/if}
{/each}
</div>
{/await}
</div>
<div
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"
Expand Down
2 changes: 1 addition & 1 deletion src/lib/types/ConvSidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export interface ConvSidebar {
updatedAt: Date;
model?: string;
assistantId?: string;
avatarHash?: string;
avatarUrl?: string;
}
133 changes: 83 additions & 50 deletions src/routes/+layout.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,38 +51,52 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
})
: null;

const conversations = await collections.conversations
.find(authCondition(locals))
.sort({ updatedAt: -1 })
.project<
Pick<Conversation, "title" | "model" | "_id" | "updatedAt" | "createdAt" | "assistantId">
>({
title: 1,
model: 1,
_id: 1,
updatedAt: 1,
createdAt: 1,
assistantId: 1,
})
.limit(300)
.toArray();
const nConversations = await collections.conversations.countDocuments(authCondition(locals));

const conversations =
nConversations === 0
? Promise.resolve([])
: collections.conversations
.find(authCondition(locals))
.sort({ updatedAt: -1 })
.project<
Pick<
Conversation,
"title" | "model" | "_id" | "updatedAt" | "createdAt" | "assistantId"
>
>({
title: 1,
model: 1,
_id: 1,
updatedAt: 1,
createdAt: 1,
assistantId: 1,
})
.limit(300)
.toArray();

const userAssistants = settings?.assistants?.map((assistantId) => assistantId.toString()) ?? [];
const userAssistantsSet = new Set(userAssistants);

const assistantIds = [
...userAssistants.map((el) => new ObjectId(el)),
...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]),
];

const assistants = await collections.assistants.find({ _id: { $in: assistantIds } }).toArray();
const assistants = conversations.then((conversations) =>
collections.assistants
.find({
_id: {
$in: [
...userAssistants.map((el) => new ObjectId(el)),
...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]),
],
},
})
.toArray()
);

const messagesBeforeLogin = env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0;

let loginRequired = false;

if (requiresUser && !locals.user && messagesBeforeLogin) {
if (conversations.length > messagesBeforeLogin) {
if (nConversations > messagesBeforeLogin) {
loginRequired = true;
} else {
// get the number of messages where `from === "assistant"` across all conversations.
Expand Down Expand Up @@ -129,25 +143,42 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
);

return {
conversations: conversations.map((conv) => {
if (settings?.hideEmojiOnSidebar) {
conv.title = conv.title.replace(/\p{Emoji}/gu, "");
}

// remove invalid unicode and trim whitespaces
conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart();

return {
id: conv._id.toString(),
title: conv.title,
model: conv.model ?? defaultModel,
updatedAt: conv.updatedAt,
assistantId: conv.assistantId?.toString(),
avatarHash:
conv.assistantId &&
assistants.find((a) => a._id.toString() === conv.assistantId?.toString())?.avatar,
};
}) satisfies ConvSidebar[],
nConversations,
conversations: conversations.then(
async (convs) =>
await Promise.all(
convs.map(async (conv) => {
if (settings?.hideEmojiOnSidebar) {
conv.title = conv.title.replace(/\p{Emoji}/gu, "");
}

// remove invalid unicode and trim whitespaces
conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart();

let avatarUrl: string | undefined = undefined;

if (conv.assistantId) {
const hash = (
await collections.assistants.findOne({
_id: new ObjectId(conv.assistantId),
})
)?.avatar;
if (hash) {
avatarUrl = `/settings/assistants/${conv.assistantId}/avatar.jpg?hash=${hash}`;
}
}

return {
id: conv._id.toString(),
title: conv.title,
model: conv.model ?? defaultModel,
updatedAt: conv.updatedAt,
assistantId: conv.assistantId?.toString(),
avatarUrl,
} satisfies ConvSidebar;
})
)
),
settings: {
searchEnabled: !!(
env.SERPAPI_KEY ||
Expand Down Expand Up @@ -223,15 +254,17 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
type: "community",
review: ReviewStatus.APPROVED,
}),
assistants: assistants
.filter((el) => userAssistantsSet.has(el._id.toString()))
.map((el) => ({
...el,
_id: el._id.toString(),
createdById: undefined,
createdByMe:
el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(),
})),
assistants: assistants.then((assistants) =>
assistants
.filter((el) => userAssistantsSet.has(el._id.toString()))
.map((el) => ({
...el,
_id: el._id.toString(),
createdById: undefined,
createdByMe:
el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(),
}))
),
user: locals.user && {
id: locals.user._id.toString(),
username: locals.user.username,
Expand Down
18 changes: 10 additions & 8 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,17 @@
$: if ($error) onError();
$: if ($titleUpdate) {
const convIdx = data.conversations.findIndex(({ id }) => id === $titleUpdate?.convId);
data.conversations.then((convs) => {
const convIdx = convs.findIndex(({ id }) => id === $titleUpdate?.convId);
if (convIdx != -1) {
data.conversations[convIdx].title = $titleUpdate?.title ?? data.conversations[convIdx].title;
}
// update data.conversations
data.conversations = [...data.conversations];
if (convIdx != -1) {
convs[convIdx].title = $titleUpdate?.title ?? convs[convIdx].title;
}
// update data.conversations
data.conversations = Promise.resolve([...convs]);
$titleUpdate = null;
$titleUpdate = null;
});
}
const settings = createSettingsStore(data.settings);
Expand Down Expand Up @@ -147,7 +149,7 @@
$: mobileNavTitle = ["/models", "/assistants", "/privacy"].includes($page.route.id ?? "")
? ""
: data.conversations.find((conv) => conv.id === $page.params.id)?.title;
: data.conversations.then((convs) => convs.find((conv) => conv.id === $page.params.id)?.title);
</script>

<svelte:head>
Expand Down
17 changes: 12 additions & 5 deletions src/routes/conversation/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { isAborted } from "$lib/stores/isAborted";
import { onMount } from "svelte";
import { page } from "$app/stores";
import { goto, invalidateAll } from "$app/navigation";
import { goto, invalidate } from "$app/navigation";
import { base } from "$app/paths";
import { shareConversation } from "$lib/shareConversation";
import { ERROR_MESSAGES, error } from "$lib/stores/errors";
Expand All @@ -24,6 +24,7 @@
import { createConvTreeStore } from "$lib/stores/convTree";
import type { v4 } from "uuid";
import { useSettingsStore } from "$lib/stores/settings.js";
import { UrlDependency } from "$lib/types/UrlDependency.js";
export let data;
Expand Down Expand Up @@ -247,7 +248,9 @@
) {
$error = update.message ?? "An error has occurred";
} else if (update.type === MessageUpdateType.Title) {
const convInData = data.conversations.find(({ id }) => id === $page.params.id);
const convInData = await data.conversations.then((convs) =>
convs.find(({ id }) => id === $page.params.id)
);
if (convInData) {
convInData.title = update.title;
Expand Down Expand Up @@ -280,7 +283,7 @@
} finally {
loading = false;
pending = false;
await invalidateAll();
await invalidate(UrlDependency.Conversation);
}
}
Expand Down Expand Up @@ -376,14 +379,18 @@
}
$: $page.params.id, (($isAborted = true), (loading = false), ($convTreeStore.editing = null));
$: title = data.conversations.find((conv) => conv.id === $page.params.id)?.title ?? data.title;
$: title = data.conversations.then(
(convs) => convs.find((conv) => conv.id === $page.params.id)?.title ?? data.title
);
const convTreeStore = createConvTreeStore();
const settings = useSettingsStore();
</script>

<svelte:head>
<title>{title}</title>
{#await title then title}
<title>{title}</title>
{/await}
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
>
<div class="w-full sm:w-auto">
<button
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"
class="mx-auto my-2 flex w-min items-center justify-center rounded-full bg-black px-3 py-1 text-base !text-white"
name="Activate model"
on:click|stopPropagation={() => {
settings.instantSet({
Expand Down
2 changes: 1 addition & 1 deletion src/routes/settings/+layout.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const load = (async ({ locals, parent }) => {
}

return {
assistants: assistants.map((el) => ({
assistants: (await assistants).map((el) => ({
...el,
reported: reportsByUser.includes(el._id),
})),
Expand Down

0 comments on commit 0cbb862

Please sign in to comment.