File size: 7,229 Bytes
a04c5e9 161c8f7 56c16ce 4331e77 008f64c 70b0659 008f64c 31daf3d e0ec969 16a2ea6 cafb235 8cabca8 4331e77 7cca7e9 007aca7 7d7a53f 308e215 9f870c5 e67ab0e 7d7a53f 161c8f7 21b8785 4331e77 21b8785 70b0659 4331e77 7cca7e9 21b8785 7cca7e9 9f870c5 70b0659 9f870c5 70b0659 3ee7730 21b8785 1ef4451 3ee7730 1ef4451 3ee7730 1ef4451 21b8785 3ee7730 4331e77 7cca7e9 7d7a53f 308e215 4331e77 7cca7e9 21b8785 007aca7 008f64c e67ab0e 008f64c 161c8f7 f17bf16 0e5ff83 4331e77 31daf3d 0e5ff83 4331e77 31daf3d 9e4f6c5 4331e77 4411049 4331e77 161c8f7 4331e77 56c16ce 4331e77 56c16ce 4331e77 161c8f7 56c16ce 4331e77 56c16ce 16a2ea6 a14217e 3ee7730 40f346c 16a2ea6 40f346c f80231f 4a17eaf ceff9ca f80231f a14217e 40f346c 4331e77 9f870c5 4331e77 b8228c1 4331e77 29f04a4 e67ab0e a449b8a 007aca7 dd75b82 007aca7 9f870c5 007aca7 4331e77 007aca7 008f64c 4331e77 007aca7 4331e77 007aca7 161c8f7 e67ab0e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
<script lang="ts" module>
export const titles: { [key: string]: string } = {
today: "Today",
week: "This week",
month: "This month",
older: "Older",
} as const;
</script>
<script lang="ts">
import { base } from "$app/paths";
import Logo from "$lib/components/icons/Logo.svelte";
import IconSun from "$lib/components/icons/IconSun.svelte";
import IconMoon from "$lib/components/icons/IconMoon.svelte";
import { switchTheme, subscribeToTheme } from "$lib/switchTheme";
import { isAborted } from "$lib/stores/isAborted";
import { onDestroy } from "svelte";
import NavConversationItem from "./NavConversationItem.svelte";
import type { LayoutData } from "../../routes/$types";
import type { ConvSidebar } from "$lib/types/ConvSidebar";
import type { Model } from "$lib/types/Model";
import { page } from "$app/state";
import InfiniteScroll from "./InfiniteScroll.svelte";
import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
import { browser } from "$app/environment";
import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
import { useAPIClient, handleResponse } from "$lib/APIClient";
import { requireAuthUser } from "$lib/utils/auth";
import { enabledServersCount } from "$lib/stores/mcpServers";
import MCPServerManager from "./mcp/MCPServerManager.svelte";
const publicConfig = usePublicConfig();
const client = useAPIClient();
interface Props {
conversations: ConvSidebar[];
user: LayoutData["user"];
p?: number;
ondeleteConversation?: (id: string) => void;
oneditConversationTitle?: (payload: { id: string; title: string }) => void;
}
let {
conversations = $bindable(),
user,
p = $bindable(0),
ondeleteConversation,
oneditConversationTitle,
}: Props = $props();
let hasMore = $state(true);
function handleNewChatClick(e: MouseEvent) {
isAborted.set(true);
if (requireAuthUser()) {
e.preventDefault();
}
}
function handleNavItemClick(e: MouseEvent) {
if (requireAuthUser()) {
e.preventDefault();
}
}
const dateRanges = [
new Date().setDate(new Date().getDate() - 1),
new Date().setDate(new Date().getDate() - 7),
new Date().setMonth(new Date().getMonth() - 1),
];
let groupedConversations = $derived({
today: conversations.filter(({ updatedAt }) => updatedAt.getTime() > dateRanges[0]),
week: conversations.filter(
({ updatedAt }) => updatedAt.getTime() > dateRanges[1] && updatedAt.getTime() < dateRanges[0]
),
month: conversations.filter(
({ updatedAt }) => updatedAt.getTime() > dateRanges[2] && updatedAt.getTime() < dateRanges[1]
),
older: conversations.filter(({ updatedAt }) => updatedAt.getTime() < dateRanges[2]),
});
const nModels: number = page.data.models.filter((el: Model) => !el.unlisted).length;
async function handleVisible() {
p++;
const newConvs = await client.conversations
.get({
query: {
p,
},
})
.then(handleResponse)
.then((r) => r.conversations)
.catch((): ConvSidebar[] => []);
if (newConvs.length === 0) {
hasMore = false;
}
conversations = [...conversations, ...newConvs];
}
$effect(() => {
if (conversations.length <= CONV_NUM_PER_PAGE) {
// reset p to 0 if there's only one page of content
// that would be caused by a data loading invalidation
p = 0;
}
});
let isDark = $state(false);
let unsubscribeTheme: (() => void) | undefined;
let showMcpModal = $state(false);
if (browser) {
unsubscribeTheme = subscribeToTheme(({ isDark: nextIsDark }) => {
isDark = nextIsDark;
});
}
onDestroy(() => {
unsubscribeTheme?.();
});
</script>
<div
class="sticky top-0 flex flex-none touch-none items-center justify-between px-1.5 py-3.5 max-sm:pt-0"
>
<a
class="flex select-none items-center rounded-xl text-lg font-semibold"
href="{publicConfig.PUBLIC_ORIGIN}{base}/"
>
<Logo classNames="dark:invert mr-[2px]" />
{publicConfig.PUBLIC_APP_NAME}
</a>
<a
href={`${base}/`}
onclick={handleNewChatClick}
class="flex rounded-lg border bg-white px-2 py-0.5 text-center shadow-sm hover:shadow-none dark:border-gray-600 dark:bg-gray-700 sm:text-smd"
title="Ctrl/Cmd + Shift + O"
>
New Chat
</a>
</div>
<div
class="scrollbar-custom flex touch-pan-y flex-col gap-1 overflow-y-auto rounded-r-xl border border-l-0 border-gray-100 from-gray-50 px-3 pb-3 pt-2 text-[.9rem] dark:border-transparent dark:from-gray-800/30 max-sm:bg-gradient-to-t md:bg-gradient-to-l"
>
<div class="flex flex-col gap-0.5">
{#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 {conv} {oneditConversationTitle} {ondeleteConversation} />
{/each}
{/if}
{/each}
</div>
{#if hasMore}
<InfiniteScroll onvisible={handleVisible} />
{/if}
</div>
<div
class="flex touch-none flex-col gap-1 rounded-r-xl border border-l-0 border-gray-100 p-3 text-sm dark:border-transparent md:mt-3 md:bg-gradient-to-l md:from-gray-50 md:dark:from-gray-800/30"
>
{#if user?.username || user?.email}
<div
class="group flex items-center gap-1.5 rounded-lg pl-2.5 pr-2 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<span
class="flex h-9 flex-none shrink items-center gap-1.5 truncate pr-2 text-gray-500 dark:text-gray-400"
>{user?.username || user?.email}</span
>
<img
src="https://huggingface.co/api/users/{user.username}/avatar?redirect=true"
class="ml-auto size-4 rounded-full border bg-gray-500 dark:border-white/40"
alt=""
/>
</div>
{/if}
<a
href="{base}/models"
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
onclick={handleNavItemClick}
>
Models
<span
class="ml-auto rounded-md bg-gray-500/5 px-1.5 py-0.5 text-xs text-gray-400 dark:bg-gray-500/20 dark:text-gray-400"
>{nModels}</span
>
</a>
{#if user?.username || user?.email}
<button
onclick={() => (showMcpModal = true)}
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
>
MCP Servers
{#if $enabledServersCount > 0}
<span
class="ml-auto rounded-md bg-blue-600/10 px-1.5 py-0.5 text-xs text-blue-600 dark:bg-blue-600/20 dark:text-blue-400"
>
{$enabledServersCount}
</span>
{/if}
</button>
{/if}
<span class="flex gap-1">
<a
href="{base}/settings/application"
class="flex h-9 flex-none flex-grow items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
onclick={handleNavItemClick}
>
Settings
</a>
<button
onclick={() => {
switchTheme();
}}
aria-label="Toggle theme"
class="flex size-9 min-w-[1.5em] flex-none items-center justify-center rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
>
{#if browser}
{#if isDark}
<IconSun />
{:else}
<IconMoon />
{/if}
{/if}
</button>
</span>
</div>
{#if showMcpModal}
<MCPServerManager onclose={() => (showMcpModal = false)} />
{/if}
|