diff --git a/backend/apps/webui/main.py b/backend/apps/webui/main.py index e19382481..5bb199352 100644 --- a/backend/apps/webui/main.py +++ b/backend/apps/webui/main.py @@ -23,6 +23,7 @@ from config import ( WEBHOOK_URL, WEBUI_AUTH_TRUSTED_EMAIL_HEADER, JWT_EXPIRES_IN, + WEBUI_BANNERS, AppConfig, ) @@ -40,6 +41,7 @@ app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE app.state.config.USER_PERMISSIONS = USER_PERMISSIONS app.state.config.WEBHOOK_URL = WEBHOOK_URL +app.state.config.BANNERS = WEBUI_BANNERS app.state.MODELS = {} diff --git a/backend/apps/webui/routers/configs.py b/backend/apps/webui/routers/configs.py index 00feafb18..c127e721b 100644 --- a/backend/apps/webui/routers/configs.py +++ b/backend/apps/webui/routers/configs.py @@ -8,6 +8,8 @@ from pydantic import BaseModel import time import uuid +from config import BannerModel + from apps.webui.models.users import Users from utils.utils import ( @@ -57,3 +59,31 @@ async def set_global_default_suggestions( data = form_data.model_dump() request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"] return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS + + +############################ +# SetBanners +############################ + + +class SetBannersForm(BaseModel): + banners: List[BannerModel] + + +@router.post("/banners", response_model=List[BannerModel]) +async def set_banners( + request: Request, + form_data: SetBannersForm, + user=Depends(get_admin_user), +): + data = form_data.model_dump() + request.app.state.config.BANNERS = data["banners"] + return request.app.state.config.BANNERS + + +@router.get("/banners", response_model=List[BannerModel]) +async def get_banners( + request: Request, + user=Depends(get_current_user), +): + return request.app.state.config.BANNERS diff --git a/backend/config.py b/backend/config.py index 5d074e250..731a3572a 100644 --- a/backend/config.py +++ b/backend/config.py @@ -8,6 +8,8 @@ from chromadb import Settings from base64 import b64encode from bs4 import BeautifulSoup from typing import TypeVar, Generic, Union +from pydantic import BaseModel +from typing import Optional from pathlib import Path import json @@ -566,6 +568,22 @@ WEBHOOK_URL = PersistentConfig( ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true" + +class BannerModel(BaseModel): + id: str + type: str + title: Optional[str] = None + content: str + dismissible: bool + timestamp: int + + +WEBUI_BANNERS = PersistentConfig( + "WEBUI_BANNERS", + "ui.banners", + [BannerModel(**banner) for banner in json.loads("[]")], +) + #################################### # WEBUI_SECRET_KEY #################################### diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts index 30d562ba4..4f53c53c8 100644 --- a/src/lib/apis/configs/index.ts +++ b/src/lib/apis/configs/index.ts @@ -1,4 +1,5 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; +import type { Banner } from '$lib/types'; export const setDefaultModels = async (token: string, models: string) => { let error = null; @@ -59,3 +60,60 @@ export const setDefaultPromptSuggestions = async (token: string, promptSuggestio return res; }; + +export const getBanners = async (token: string): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/banners`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const setBanners = async (token: string, banners: Banner[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/banners`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + banners: banners + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/components/admin/Settings/Banners.svelte b/src/lib/components/admin/Settings/Banners.svelte new file mode 100644 index 000000000..f9d5f2c22 --- /dev/null +++ b/src/lib/components/admin/Settings/Banners.svelte @@ -0,0 +1,136 @@ + + +
{ + updateBanners(); + saveHandler(); + }} +> +
+
+
+
+ {$i18n.t('Banners')} +
+ + +
+
+ {#each banners as banner, bannerIdx} +
+
+ + + + +
+ + + +
+
+ + +
+ {/each} +
+
+
+
+ +
+
diff --git a/src/lib/components/admin/SettingsModal.svelte b/src/lib/components/admin/SettingsModal.svelte index 923ab576a..38a2602b6 100644 --- a/src/lib/components/admin/SettingsModal.svelte +++ b/src/lib/components/admin/SettingsModal.svelte @@ -6,6 +6,9 @@ import General from './Settings/General.svelte'; import Users from './Settings/Users.svelte'; + import Banners from '$lib/components/admin/Settings/Banners.svelte'; + import { toast } from 'svelte-sonner'; + const i18n = getContext('i18n'); export let show = false; @@ -117,24 +120,63 @@
{$i18n.t('Database')}
+ +
{#if selectedTab === 'general'} { show = false; + toast.success($i18n.t('Settings saved successfully!')); }} /> {:else if selectedTab === 'users'} { show = false; + toast.success($i18n.t('Settings saved successfully!')); }} /> {:else if selectedTab === 'db'} { show = false; + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'banners'} + { + show = false; + toast.success($i18n.t('Settings saved successfully!')); }} /> {/if} diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 507aa785d..f0352af70 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -15,7 +15,8 @@ settings, showSidebar, tags as _tags, - WEBUI_NAME + WEBUI_NAME, + banners } from '$lib/stores'; import { convertMessagesToHistory, copyToClipboard, splitStream } from '$lib/utils'; @@ -40,6 +41,7 @@ import { queryMemory } from '$lib/apis/memories'; import type { Writable } from 'svelte/store'; import type { i18n as i18nType } from 'i18next'; + import Banner from '../common/Banner.svelte'; const i18n: Writable = getContext('i18n'); @@ -1004,6 +1006,32 @@ {chat} {initNewChat} /> + + {#if $banners.length > 0} +
+
+ {#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner} + { + const bannerId = e.detail; + + localStorage.setItem( + 'dismissedBannerIds', + JSON.stringify( + [ + bannerId, + ...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]') + ].filter((id) => $banners.find((b) => b.id === id)) + ) + ); + }} + /> + {/each} +
+
+ {/if} +