Spaces:
Running
Running
File size: 8,492 Bytes
f3adf00 e88d77a a1a6daf 1dae9c1 98051f8 e88d77a a8a9533 e88d77a ffb0dba af319d7 2ec957c ffb0dba e88d77a c6896b8 df3243b e88d77a 14ef8d0 73de8f0 7086abd a1a6daf 8aab1a5 a1a6daf e88d77a 73de8f0 59de250 a1a6daf ffb0dba a1a6daf ffb0dba 73de8f0 ffb0dba a1a6daf 973387a ffb0dba e3c83d9 308b54f bd8f105 98051f8 308b54f 98051f8 308b54f ffb0dba 308b54f a1a6daf 1dae9c1 d72b096 308b54f ffb0dba 308b54f ffb0dba 78cdca3 a1a6daf 78cdca3 ffb0dba 8b18180 a1a6daf 3c110ed 8b18180 a1a6daf c6896b8 a1a6daf c6896b8 a1a6daf e99e7c2 48288dc cc54380 48288dc cc54380 7af80c2 a1a6daf b4492f3 a1a6daf b4492f3 a1a6daf f3adf00 a89f288 a8a9533 a89f288 6a95eb2 df3243b 6e16d44 a8a9533 df3243b a8a9533 df3243b a8a9533 df3243b a8a9533 df3243b 67577d7 a8a9533 922a0f7 67577d7 a8a9533 922a0f7 67577d7 a8a9533 67577d7 2acfeb9 a8a9533 2acfeb9 7affb80 a8a9533 7affb80 a8a9533 7affb80 845c6da a8a9533 845c6da a89f288 b4492f3 7bcf4d9 df3243b 14ef8d0 73de8f0 8aab1a5 94ed0cf e7b3dd5 06feee8 8aab1a5 caf5a3e a1a6daf caf5a3e 6c59df2 7086abd a1a6daf 566c2fc cc38b64 7086abd 78cdca3 7086abd e88d77a caf5a3e e88d77a 7086abd a1a6daf 566c2fc cc38b64 7086abd 78cdca3 7086abd 8aab1a5 ffb0dba a1a6daf 8aab1a5 |
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 |
<script lang="ts">
import "../styles/main.css";
import { onDestroy, onMount, untrack } from "svelte";
import { goto } from "$app/navigation";
import { base } from "$app/paths";
import { page } from "$app/stores";
import { env as envPublic } from "$env/dynamic/public";
import { error } from "$lib/stores/errors";
import { createSettingsStore } from "$lib/stores/settings";
import { shareConversation } from "$lib/shareConversation";
import Toast from "$lib/components/Toast.svelte";
import NavMenu from "$lib/components/NavMenu.svelte";
import MobileNav from "$lib/components/MobileNav.svelte";
import titleUpdate from "$lib/stores/titleUpdate";
import DisclaimerModal from "$lib/components/DisclaimerModal.svelte";
import ExpandNavigation from "$lib/components/ExpandNavigation.svelte";
import { loginModalOpen } from "$lib/stores/loginModal";
import LoginModal from "$lib/components/LoginModal.svelte";
import OverloadedModal from "$lib/components/OverloadedModal.svelte";
import { isHuggingChat } from "$lib/utils/isHuggingChat";
let { data = $bindable(), children } = $props();
let conversations = $state(data.conversations);
$effect(() => {
data.conversations && untrack(() => (conversations = data.conversations));
});
let isNavCollapsed = $state(false);
let overloadedModalOpen = $state(false);
let errorToastTimeout: ReturnType<typeof setTimeout>;
let currentError: string | undefined = $state();
async function onError() {
// If a new different error comes, wait for the current error to hide first
if ($error && currentError && $error !== currentError) {
clearTimeout(errorToastTimeout);
currentError = undefined;
await new Promise((resolve) => setTimeout(resolve, 300));
}
currentError = $error;
if (currentError === "Model is overloaded") {
overloadedModalOpen = true;
}
errorToastTimeout = setTimeout(() => {
$error = undefined;
currentError = undefined;
}, 10000);
}
async function deleteConversation(id: string) {
try {
const res = await fetch(`${base}/conversation/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
$error = "Error while deleting conversation, try again.";
return;
}
conversations = conversations.filter((conv) => conv.id !== id);
if ($page.params.id === id) {
await goto(`${base}/`, { invalidateAll: true });
}
} catch (err) {
console.error(err);
$error = String(err);
}
}
async function editConversationTitle(id: string, title: string) {
try {
const res = await fetch(`${base}/conversation/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title }),
});
if (!res.ok) {
$error = "Error while editing title, try again.";
return;
}
conversations = conversations.map((conv) => (conv.id === id ? { ...conv, title } : conv));
} catch (err) {
console.error(err);
$error = String(err);
}
}
onDestroy(() => {
clearTimeout(errorToastTimeout);
});
$effect(() => {
if ($error) onError();
});
$effect(() => {
if ($titleUpdate) {
const convIdx = conversations.findIndex(({ id }) => id === $titleUpdate?.convId);
if (convIdx != -1) {
conversations[convIdx].title = $titleUpdate?.title ?? conversations[convIdx].title;
}
$titleUpdate = null;
}
});
const settings = createSettingsStore(data.settings);
onMount(async () => {
if ($page.url.searchParams.has("model")) {
await settings
.instantSet({
activeModel: $page.url.searchParams.get("model") ?? $settings.activeModel,
})
.then(async () => {
const query = new URLSearchParams($page.url.searchParams.toString());
query.delete("model");
await goto(`${base}/?${query.toString()}`, {
invalidateAll: true,
});
});
}
if ($page.url.searchParams.has("tools")) {
const tools = $page.url.searchParams.get("tools")?.split(",");
await settings
.instantSet({
tools: [...($settings.tools ?? []), ...(tools ?? [])],
})
.then(async () => {
const query = new URLSearchParams($page.url.searchParams.toString());
query.delete("tools");
await goto(`${base}/?${query.toString()}`, {
invalidateAll: true,
});
});
}
});
let mobileNavTitle = $derived(
["/models", "/assistants", "/privacy", "/tools"].includes($page.route.id ?? "")
? ""
: conversations.find((conv) => conv.id === $page.params.id)?.title
);
let showDisclaimer = $derived(
!$settings.ethicsModalAccepted &&
$page.url.pathname !== `${base}/privacy` &&
envPublic.PUBLIC_APP_DISCLAIMER === "1" &&
!($page.data.shared === true)
);
</script>
<svelte:head>
<title>{envPublic.PUBLIC_APP_NAME}</title>
<meta name="description" content="The first open source alternative to ChatGPT. 💪" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@huggingface" />
<!-- use those meta tags everywhere except on the share assistant page -->
<!-- feel free to refacto if there's a better way -->
{#if !$page.url.pathname.includes("/assistant/") && $page.route.id !== "/assistants" && !$page.url.pathname.includes("/models/") && !$page.url.pathname.includes("/tools")}
<meta property="og:title" content={envPublic.PUBLIC_APP_NAME} />
<meta property="og:type" content="website" />
<meta property="og:url" content="{envPublic.PUBLIC_ORIGIN || $page.url.origin}{base}" />
<meta
property="og:image"
content="{envPublic.PUBLIC_ORIGIN ||
$page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/thumbnail.png"
/>
<meta property="og:description" content={envPublic.PUBLIC_APP_DESCRIPTION} />
{/if}
<link
rel="icon"
href="{envPublic.PUBLIC_ORIGIN ||
$page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/favicon.ico"
sizes="32x32"
/>
<link
rel="icon"
href="{envPublic.PUBLIC_ORIGIN ||
$page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/icon.svg"
type="image/svg+xml"
/>
<link
rel="apple-touch-icon"
href="{envPublic.PUBLIC_ORIGIN ||
$page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/apple-touch-icon.png"
/>
<link
rel="manifest"
href="{envPublic.PUBLIC_ORIGIN ||
$page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/manifest.json"
/>
{#if envPublic.PUBLIC_PLAUSIBLE_SCRIPT_URL && envPublic.PUBLIC_ORIGIN}
<script
defer
data-domain={new URL(envPublic.PUBLIC_ORIGIN).hostname}
src={envPublic.PUBLIC_PLAUSIBLE_SCRIPT_URL}
></script>
{/if}
{#if envPublic.PUBLIC_APPLE_APP_ID}
<meta name="apple-itunes-app" content={`app-id=${envPublic.PUBLIC_APPLE_APP_ID}`} />
{/if}
</svelte:head>
{#if showDisclaimer}
<DisclaimerModal on:close={() => ($settings.ethicsModalAccepted = true)} />
{/if}
{#if $loginModalOpen}
<LoginModal
on:close={() => {
$loginModalOpen = false;
}}
/>
{/if}
{#if overloadedModalOpen && isHuggingChat}
<OverloadedModal onClose={() => (overloadedModalOpen = false)} />
{/if}
<div
class="fixed grid h-full w-screen grid-cols-1 grid-rows-[auto,1fr] overflow-hidden text-smd {!isNavCollapsed
? 'md:grid-cols-[290px,1fr]'
: 'md:grid-cols-[0px,1fr]'} transition-[300ms] [transition-property:grid-template-columns] dark:text-gray-300 md:grid-rows-[1fr]"
>
<ExpandNavigation
isCollapsed={isNavCollapsed}
onClick={() => (isNavCollapsed = !isNavCollapsed)}
classNames="absolute inset-y-0 z-10 my-auto {!isNavCollapsed
? 'left-[290px]'
: 'left-0'} *:transition-transform"
/>
<MobileNav title={mobileNavTitle}>
<NavMenu
{conversations}
user={data.user}
canLogin={data.user === undefined && data.loginEnabled}
on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
on:deleteConversation={(ev) => deleteConversation(ev.detail)}
on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
/>
</MobileNav>
<nav
class="grid max-h-screen grid-cols-1 grid-rows-[auto,1fr,auto] overflow-hidden *:w-[290px] max-md:hidden"
>
<NavMenu
{conversations}
user={data.user}
canLogin={data.user === undefined && data.loginEnabled}
on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
on:deleteConversation={(ev) => deleteConversation(ev.detail)}
on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
/>
</nav>
{#if currentError}
<Toast message={currentError} />
{/if}
{@render children?.()}
</div>
|