2023-11-20 01:47:07 +00:00
< script lang = "ts" >
2024-03-01 09:18:07 +00:00
import { toast } from 'svelte-sonner';
2024-03-02 20:38:51 +00:00
import { onMount , tick , getContext } from 'svelte';
2023-12-27 02:44:08 +00:00
import { openDB , deleteDB } from 'idb';
2023-12-27 00:35:01 +00:00
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
2024-02-23 08:47:54 +00:00
import { goto } from '$app/navigation';
2024-10-02 00:35:35 +00:00
import { page } from '$app/stores';
import { fade } from 'svelte/transition';
2024-02-23 08:47:54 +00:00
2025-03-27 09:27:56 +00:00
import { getModels , getToolServersData , getVersionUpdates } from '$lib/apis';
2024-06-11 03:58:47 +00:00
import { getTools } from '$lib/apis/tools';
import { getBanners } from '$lib/apis/configs';
import { getUserSettings } from '$lib/apis/users';
2023-12-26 20:50:52 +00:00
2024-10-02 00:35:35 +00:00
import { WEBUI_VERSION } from '$lib/constants';
import { compareVersion } from '$lib/utils';
2024-01-08 09:26:15 +00:00
import {
2024-10-02 00:35:35 +00:00
config,
2024-01-08 09:26:15 +00:00
user,
settings,
models,
prompts,
2024-10-02 05:45:04 +00:00
knowledge,
2024-10-02 00:35:35 +00:00
tools,
functions,
2024-02-23 08:47:54 +00:00
tags,
2024-05-26 19:18:43 +00:00
banners,
2024-10-02 00:35:35 +00:00
showSettings,
2025-07-18 08:47:22 +00:00
showShortcuts,
2024-02-23 08:47:54 +00:00
showChangelog,
2025-03-27 09:27:56 +00:00
temporaryChatEnabled,
2025-05-18 21:39:33 +00:00
toolServers,
2025-08-08 09:09:40 +00:00
showSearch,
showSidebar
2024-01-08 09:26:15 +00:00
} from '$lib/stores';
2023-12-26 21:10:50 +00:00
2023-12-26 20:50:52 +00:00
import Sidebar from '$lib/components/layout/Sidebar.svelte';
2024-10-02 00:35:35 +00:00
import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
2024-02-23 08:30:26 +00:00
import ChangelogModal from '$lib/components/ChangelogModal.svelte';
2024-06-04 04:17:43 +00:00
import AccountPending from '$lib/components/layout/Overlay/AccountPending.svelte';
2024-09-24 10:40:13 +00:00
import UpdateInfoToast from '$lib/components/layout/UpdateInfoToast.svelte';
2025-04-10 16:15:08 +00:00
import Spinner from '$lib/components/common/Spinner.svelte';
2025-10-12 00:59:08 +00:00
import { Shortcut , shortcuts } from '$lib/shortcuts';
2023-12-26 20:50:52 +00:00
2024-03-02 20:38:51 +00:00
const i18n = getContext('i18n');
2023-11-19 05:41:43 +00:00
let loaded = false;
2023-12-27 01:09:24 +00:00
let DB = null;
let localDBChats = [];
2024-09-26 02:05:28 +00:00
let version;
2025-10-02 18:26:41 +00:00
const clearChatInputStorage = () => {
2025-10-02 15:23:43 +00:00
const chatInputKeys = Object.keys(localStorage).filter((key) => key.startsWith('chat-input'));
if (chatInputKeys.length > 0) {
chatInputKeys.forEach((key) => {
localStorage.removeItem(key);
});
}
2025-10-02 18:26:41 +00:00
};
2025-10-02 15:23:43 +00:00
2025-10-02 18:26:41 +00:00
const checkLocalDBChats = async () => {
try {
// Check if IndexedDB exists
DB = await openDB('Chats', 1);
2023-12-27 00:35:01 +00:00
2025-10-02 18:26:41 +00:00
if (!DB) {
return;
}
2023-12-27 00:35:01 +00:00
2025-10-02 18:26:41 +00:00
const chats = await DB.getAllFromIndex('chats', 'timestamp');
localDBChats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
2025-10-02 15:23:43 +00:00
2025-10-02 18:26:41 +00:00
if (localDBChats.length === 0) {
await deleteDB('Chats');
2023-12-27 02:44:08 +00:00
}
2025-10-02 18:26:41 +00:00
} catch (error) {
// IndexedDB Not Found
}
};
2023-12-27 00:35:01 +00:00
2025-10-14 11:43:51 +00:00
const setUserSettings = async (cb: () => Promise< void > ) => {
let userSettings = await getUserSettings(localStorage.token).catch((error) => {
2025-10-02 18:26:41 +00:00
console.error(error);
return null;
});
2025-10-14 11:43:51 +00:00
if (!userSettings) {
try {
userSettings = JSON.parse(localStorage.getItem('settings') ?? '{} ');
} catch (e: unknown) {
console.error('Failed to parse settings from localStorage', e);
userSettings = {} ;
2025-10-02 15:23:43 +00:00
}
2025-10-02 18:26:41 +00:00
}
2024-08-25 00:34:39 +00:00
2025-10-14 11:43:51 +00:00
if (userSettings?.ui) {
settings.set(userSettings.ui);
}
if (cb) {
await cb();
2025-10-02 18:26:41 +00:00
}
};
const setModels = async () => {
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections ? ($settings?.directConnections ?? null) : null
)
);
};
const setToolServers = async () => {
let toolServersData = await getToolServersData($settings?.toolServers ?? []);
toolServersData = toolServersData.filter((data) => {
if (!data || data.error) {
toast.error(
$i18n.t(`Failed to connect to {{ URL }} OpenAPI tool server`, {
URL: data?.url
})
);
return false;
2024-05-27 05:47:42 +00:00
}
2025-10-02 18:26:41 +00:00
return true;
});
toolServers.set(toolServersData);
};
2025-10-02 15:23:43 +00:00
2025-10-02 18:26:41 +00:00
const setBanners = async () => {
const bannersData = await getBanners(localStorage.token);
banners.set(bannersData);
};
2025-10-02 15:23:43 +00:00
2025-10-02 18:26:41 +00:00
const setTools = async () => {
const toolsData = await getTools(localStorage.token);
tools.set(toolsData);
};
onMount(async () => {
if ($user === undefined || $user === null) {
await goto('/auth');
return;
}
if (!['user', 'admin'].includes($user?.role)) {
return;
}
2025-10-02 15:23:43 +00:00
2025-10-02 18:26:41 +00:00
clearChatInputStorage();
2025-10-02 15:23:43 +00:00
await Promise.all([
2025-10-02 18:26:41 +00:00
checkLocalDBChats(),
setBanners(),
setTools(),
setUserSettings(async () => {
await Promise.all([setModels(), setToolServers()]);
2025-10-02 15:23:43 +00:00
})
]);
2025-10-27 02:33:39 +00:00
// Helper function to check if the pressed keys match the shortcut definition
2025-11-06 18:08:48 +00:00
const isShortcutMatch = (event: KeyboardEvent, shortcut): boolean => {
const keys = shortcut?.keys || [];
2023-12-28 10:46:57 +00:00
2025-11-06 18:08:48 +00:00
const normalized = keys.map((k) => k.toLowerCase());
const needCtrl = normalized.includes('ctrl') || normalized.includes('mod');
const needShift = normalized.includes('shift');
const needAlt = normalized.includes('alt');
2025-07-18 08:47:22 +00:00
2025-11-06 18:31:55 +00:00
const mainKeys = normalized.filter((k) => !['ctrl', 'shift', 'alt', 'mod'].includes(k));
2025-11-06 18:08:48 +00:00
// Get the main key pressed
2025-11-06 18:31:55 +00:00
const keyPressed = event.key.toLowerCase();
2024-12-01 02:07:49 +00:00
2025-11-06 18:08:48 +00:00
// Check modifiers
if (needShift & & !event.shiftKey) return false;
if (needCtrl & & !(event.ctrlKey || event.metaKey)) return false;
if (!needCtrl & & (event.ctrlKey || event.metaKey)) return false;
if (needAlt & & !event.altKey) return false;
if (!needAlt & & event.altKey) return false;
if (mainKeys.length & & !mainKeys.includes(keyPressed)) return false;
return true;
2025-10-27 02:33:39 +00:00
};
const setupKeyboardShortcuts = () => {
document.addEventListener('keydown', async (event) => {
2025-11-06 18:08:48 +00:00
if (isShortcutMatch(event, shortcuts[Shortcut.SEARCH])) {
2025-11-06 18:24:34 +00:00
console.log('Shortcut triggered: SEARCH');
2025-11-06 18:08:48 +00:00
event.preventDefault();
showSearch.set(!$showSearch);
} else if (isShortcutMatch(event, shortcuts[Shortcut.NEW_CHAT])) {
2025-11-06 18:24:34 +00:00
console.log('Shortcut triggered: NEW_CHAT');
2025-11-06 18:08:48 +00:00
event.preventDefault();
document.getElementById('sidebar-new-chat-button')?.click();
} else if (isShortcutMatch(event, shortcuts[Shortcut.FOCUS_INPUT])) {
2025-11-06 18:24:34 +00:00
console.log('Shortcut triggered: FOCUS_INPUT');
2025-11-06 18:08:48 +00:00
event.preventDefault();
document.getElementById('chat-input')?.focus();
} else if (isShortcutMatch(event, shortcuts[Shortcut.COPY_LAST_CODE_BLOCK])) {
2025-11-06 18:24:34 +00:00
console.log('Shortcut triggered: COPY_LAST_CODE_BLOCK');
2025-11-06 18:08:48 +00:00
event.preventDefault();
[...document.getElementsByClassName('copy-code-button')]?.at(-1)?.click();
} else if (isShortcutMatch(event, shortcuts[Shortcut.COPY_LAST_RESPONSE])) {
2025-11-06 18:24:34 +00:00
console.log('Shortcut triggered: COPY_LAST_RESPONSE');
2025-11-06 18:08:48 +00:00
event.preventDefault();
[...document.getElementsByClassName('copy-response-button')]?.at(-1)?.click();
} else if (isShortcutMatch(event, shortcuts[Shortcut.TOGGLE_SIDEBAR])) {
2025-11-06 18:24:34 +00:00
console.log('Shortcut triggered: TOGGLE_SIDEBAR');
2025-11-06 18:08:48 +00:00
event.preventDefault();
showSidebar.set(!$showSidebar);
} else if (isShortcutMatch(event, shortcuts[Shortcut.DELETE_CHAT])) {
2025-11-06 18:24:34 +00:00
console.log('Shortcut triggered: DELETE_CHAT');
2025-11-06 18:08:48 +00:00
event.preventDefault();
document.getElementById('delete-chat-button')?.click();
} else if (isShortcutMatch(event, shortcuts[Shortcut.OPEN_SETTINGS])) {
2025-11-06 18:24:34 +00:00
console.log('Shortcut triggered: OPEN_SETTINGS');
2025-11-06 18:08:48 +00:00
event.preventDefault();
showSettings.set(!$showSettings);
} else if (isShortcutMatch(event, shortcuts[Shortcut.SHOW_SHORTCUTS])) {
2025-11-06 18:24:34 +00:00
console.log('Shortcut triggered: SHOW_SHORTCUTS');
2025-11-06 18:08:48 +00:00
event.preventDefault();
showShortcuts.set(!$showShortcuts);
} else if (isShortcutMatch(event, shortcuts[Shortcut.CLOSE_MODAL])) {
2025-11-06 18:24:34 +00:00
console.log('Shortcut triggered: CLOSE_MODAL');
2025-11-06 18:08:48 +00:00
event.preventDefault();
showSettings.set(false);
showShortcuts.set(false);
} else if (isShortcutMatch(event, shortcuts[Shortcut.NEW_TEMPORARY_CHAT])) {
2025-11-06 18:24:34 +00:00
console.log('Shortcut triggered: NEW_TEMPORARY_CHAT');
2025-11-06 18:08:48 +00:00
event.preventDefault();
if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
temporaryChatEnabled.set(true);
} else {
temporaryChatEnabled.set(!$temporaryChatEnabled);
2025-10-27 02:33:39 +00:00
}
2025-11-06 18:08:48 +00:00
await goto('/');
setTimeout(() => {
document.getElementById('new-chat-button')?.click();
}, 0);
} else if (isShortcutMatch(event, shortcuts[Shortcut.GENERATE_MESSAGE_PAIR])) {
2025-11-06 18:24:34 +00:00
console.log('Shortcut triggered: GENERATE_MESSAGE_PAIR');
2025-11-06 18:08:48 +00:00
event.preventDefault();
document.getElementById('generate-message-pair-button')?.click();
} else if (isShortcutMatch(event, shortcuts[Shortcut.REGENERATE_RESPONSE])) {
2025-11-06 18:24:34 +00:00
console.log('Shortcut triggered: REGENERATE_RESPONSE');
2025-11-06 18:08:48 +00:00
event.preventDefault();
[...document.getElementsByClassName('regenerate-response-button')]?.at(-1)?.click();
2024-12-01 02:07:49 +00:00
}
2025-10-27 02:33:39 +00:00
});
};
2025-10-02 15:23:43 +00:00
setupKeyboardShortcuts();
2023-12-28 10:46:57 +00:00
2025-10-02 15:23:43 +00:00
if ($user?.role === 'admin' && ($settings?.showChangelog ?? true)) {
showChangelog.set($settings?.version !== $config.version);
}
2024-02-23 08:47:54 +00:00
2025-10-02 15:23:43 +00:00
if ($user?.role === 'admin' || ($user?.permissions?.chat?.temporary ?? true)) {
if ($page.url.searchParams.get('temporary-chat') === 'true') {
temporaryChatEnabled.set(true);
}
2025-04-01 00:58:43 +00:00
2025-10-02 15:23:43 +00:00
if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
temporaryChatEnabled.set(true);
2025-04-01 00:58:43 +00:00
}
2025-10-02 15:23:43 +00:00
}
2025-04-01 00:58:43 +00:00
2025-10-02 15:23:43 +00:00
// Check for version updates
if ($user?.role === 'admin' && $config?.features?.enable_version_update_check) {
// Check if the user has dismissed the update toast in the last 24 hours
if (localStorage.dismissedUpdateToast) {
const dismissedUpdateToast = new Date(Number(localStorage.dismissedUpdateToast));
const now = new Date();
2024-09-24 10:40:13 +00:00
2025-10-02 15:23:43 +00:00
if (now - dismissedUpdateToast > 24 * 60 * 60 * 1000) {
2024-09-27 12:41:29 +00:00
checkForVersionUpdates();
2024-09-25 18:47:04 +00:00
}
2025-10-02 15:23:43 +00:00
} else {
checkForVersionUpdates();
2024-09-25 18:47:04 +00:00
}
2023-12-26 21:10:50 +00:00
}
2025-10-02 15:23:43 +00:00
await tick();
2023-12-15 03:43:52 +00:00
2023-11-19 05:41:43 +00:00
loaded = true;
});
2024-09-24 10:40:13 +00:00
const checkForVersionUpdates = async () => {
2024-09-26 02:05:28 +00:00
version = await getVersionUpdates(localStorage.token).catch((error) => {
2024-09-24 10:40:13 +00:00
return {
current: WEBUI_VERSION,
latest: WEBUI_VERSION
};
});
};
2023-11-19 00:47:12 +00:00
< / script >
2024-05-02 06:11:16 +00:00
< SettingsModal bind:show = { $showSettings } / >
< ChangelogModal bind:show = { $showChangelog } / >
2024-04-27 23:47:11 +00:00
2024-10-21 07:30:29 +00:00
{ #if version && compareVersion ( version . latest , version . current ) && ( $settings ? . showUpdateToast ?? true )}
2024-09-26 02:05:28 +00:00
< div class = " absolute bottom-8 right-8 z-50" in:fade = {{ duration : 100 }} >
2024-10-07 06:41:50 +00:00
< UpdateInfoToast
{ version }
on:close={() => {
localStorage.setItem('dismissedUpdateToast', Date.now().toString());
version = null;
}}
/>
2024-09-26 02:05:28 +00:00
< / div >
{ /if }
2025-04-23 18:12:22 +00:00
{ #if $user }
< div class = "app relative" >
< div
class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-900 h-screen max-h-[100dvh] overflow-auto flex flex-row justify-end"
>
{ #if ! [ 'user' , 'admin' ]. includes ( $user ? . role )}
< AccountPending / >
2025-07-02 12:11:13 +00:00
{ : else }
{ #if localDBChats . length > 0 }
< div class = "fixed w-full h-full flex z-50" >
< div
class="absolute w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 flex justify-center"
>
< div class = "m-auto pb-44 flex flex-col justify-center" >
< div class = "max-w-md" >
< div class = "text-center dark:text-white text-2xl font-medium z-50" >
2025-08-20 18:49:05 +00:00
{ $i18n . t ( 'Important Update' )} < br />
{ $i18n . t ( 'Action Required for Chat Log Storage' )}
2025-07-02 12:11:13 +00:00
< / div >
< div class = " mt-4 text-center text-sm dark:text-gray-200 w-full" >
{ $i18n . t (
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through"
)}
2025-11-24 02:17:14 +00:00
< span class = "font-medium dark:text-white"
2025-07-02 12:11:13 +00:00
>{ $i18n . t ( 'Settings' )} > { $i18n . t ( 'Chats' )} > { $i18n . t ( 'Import Chats' )} < /span
>. { $i18n . t (
'This ensures that your valuable conversations are securely saved to your backend database. Thank you!'
)}
< / div >
< div class = " mt-6 mx-auto relative group w-fit" >
< button
class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 transition font-medium text-sm"
on:click={ async () => {
let blob = new Blob([JSON.stringify(localDBChats)], {
type: 'application/json'
});
saveAs(blob, `chat-export-${ Date . now ()} .json`);
const tx = DB.transaction('chats', 'readwrite');
await Promise.all([tx.store.clear(), tx.done]);
await deleteDB('Chats');
localDBChats = [];
}}
>
2025-08-19 18:39:17 +00:00
{ $i18n . t ( 'Download & Delete' )}
2025-07-02 12:11:13 +00:00
< / button >
< button
class="text-xs text-center w-full mt-2 text-gray-400 underline"
on:click={ async () => {
localDBChats = [];
}}>{ $i18n . t ( 'Close' )} < /button
>
< / div >
2025-04-23 18:12:22 +00:00
< / div >
2023-12-19 19:57:49 +00:00
< / div >
2023-12-15 03:43:52 +00:00
< / div >
< / div >
2025-07-02 12:11:13 +00:00
{ /if }
2025-04-23 18:12:22 +00:00
2025-07-02 12:11:13 +00:00
< Sidebar / >
2025-04-23 18:12:22 +00:00
2025-07-02 12:11:13 +00:00
{ #if loaded }
< slot / >
{ : else }
2025-08-08 09:09:40 +00:00
< div
class="w-full flex-1 h-full flex items-center justify-center { $showSidebar
2025-12-11 04:54:36 +00:00
? ' md:max-w-[calc(100%-var(--sidebar-width))]'
2025-08-08 09:09:40 +00:00
: ' '}"
>
2025-07-02 12:11:13 +00:00
< Spinner className = "size-5" / >
< / div >
{ /if }
2025-04-23 18:12:22 +00:00
{ /if }
< / div >
2023-11-20 01:47:07 +00:00
< / div >
2025-04-23 18:12:22 +00:00
{ /if }
2023-11-20 01:47:07 +00:00
< style >
.loading {
display: inline-block;
clip-path: inset(0 1ch 0 0);
animation: l 1s steps(3) infinite;
letter-spacing: -0.5px;
}
@keyframes l {
to {
clip-path: inset(0 -1ch 0 0);
}
}
pre[class*='language-'] {
position: relative;
overflow: auto;
/* make space */
margin: 5px 0;
padding: 1.75rem 0 1.75rem 1rem;
border-radius: 10px;
}
pre[class*='language-'] button {
position: absolute;
top: 5px;
right: 5px;
font-size: 0.9rem;
padding: 0.15rem;
background-color: #828282;
border: ridge 1px #7b7b7c;
border-radius: 5px;
text-shadow: #c4c4c4 0 0 2px;
}
pre[class*='language-'] button:hover {
cursor: pointer;
background-color: #bcbabb;
}
2025-10-27 02:33:39 +00:00
< / style >