diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index b5dd72192..86d27f4dc 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -84,6 +84,8 @@ jobs: outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max + build-args: | + BUILD_HASH=${{ github.sha }} - name: Export digest run: | @@ -170,7 +172,9 @@ jobs: outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - build-args: USE_CUDA=true + build-args: | + BUILD_HASH=${{ github.sha }} + USE_CUDA=true - name: Export digest run: | @@ -257,7 +261,9 @@ jobs: outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - build-args: USE_OLLAMA=true + build-args: | + BUILD_HASH=${{ github.sha }} + USE_OLLAMA=true - name: Export digest run: | diff --git a/Dockerfile b/Dockerfile index 52987b5a6..be5c1da41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,12 +11,14 @@ ARG USE_CUDA_VER=cu121 # IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them. ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 ARG USE_RERANKING_MODEL="" +ARG BUILD_HASH=dev-build # Override at your own risk - non-root configurations are untested ARG UID=0 ARG GID=0 ######## WebUI frontend ######## FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build +ARG BUILD_HASH WORKDIR /app @@ -24,6 +26,7 @@ COPY package.json package-lock.json ./ RUN npm ci COPY . . +ENV APP_BUILD_HASH=${BUILD_HASH} RUN npm run build ######## WebUI backend ######## @@ -35,6 +38,7 @@ ARG USE_OLLAMA ARG USE_CUDA_VER ARG USE_EMBEDDING_MODEL ARG USE_RERANKING_MODEL +ARG BUILD_HASH ARG UID ARG GID @@ -150,4 +154,6 @@ HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.stat USER $UID:$GID +ENV WEBUI_BUILD_VERSION=${BUILD_HASH} + CMD [ "bash", "start.sh"] diff --git a/backend/apps/webui/main.py b/backend/apps/webui/main.py index e5ef35058..b823859a6 100644 --- a/backend/apps/webui/main.py +++ b/backend/apps/webui/main.py @@ -13,7 +13,7 @@ from apps.webui.routers import ( utils, ) from config import ( - WEBUI_VERSION, + WEBUI_BUILD_HASH, WEBUI_AUTH, DEFAULT_MODELS, DEFAULT_PROMPT_SUGGESTIONS, @@ -23,6 +23,7 @@ from config import ( WEBHOOK_URL, WEBUI_AUTH_TRUSTED_EMAIL_HEADER, JWT_EXPIRES_IN, + WEBUI_BANNERS, AppConfig, ENABLE_COMMUNITY_SHARING, ) @@ -41,6 +42,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.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING 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 83eb1b37a..28ace5d5d 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 @@ -166,10 +168,10 @@ CHANGELOG = changelog_json #################################### -# WEBUI_VERSION +# WEBUI_BUILD_HASH #################################### -WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100") +WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build") #################################### # DATA/FRONTEND BUILD DIR @@ -572,6 +574,21 @@ ENABLE_COMMUNITY_SHARING = PersistentConfig( os.environ.get("ENABLE_COMMUNITY_SHARING", "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/backend/main.py b/backend/main.py index 2dfbce802..586ad27e2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -55,6 +55,7 @@ from config import ( WEBHOOK_URL, ENABLE_ADMIN_EXPORT, AppConfig, + WEBUI_BUILD_HASH, ) from constants import ERROR_MESSAGES @@ -84,7 +85,8 @@ print( |_| -v{VERSION} - building the best open-source AI user interface. +v{VERSION} - building the best open-source AI user interface. +{f"Commit: {WEBUI_BUILD_HASH}" if WEBUI_BUILD_HASH != "dev-build" else ""} https://github.com/open-webui/open-webui """ ) diff --git a/hatch_build.py b/hatch_build.py index 2fa9e4805..8ddaf0749 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -1,4 +1,5 @@ # noqa: INP001 +import os import shutil import subprocess from sys import stderr @@ -18,4 +19,5 @@ class CustomBuildHook(BuildHookInterface): stderr.write("### npm install\n") subprocess.run([npm, "install"], check=True) # noqa: S603 stderr.write("\n### npm run build\n") + os.environ["APP_BUILD_HASH"] = version subprocess.run([npm, "run", "build"], check=True) # noqa: S603 diff --git a/package-lock.json b/package-lock.json index 5d5a1c6e3..5b1461939 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.2.0.dev1", + "version": "0.2.0.dev2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.2.0.dev1", + "version": "0.2.0.dev2", "dependencies": { "@pyscript/core": "^0.4.32", "@sveltejs/adapter-node": "^1.3.1", diff --git a/package.json b/package.json index 49a417da0..8522cffe5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.2.0.dev1", + "version": "0.2.0.dev2", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", 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..e440727ac --- /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..10862e56d 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,34 @@ {chat} {initNewChat} /> + + {#if $banners.length > 0 && !$chatId} +
+
+ {#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} +