123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- <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,
- theme,
- WEBUI_NAME,
- mobile,
- socket,
- activeUserCount,
- USAGE_POOL,
- chatId,
- chats,
- currentChatPage,
- tags
- } 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';
- setContext('i18n', i18n);
- 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-count', (data) => {
- console.log('user-count', data);
- activeUserCount.set(data.count);
- });
- _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}`);
- if (!chat || document.visibilityState !== 'visible') {
- 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) {
- 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) => {
- // check url path
- const channel = $page.url.pathname.includes(`/channels/${event.channel_id}`);
- if ((!channel || document.visibilityState !== 'visible') && event?.user?.id !== $user?.id) {
- await tick();
- const type = event?.data?.type ?? null;
- const data = event?.data?.data ?? null;
- if (type === 'message') {
- toast.custom(NotificationToast, {
- componentProps: {
- onClick: () => {
- goto(`/channels/${event.channel_id}`);
- },
- content: data?.content,
- title: event?.channel?.name
- },
- duration: 15000,
- unstyled: true
- });
- }
- }
- };
- onMount(async () => {
- 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}
- <slot />
- {/if}
- <Toaster
- theme={$theme.includes('dark')
- ? 'dark'
- : $theme === 'system'
- ? window.matchMedia('(prefers-color-scheme: dark)').matches
- ? 'dark'
- : 'light'
- : 'light'}
- richColors
- position="top-right"
- />
|