Spaces:
Paused
Paused
File size: 10,053 Bytes
c6ccfa4 7086abd ee691dc 7086abd 4265c02 48059af dd5d9ad 018a142 df3243b b1253fd 797e348 5290cbb 612f16b 782a4fb c6ccfa4 612f16b 7e91987 5ad620b 612f16b 7086abd a1a6daf 4265c02 a1a6daf 5290cbb a1a6daf 5290cbb 4265c02 d90f53e a1a6daf 575fe85 d90f53e 575fe85 d90f53e 575fe85 a1a6daf d90f53e b1253fd 5290cbb 612f16b 5ad620b 5290cbb a1a6daf 782a4fb 7086abd 6c59df2 a8a9533 48059af a8a9533 67577d7 48059af 1a774d9 a1a6daf 7086abd ee691dc 6c59df2 ee691dc c6ccfa4 7e91987 c6ccfa4 7e91987 9d36148 a1a6daf 9d36148 a1a6daf 9d36148 d90f53e 9d36148 e8c0c11 9d36148 5290cbb 9d36148 7086abd ee691dc 782a4fb ee691dc 018a142 612f16b d90f53e 566c2fc 018a142 566c2fc f61126a 612f16b 06feee8 f61126a 612f16b f61126a 612f16b 566c2fc 6233bdc 612f16b 6233bdc b1253fd 57162d1 797e348 0c3e3b2 6a5e4c9 0c3e3b2 797e348 0c3e3b2 797e348 782a4fb c6ccfa4 782a4fb 7086abd | 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | <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 { switchTheme } from "$lib/switchTheme";
import { isAborted } from "$lib/stores/isAborted";
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/stores";
import InfiniteScroll from "./InfiniteScroll.svelte";
import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
import { goto } from "$app/navigation";
import { browser } from "$app/environment";
import { toggleSearch } from "./chat/Search.svelte";
import CarbonSearch from "~icons/carbon/search";
import { closeMobileNav } from "./MobileNav.svelte";
import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
import { useAPIClient, handleResponse } from "$lib/APIClient";
const publicConfig = usePublicConfig();
const client = useAPIClient();
interface Props {
conversations: ConvSidebar[];
canLogin: boolean;
user: LayoutData["user"];
p?: number;
}
let { conversations = $bindable(), canLogin, user, p = $bindable(0) }: Props = $props();
let hasMore = $state(true);
function handleNewChatClick() {
isAborted.set(true);
}
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(() => []);
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 theme = $state(browser ? localStorage.theme : "light");
</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 items-center rounded-xl text-lg font-semibold"
href="{publicConfig.PUBLIC_ORIGIN}{base}/"
>
<Logo classNames="mr-1" />
{publicConfig.PUBLIC_APP_NAME}
</a>
{#if $page.url.pathname !== base + "/"}
<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"
>
New Chat
</a>
{/if}
</div>
<div
class="scrollbar-custom flex touch-pan-y 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"
>
<button
class="group mx-auto flex w-full flex-row items-center justify-stretch gap-x-2 rounded-xl px-2 py-1 align-middle text-gray-600 hover:bg-gray-500/20 dark:text-gray-400"
onclick={() => {
closeMobileNav();
toggleSearch();
}}
>
<CarbonSearch class="text-xs" />
<span class="block">Search chats</span>
{#if !isVirtualKeyboard()}
<span class="invisible ml-auto text-xs text-gray-500 group-hover:visible"
><kbd>ctrl</kbd>+<kbd>k</kbd></span
>
{/if}
</button>
{#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"></div>
{#each Array(100) as _}
<div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700"></div>
{/each}
</div>
</div>
{/if}
{:then groupedConversations}
<div 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>
{#if hasMore}
<InfiniteScroll on:visible={handleVisible} />
{/if}
{/await}
</div>
<div
class="flex touch-none flex-col gap-1 rounded-r-xl p-3 text-sm md:mt-3 md:bg-gradient-to-l md:from-gray-50 md:dark:from-gray-800/30"
>
{#if user?.username || user?.email}
<button
onclick={async () => {
await fetch(`${base}/logout`, {
method: "POST",
});
await goto(base + "/", { invalidateAll: true });
}}
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
>
{#if !user.logoutDisabled}
<span
class="ml-auto h-6 flex-none items-center gap-1.5 rounded-md border bg-white px-2 text-gray-700 shadow-sm group-hover:flex hover:shadow-none dark:border-gray-600 dark:bg-gray-600 dark:text-gray-400 dark:hover:text-gray-300 md:hidden"
>
Sign Out
</span>
{/if}
</button>
{/if}
{#if canLogin}
<a
href="{base}/login"
class="flex h-9 w-full 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"
>
Login
</a>
{/if}
{#if nModels > 1}
<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"
>
Models
<span
class="ml-auto rounded-full border border-gray-300 px-2 py-0.5 text-xs text-gray-500 dark:border-gray-500 dark:text-gray-400"
>{nModels}</span
>
</a>
{/if}
{#if $page.data.enableAssistants}
<a
href="{base}/assistants"
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"
>
Assistants
</a>
{/if}
{#if $page.data.enableCommunityTools}
<a
href="{base}/tools"
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"
>
Tools
<span
class="ml-auto rounded-full border border-purple-300 px-2 py-0.5 text-xs text-purple-500 dark:border-purple-500 dark:text-purple-400"
>New</span
>
</a>
{/if}
<span class="flex flex-row-reverse gap-1 md:flex-row">
<a
href="{base}/settings"
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"
>
Settings
</a>
<button
onclick={() => {
switchTheme();
theme = localStorage.theme;
}}
aria-label="Toggle theme"
class="flex h-9 min-w-[1.5em] flex-none items-center rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
>
{#if browser}
{#if theme === "dark"}
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
focusable="false"
role="img"
width="1em"
height="1em"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 32 32"
stroke-width="1.5"
><path
d="M16 12.005a4 4 0 1 1-4 4a4.005 4.005 0 0 1 4-4m0-2a6 6 0 1 0 6 6a6 6 0 0 0-6-6z"
fill="currentColor"
stroke="currentColor"
stroke-width="0.5"
></path><path d="M5.394 6.813l1.414-1.415l3.506 3.506L8.9 10.318z" fill="currentColor"
></path><path d="M2 15.005h5v2H2z" fill="currentColor"></path><path
stroke="currentColor"
stroke-width="0.5"
d="M5.394 25.197L8.9 21.691l1.414 1.415l-3.506 3.505z"
fill="currentColor"
></path><path d="M15 25.005h2v5h-2z" fill="currentColor"></path><path
stroke="currentColor"
stroke-width="0.5"
d="M21.687 23.106l1.414-1.415l3.506 3.506l-1.414 1.414z"
fill="currentColor"
></path><path d="M25 15.005h5v2h-5z" fill="currentColor"></path><path
stroke="currentColor"
stroke-width="0.5"
d="M21.687 8.904l3.506-3.506l1.414 1.415l-3.506 3.505z"
fill="currentColor"
></path><path d="M15 2.005h2v5h-2z" fill="currentColor"></path></svg
>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
focusable="false"
role="img"
width="1em"
height="1em"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 32 32"
stroke-width="1.5"
><path
d="M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194a11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1.002 1.002 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3z"
fill="currentColor"
stroke="currentColor"
stroke-width="0.5"
></path></svg
>
{/if}
{/if}
</button>
</span>
</div>
|