mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-11 20:05:19 +00:00
439 lines
13 KiB
Svelte
439 lines
13 KiB
Svelte
<script lang="ts">
|
|
import { toast } from 'svelte-sonner';
|
|
import { onMount, tick, getContext } from 'svelte';
|
|
import { openDB, deleteDB } from 'idb';
|
|
import fileSaver from 'file-saver';
|
|
const { saveAs } = fileSaver;
|
|
|
|
import { goto } from '$app/navigation';
|
|
import { page } from '$app/stores';
|
|
import { fade } from 'svelte/transition';
|
|
|
|
import { getModels, getToolServersData, getVersionUpdates } from '$lib/apis';
|
|
import { getTools } from '$lib/apis/tools';
|
|
import { getBanners } from '$lib/apis/configs';
|
|
import { getUserSettings } from '$lib/apis/users';
|
|
|
|
import { WEBUI_VERSION } from '$lib/constants';
|
|
import { compareVersion } from '$lib/utils';
|
|
|
|
import {
|
|
config,
|
|
user,
|
|
settings,
|
|
models,
|
|
prompts,
|
|
knowledge,
|
|
tools,
|
|
functions,
|
|
tags,
|
|
banners,
|
|
showSettings,
|
|
showShortcuts,
|
|
showChangelog,
|
|
temporaryChatEnabled,
|
|
toolServers,
|
|
showSearch,
|
|
showSidebar
|
|
} from '$lib/stores';
|
|
|
|
import Sidebar from '$lib/components/layout/Sidebar.svelte';
|
|
import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
|
|
import ChangelogModal from '$lib/components/ChangelogModal.svelte';
|
|
import AccountPending from '$lib/components/layout/Overlay/AccountPending.svelte';
|
|
import UpdateInfoToast from '$lib/components/layout/UpdateInfoToast.svelte';
|
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
|
import { Shortcut, shortcuts } from '$lib/shortcuts';
|
|
|
|
const i18n = getContext('i18n');
|
|
|
|
let loaded = false;
|
|
let DB = null;
|
|
let localDBChats = [];
|
|
|
|
let version;
|
|
|
|
const clearChatInputStorage = () => {
|
|
const chatInputKeys = Object.keys(localStorage).filter((key) => key.startsWith('chat-input'));
|
|
if (chatInputKeys.length > 0) {
|
|
chatInputKeys.forEach((key) => {
|
|
localStorage.removeItem(key);
|
|
});
|
|
}
|
|
};
|
|
|
|
const checkLocalDBChats = async () => {
|
|
try {
|
|
// Check if IndexedDB exists
|
|
DB = await openDB('Chats', 1);
|
|
|
|
if (!DB) {
|
|
return;
|
|
}
|
|
|
|
const chats = await DB.getAllFromIndex('chats', 'timestamp');
|
|
localDBChats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
|
|
|
|
if (localDBChats.length === 0) {
|
|
await deleteDB('Chats');
|
|
}
|
|
} catch (error) {
|
|
// IndexedDB Not Found
|
|
}
|
|
};
|
|
|
|
const setUserSettings = async (cb: () => Promise<void>) => {
|
|
let userSettings = await getUserSettings(localStorage.token).catch((error) => {
|
|
console.error(error);
|
|
return null;
|
|
});
|
|
|
|
if (!userSettings) {
|
|
try {
|
|
userSettings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
|
} catch (e: unknown) {
|
|
console.error('Failed to parse settings from localStorage', e);
|
|
userSettings = {};
|
|
}
|
|
}
|
|
|
|
if (userSettings?.ui) {
|
|
settings.set(userSettings.ui);
|
|
}
|
|
|
|
if (cb) {
|
|
await cb();
|
|
}
|
|
};
|
|
|
|
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;
|
|
}
|
|
return true;
|
|
});
|
|
toolServers.set(toolServersData);
|
|
};
|
|
|
|
const setBanners = async () => {
|
|
const bannersData = await getBanners(localStorage.token);
|
|
banners.set(bannersData);
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
clearChatInputStorage();
|
|
await Promise.all([
|
|
checkLocalDBChats(),
|
|
setBanners(),
|
|
setTools(),
|
|
setUserSettings(async () => {
|
|
await Promise.all([setModels(), setToolServers()]);
|
|
})
|
|
]);
|
|
|
|
// Helper function to check if the pressed keys match the shortcut definition
|
|
const isShortcutMatch = (event: KeyboardEvent, shortcut): boolean => {
|
|
const keys = shortcut?.keys || [];
|
|
|
|
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');
|
|
|
|
const mainKeys = normalized.filter((k) => !['ctrl', 'shift', 'alt', 'mod'].includes(k));
|
|
|
|
// Get the main key pressed
|
|
const keyPressed = event.key.toLowerCase();
|
|
|
|
// 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;
|
|
};
|
|
|
|
const setupKeyboardShortcuts = () => {
|
|
document.addEventListener('keydown', async (event) => {
|
|
if (isShortcutMatch(event, shortcuts[Shortcut.SEARCH])) {
|
|
console.log('Shortcut triggered: SEARCH');
|
|
event.preventDefault();
|
|
showSearch.set(!$showSearch);
|
|
} else if (isShortcutMatch(event, shortcuts[Shortcut.NEW_CHAT])) {
|
|
console.log('Shortcut triggered: NEW_CHAT');
|
|
event.preventDefault();
|
|
document.getElementById('sidebar-new-chat-button')?.click();
|
|
} else if (isShortcutMatch(event, shortcuts[Shortcut.FOCUS_INPUT])) {
|
|
console.log('Shortcut triggered: FOCUS_INPUT');
|
|
event.preventDefault();
|
|
document.getElementById('chat-input')?.focus();
|
|
} else if (isShortcutMatch(event, shortcuts[Shortcut.COPY_LAST_CODE_BLOCK])) {
|
|
console.log('Shortcut triggered: COPY_LAST_CODE_BLOCK');
|
|
event.preventDefault();
|
|
[...document.getElementsByClassName('copy-code-button')]?.at(-1)?.click();
|
|
} else if (isShortcutMatch(event, shortcuts[Shortcut.COPY_LAST_RESPONSE])) {
|
|
console.log('Shortcut triggered: COPY_LAST_RESPONSE');
|
|
event.preventDefault();
|
|
[...document.getElementsByClassName('copy-response-button')]?.at(-1)?.click();
|
|
} else if (isShortcutMatch(event, shortcuts[Shortcut.TOGGLE_SIDEBAR])) {
|
|
console.log('Shortcut triggered: TOGGLE_SIDEBAR');
|
|
event.preventDefault();
|
|
showSidebar.set(!$showSidebar);
|
|
} else if (isShortcutMatch(event, shortcuts[Shortcut.DELETE_CHAT])) {
|
|
console.log('Shortcut triggered: DELETE_CHAT');
|
|
event.preventDefault();
|
|
document.getElementById('delete-chat-button')?.click();
|
|
} else if (isShortcutMatch(event, shortcuts[Shortcut.OPEN_SETTINGS])) {
|
|
console.log('Shortcut triggered: OPEN_SETTINGS');
|
|
event.preventDefault();
|
|
showSettings.set(!$showSettings);
|
|
} else if (isShortcutMatch(event, shortcuts[Shortcut.SHOW_SHORTCUTS])) {
|
|
console.log('Shortcut triggered: SHOW_SHORTCUTS');
|
|
event.preventDefault();
|
|
showShortcuts.set(!$showShortcuts);
|
|
} else if (isShortcutMatch(event, shortcuts[Shortcut.CLOSE_MODAL])) {
|
|
console.log('Shortcut triggered: CLOSE_MODAL');
|
|
event.preventDefault();
|
|
showSettings.set(false);
|
|
showShortcuts.set(false);
|
|
} else if (isShortcutMatch(event, shortcuts[Shortcut.NEW_TEMPORARY_CHAT])) {
|
|
console.log('Shortcut triggered: NEW_TEMPORARY_CHAT');
|
|
event.preventDefault();
|
|
if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
|
|
temporaryChatEnabled.set(true);
|
|
} else {
|
|
temporaryChatEnabled.set(!$temporaryChatEnabled);
|
|
}
|
|
await goto('/');
|
|
setTimeout(() => {
|
|
document.getElementById('new-chat-button')?.click();
|
|
}, 0);
|
|
} else if (isShortcutMatch(event, shortcuts[Shortcut.GENERATE_MESSAGE_PAIR])) {
|
|
console.log('Shortcut triggered: GENERATE_MESSAGE_PAIR');
|
|
event.preventDefault();
|
|
document.getElementById('generate-message-pair-button')?.click();
|
|
} else if (isShortcutMatch(event, shortcuts[Shortcut.REGENERATE_RESPONSE])) {
|
|
console.log('Shortcut triggered: REGENERATE_RESPONSE');
|
|
event.preventDefault();
|
|
[...document.getElementsByClassName('regenerate-response-button')]?.at(-1)?.click();
|
|
}
|
|
});
|
|
};
|
|
setupKeyboardShortcuts();
|
|
|
|
if ($user?.role === 'admin' && ($settings?.showChangelog ?? true)) {
|
|
showChangelog.set($settings?.version !== $config.version);
|
|
}
|
|
|
|
if ($user?.role === 'admin' || ($user?.permissions?.chat?.temporary ?? true)) {
|
|
if ($page.url.searchParams.get('temporary-chat') === 'true') {
|
|
temporaryChatEnabled.set(true);
|
|
}
|
|
|
|
if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
|
|
temporaryChatEnabled.set(true);
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
|
|
if (now - dismissedUpdateToast > 24 * 60 * 60 * 1000) {
|
|
checkForVersionUpdates();
|
|
}
|
|
} else {
|
|
checkForVersionUpdates();
|
|
}
|
|
}
|
|
await tick();
|
|
|
|
loaded = true;
|
|
});
|
|
|
|
const checkForVersionUpdates = async () => {
|
|
version = await getVersionUpdates(localStorage.token).catch((error) => {
|
|
return {
|
|
current: WEBUI_VERSION,
|
|
latest: WEBUI_VERSION
|
|
};
|
|
});
|
|
};
|
|
</script>
|
|
|
|
<SettingsModal bind:show={$showSettings} />
|
|
<ChangelogModal bind:show={$showChangelog} />
|
|
|
|
{#if version && compareVersion(version.latest, version.current) && ($settings?.showUpdateToast ?? true)}
|
|
<div class=" absolute bottom-8 right-8 z-50" in:fade={{ duration: 100 }}>
|
|
<UpdateInfoToast
|
|
{version}
|
|
on:close={() => {
|
|
localStorage.setItem('dismissedUpdateToast', Date.now().toString());
|
|
version = null;
|
|
}}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
|
|
{#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 />
|
|
{: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">
|
|
{$i18n.t('Important Update')}<br />
|
|
{$i18n.t('Action Required for Chat Log Storage')}
|
|
</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"
|
|
)}
|
|
<span class="font-medium dark:text-white"
|
|
>{$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 = [];
|
|
}}
|
|
>
|
|
{$i18n.t('Download & Delete')}
|
|
</button>
|
|
|
|
<button
|
|
class="text-xs text-center w-full mt-2 text-gray-400 underline"
|
|
on:click={async () => {
|
|
localDBChats = [];
|
|
}}>{$i18n.t('Close')}</button
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<Sidebar />
|
|
|
|
{#if loaded}
|
|
<slot />
|
|
{:else}
|
|
<div
|
|
class="w-full flex-1 h-full flex items-center justify-center {$showSidebar
|
|
? ' md:max-w-[calc(100%-var(--sidebar-width))]'
|
|
: ' '}"
|
|
>
|
|
<Spinner className="size-5" />
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<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;
|
|
}
|
|
</style>
|