123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- <script>
- import { io } from 'socket.io-client';
- import { spring } from 'svelte/motion';
- import PyodideWorker from '$lib/workers/pyodide.worker?worker';
- 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,
- appInfo
- } 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 AppSidebar from '$lib/components/app/AppSidebar.svelte';
- import { chatCompletion } from '$lib/apis/openai';
- 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 executePythonAsWorker = async (id, code, cb) => {
- let result = null;
- let stdout = null;
- let stderr = null;
- let executing = true;
- let packages = [
- code.includes('requests') ? 'requests' : null,
- code.includes('bs4') ? 'beautifulsoup4' : null,
- code.includes('numpy') ? 'numpy' : null,
- code.includes('pandas') ? 'pandas' : null,
- code.includes('matplotlib') ? 'matplotlib' : null,
- code.includes('sklearn') ? 'scikit-learn' : null,
- code.includes('scipy') ? 'scipy' : null,
- code.includes('re') ? 'regex' : null,
- code.includes('seaborn') ? 'seaborn' : null,
- code.includes('sympy') ? 'sympy' : null,
- code.includes('tiktoken') ? 'tiktoken' : null,
- code.includes('pytz') ? 'pytz' : null
- ].filter(Boolean);
- const pyodideWorker = new PyodideWorker();
- pyodideWorker.postMessage({
- id: id,
- code: code,
- packages: packages
- });
- setTimeout(() => {
- if (executing) {
- executing = false;
- stderr = 'Execution Time Limit Exceeded';
- pyodideWorker.terminate();
- if (cb) {
- cb(
- JSON.parse(
- JSON.stringify(
- {
- stdout: stdout,
- stderr: stderr,
- result: result
- },
- (_key, value) => (typeof value === 'bigint' ? value.toString() : value)
- )
- )
- );
- }
- }
- }, 60000);
- pyodideWorker.onmessage = (event) => {
- console.log('pyodideWorker.onmessage', event);
- const { id, ...data } = event.data;
- console.log(id, data);
- data['stdout'] && (stdout = data['stdout']);
- data['stderr'] && (stderr = data['stderr']);
- data['result'] && (result = data['result']);
- if (cb) {
- cb(
- JSON.parse(
- JSON.stringify(
- {
- stdout: stdout,
- stderr: stderr,
- result: result
- },
- (_key, value) => (typeof value === 'bigint' ? value.toString() : value)
- )
- )
- );
- }
- executing = false;
- };
- pyodideWorker.onerror = (event) => {
- console.log('pyodideWorker.onerror', event);
- if (cb) {
- cb(
- JSON.parse(
- JSON.stringify(
- {
- stdout: stdout,
- stderr: stderr,
- result: result
- },
- (_key, value) => (typeof value === 'bigint' ? value.toString() : value)
- )
- )
- );
- }
- executing = false;
- };
- };
- const chatEventHandler = async (event, cb) => {
- 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;
- }
- }
- await tick();
- const type = event?.data?.type ?? null;
- const data = event?.data?.data ?? null;
- if ((event.chat_id !== $chatId && !$temporaryChatEnabled) || isFocused) {
- 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));
- }
- } else if (data?.session_id === $socket.id) {
- if (type === 'execute:python') {
- console.log('execute:python', data);
- executePythonAsWorker(data.id, data.code, cb);
- } else if (type === 'request:chat:completion') {
- console.log(data, $socket.id);
- const { session_id, channel, form_data, model } = data;
- try {
- const directConnections = $settings?.directConnections ?? {};
- if (directConnections) {
- const urlIdx = model?.urlIdx;
- const OPENAI_API_URL = directConnections.OPENAI_API_BASE_URLS[urlIdx];
- const OPENAI_API_KEY = directConnections.OPENAI_API_KEYS[urlIdx];
- const API_CONFIG = directConnections.OPENAI_API_CONFIGS[urlIdx];
- try {
- if (API_CONFIG?.prefix_id) {
- const prefixId = API_CONFIG.prefix_id;
- form_data['model'] = form_data['model'].replace(`${prefixId}.`, ``);
- }
- const [res, controller] = await chatCompletion(
- OPENAI_API_KEY,
- form_data,
- OPENAI_API_URL
- );
- if (res) {
- // raise if the response is not ok
- if (!res.ok) {
- throw await res.json();
- }
- if (form_data?.stream ?? false) {
- cb({
- status: true
- });
- console.log({ status: true });
- // res will either be SSE or JSON
- const reader = res.body.getReader();
- const decoder = new TextDecoder();
- const processStream = async () => {
- while (true) {
- // Read data chunks from the response stream
- const { done, value } = await reader.read();
- if (done) {
- break;
- }
- // Decode the received chunk
- const chunk = decoder.decode(value, { stream: true });
- // Process lines within the chunk
- const lines = chunk.split('\n').filter((line) => line.trim() !== '');
- for (const line of lines) {
- console.log(line);
- $socket?.emit(channel, line);
- }
- }
- };
- // Process the stream in the background
- await processStream();
- } else {
- const data = await res.json();
- cb(data);
- }
- } else {
- throw new Error('An error occurred while fetching the completion');
- }
- } catch (error) {
- console.error('chatCompletion', error);
- cb(error);
- }
- }
- } catch (error) {
- console.error('chatCompletion', error);
- cb(error);
- } finally {
- $socket.emit(channel, {
- done: true
- });
- }
- } else {
- console.log('chatEventHandler', event);
- }
- }
- };
- 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 (typeof window !== 'undefined' && window.applyTheme) {
- window.applyTheme();
- }
- if (window?.electronAPI) {
- const info = await window.electronAPI.send({
- type: 'app:info'
- });
- if (info) {
- isApp.set(true);
- appInfo.set(info);
- const data = await window.electronAPI.send({
- type: 'app:data'
- });
- if (data) {
- appData.set(data);
- }
- }
- }
- // 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">
- <AppSidebar />
- <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"
- />
|