123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- <script>
- import { io } from 'socket.io-client';
- import { spring } from 'svelte/motion';
- let loadingProgress = spring(0, {
- stiffness: 0.05
- });
- import { onMount, tick, setContext } from 'svelte';
- import {
- config,
- user,
- settings,
- theme,
- WEBUI_NAME,
- mobile,
- socket,
- activeUserIds,
- USAGE_POOL,
- chatId,
- chats,
- currentChatPage,
- tags,
- temporaryChatEnabled,
- isLastActiveTab,
- isApp,
- appVersion
- } from '$lib/stores';
- import { goto } from '$app/navigation';
- import { page } from '$app/stores';
- import { Toaster, toast } from 'svelte-sonner';
- import { getBackendConfig } from '$lib/apis';
- import { getSessionUser } from '$lib/apis/auths';
- import '../tailwind.css';
- import '../app.css';
- import 'tippy.js/dist/tippy.css';
- import { WEBUI_BASE_URL, WEBUI_HOSTNAME } from '$lib/constants';
- import i18n, { initI18n, getLanguages } from '$lib/i18n';
- import { bestMatchingLanguage } from '$lib/utils';
- import { getAllTags, getChatList } from '$lib/apis/chats';
- import NotificationToast from '$lib/components/NotificationToast.svelte';
- import AppControls from '$lib/components/app/AppControls.svelte';
- setContext('i18n', i18n);
- const bc = new BroadcastChannel('active-tab-channel');
- let loaded = false;
- const BREAKPOINT = 768;
- const setupSocket = async (enableWebsocket) => {
- const _socket = io(`${WEBUI_BASE_URL}` || undefined, {
- reconnection: true,
- reconnectionDelay: 1000,
- reconnectionDelayMax: 5000,
- randomizationFactor: 0.5,
- path: '/ws/socket.io',
- transports: enableWebsocket ? ['websocket'] : ['polling', 'websocket'],
- auth: { token: localStorage.token }
- });
- await socket.set(_socket);
- _socket.on('connect_error', (err) => {
- console.log('connect_error', err);
- });
- _socket.on('connect', () => {
- console.log('connected', _socket.id);
- });
- _socket.on('reconnect_attempt', (attempt) => {
- console.log('reconnect_attempt', attempt);
- });
- _socket.on('reconnect_failed', () => {
- console.log('reconnect_failed');
- });
- _socket.on('disconnect', (reason, details) => {
- console.log(`Socket ${_socket.id} disconnected due to ${reason}`);
- if (details) {
- console.log('Additional details:', details);
- }
- });
- _socket.on('user-list', (data) => {
- console.log('user-list', data);
- activeUserIds.set(data.user_ids);
- });
- _socket.on('usage', (data) => {
- console.log('usage', data);
- USAGE_POOL.set(data['models']);
- });
- };
- const chatEventHandler = async (event) => {
- const chat = $page.url.pathname.includes(`/c/${event.chat_id}`);
- let isFocused = document.visibilityState !== 'visible';
- if (window.electronAPI) {
- const res = await window.electronAPI.send({
- type: 'window:isFocused'
- });
- if (res) {
- isFocused = res.isFocused;
- }
- }
- if ((event.chat_id !== $chatId && !$temporaryChatEnabled) || isFocused) {
- await tick();
- const type = event?.data?.type ?? null;
- const data = event?.data?.data ?? null;
- if (type === 'chat:completion') {
- const { done, content, title } = data;
- if (done) {
- if ($isLastActiveTab) {
- if ($settings?.notificationEnabled ?? false) {
- new Notification(`${title} | Open WebUI`, {
- body: content,
- icon: `${WEBUI_BASE_URL}/static/favicon.png`
- });
- }
- }
- toast.custom(NotificationToast, {
- componentProps: {
- onClick: () => {
- goto(`/c/${event.chat_id}`);
- },
- content: content,
- title: title
- },
- duration: 15000,
- unstyled: true
- });
- }
- } else if (type === 'chat:title') {
- currentChatPage.set(1);
- await chats.set(await getChatList(localStorage.token, $currentChatPage));
- } else if (type === 'chat:tags') {
- tags.set(await getAllTags(localStorage.token));
- }
- }
- };
- const channelEventHandler = async (event) => {
- if (event.data?.type === 'typing') {
- return;
- }
- // check url path
- const channel = $page.url.pathname.includes(`/channels/${event.channel_id}`);
- let isFocused = document.visibilityState !== 'visible';
- if (window.electronAPI) {
- const res = await window.electronAPI.send({
- type: 'window:isFocused'
- });
- if (res) {
- isFocused = res.isFocused;
- }
- }
- if ((!channel || isFocused) && event?.user?.id !== $user?.id) {
- await tick();
- const type = event?.data?.type ?? null;
- const data = event?.data?.data ?? null;
- if (type === 'message') {
- if ($isLastActiveTab) {
- if ($settings?.notificationEnabled ?? false) {
- new Notification(`${data?.user?.name} (#${event?.channel?.name}) | Open WebUI`, {
- body: data?.content,
- icon: data?.user?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`
- });
- }
- }
- toast.custom(NotificationToast, {
- componentProps: {
- onClick: () => {
- goto(`/channels/${event.channel_id}`);
- },
- content: data?.content,
- title: event?.channel?.name
- },
- duration: 15000,
- unstyled: true
- });
- }
- }
- };
- onMount(async () => {
- if (window?.electronAPI) {
- const res = await window.electronAPI.send({
- type: 'version'
- });
- if (res) {
- isApp.set(true);
- appVersion.set(res.version);
- }
- }
- // Listen for messages on the BroadcastChannel
- bc.onmessage = (event) => {
- if (event.data === 'active') {
- isLastActiveTab.set(false); // Another tab became active
- }
- };
- // Set yourself as the last active tab when this tab is focused
- const handleVisibilityChange = () => {
- if (document.visibilityState === 'visible') {
- isLastActiveTab.set(true); // This tab is now the active tab
- bc.postMessage('active'); // Notify other tabs that this tab is active
- }
- };
- // Add event listener for visibility state changes
- document.addEventListener('visibilitychange', handleVisibilityChange);
- // Call visibility change handler initially to set state on load
- handleVisibilityChange();
- theme.set(localStorage.theme);
- mobile.set(window.innerWidth < BREAKPOINT);
- const onResize = () => {
- if (window.innerWidth < BREAKPOINT) {
- mobile.set(true);
- } else {
- mobile.set(false);
- }
- };
- window.addEventListener('resize', onResize);
- let backendConfig = null;
- try {
- backendConfig = await getBackendConfig();
- console.log('Backend config:', backendConfig);
- } catch (error) {
- console.error('Error loading backend config:', error);
- }
- // Initialize i18n even if we didn't get a backend config,
- // so `/error` can show something that's not `undefined`.
- initI18n();
- if (!localStorage.locale) {
- const languages = await getLanguages();
- const browserLanguages = navigator.languages
- ? navigator.languages
- : [navigator.language || navigator.userLanguage];
- const lang = backendConfig.default_locale
- ? backendConfig.default_locale
- : bestMatchingLanguage(languages, browserLanguages, 'en-US');
- $i18n.changeLanguage(lang);
- }
- if (backendConfig) {
- // Save Backend Status to Store
- await config.set(backendConfig);
- await WEBUI_NAME.set(backendConfig.name);
- if ($config) {
- await setupSocket($config.features?.enable_websocket ?? true);
- if (localStorage.token) {
- // Get Session User Info
- const sessionUser = await getSessionUser(localStorage.token).catch((error) => {
- toast.error(error);
- return null;
- });
- if (sessionUser) {
- // Save Session User to Store
- $socket.emit('user-join', { auth: { token: sessionUser.token } });
- $socket?.on('chat-events', chatEventHandler);
- $socket?.on('channel-events', channelEventHandler);
- await user.set(sessionUser);
- await config.set(await getBackendConfig());
- } else {
- // Redirect Invalid Session User to /auth Page
- localStorage.removeItem('token');
- await goto('/auth');
- }
- } else {
- // Don't redirect if we're already on the auth page
- // Needed because we pass in tokens from OAuth logins via URL fragments
- if ($page.url.pathname !== '/auth') {
- await goto('/auth');
- }
- }
- }
- } else {
- // Redirect to /error when Backend Not Detected
- await goto(`/error`);
- }
- await tick();
- if (
- document.documentElement.classList.contains('her') &&
- document.getElementById('progress-bar')
- ) {
- loadingProgress.subscribe((value) => {
- const progressBar = document.getElementById('progress-bar');
- if (progressBar) {
- progressBar.style.width = `${value}%`;
- }
- });
- await loadingProgress.set(100);
- document.getElementById('splash-screen')?.remove();
- const audio = new Audio(`/audio/greeting.mp3`);
- const playAudio = () => {
- audio.play();
- document.removeEventListener('click', playAudio);
- };
- document.addEventListener('click', playAudio);
- loaded = true;
- } else {
- document.getElementById('splash-screen')?.remove();
- loaded = true;
- }
- return () => {
- window.removeEventListener('resize', onResize);
- };
- });
- </script>
- <svelte:head>
- <title>{$WEBUI_NAME}</title>
- <link crossorigin="anonymous" rel="icon" href="{WEBUI_BASE_URL}/static/favicon.png" />
- <!-- rosepine themes have been disabled as it's not up to date with our latest version. -->
- <!-- feel free to make a PR to fix if anyone wants to see it return -->
- <!-- <link rel="stylesheet" type="text/css" href="/themes/rosepine.css" />
- <link rel="stylesheet" type="text/css" href="/themes/rosepine-dawn.css" /> -->
- </svelte:head>
- {#if loaded}
- {#if $isApp}
- <div class="flex flex-row h-screen">
- <AppControls />
- <div class="w-full flex-1 max-w-[calc(100%-4.5rem)]">
- <slot />
- </div>
- </div>
- {:else}
- <slot />
- {/if}
- {/if}
- <Toaster
- theme={$theme.includes('dark')
- ? 'dark'
- : $theme === 'system'
- ? window.matchMedia('(prefers-color-scheme: dark)').matches
- ? 'dark'
- : 'light'
- : 'light'}
- richColors
- position="top-right"
- />
|