diff --git a/backend/apps/audio/main.py b/backend/apps/audio/main.py index 2121ffe6f5..d66a9fa11e 100644 --- a/backend/apps/audio/main.py +++ b/backend/apps/audio/main.py @@ -38,6 +38,7 @@ from config import ( AUDIO_TTS_MODEL, AUDIO_TTS_VOICE, AppConfig, + CORS_ALLOW_ORIGIN, ) from constants import ERROR_MESSAGES from utils.utils import ( @@ -52,7 +53,7 @@ log.setLevel(SRC_LOG_LEVELS["AUDIO"]) app = FastAPI() app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=CORS_ALLOW_ORIGIN, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/backend/apps/images/main.py b/backend/apps/images/main.py index d2f5ddd5d6..401bf55622 100644 --- a/backend/apps/images/main.py +++ b/backend/apps/images/main.py @@ -1,15 +1,10 @@ import re import requests -import base64 from fastapi import ( FastAPI, Request, Depends, HTTPException, - status, - UploadFile, - File, - Form, ) from fastapi.middleware.cors import CORSMiddleware @@ -20,7 +15,6 @@ from utils.utils import ( ) from apps.images.utils.comfyui import ImageGenerationPayload, comfyui_generate_image -from utils.misc import calculate_sha256 from typing import Optional from pydantic import BaseModel from pathlib import Path @@ -51,6 +45,7 @@ from config import ( IMAGE_SIZE, IMAGE_STEPS, AppConfig, + CORS_ALLOW_ORIGIN, ) log = logging.getLogger(__name__) @@ -62,7 +57,7 @@ IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True) app = FastAPI() app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=CORS_ALLOW_ORIGIN, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index 37b72a1053..0fa3abb6d4 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -41,6 +41,7 @@ from config import ( MODEL_FILTER_LIST, UPLOAD_DIR, AppConfig, + CORS_ALLOW_ORIGIN, ) from utils.misc import ( calculate_sha256, @@ -55,7 +56,7 @@ log.setLevel(SRC_LOG_LEVELS["OLLAMA"]) app = FastAPI() app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=CORS_ALLOW_ORIGIN, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py index d344c66222..9ad67c40c7 100644 --- a/backend/apps/openai/main.py +++ b/backend/apps/openai/main.py @@ -32,6 +32,7 @@ from config import ( ENABLE_MODEL_FILTER, MODEL_FILTER_LIST, AppConfig, + CORS_ALLOW_ORIGIN, ) from typing import Optional, Literal, overload @@ -45,7 +46,7 @@ log.setLevel(SRC_LOG_LEVELS["OPENAI"]) app = FastAPI() app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=CORS_ALLOW_ORIGIN, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index f9788556bc..7b2fbc6794 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -129,6 +129,7 @@ from config import ( RAG_WEB_SEARCH_RESULT_COUNT, RAG_WEB_SEARCH_CONCURRENT_REQUESTS, RAG_EMBEDDING_OPENAI_BATCH_SIZE, + CORS_ALLOW_ORIGIN, ) from constants import ERROR_MESSAGES @@ -240,12 +241,9 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function( app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE, ) -origins = ["*"] - - app.add_middleware( CORSMiddleware, - allow_origins=origins, + allow_origins=CORS_ALLOW_ORIGIN, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/backend/apps/webui/main.py b/backend/apps/webui/main.py index 2ed35bf174..2dbe7f787b 100644 --- a/backend/apps/webui/main.py +++ b/backend/apps/webui/main.py @@ -44,10 +44,12 @@ from config import ( JWT_EXPIRES_IN, WEBUI_BANNERS, ENABLE_COMMUNITY_SHARING, + ENABLE_MESSAGE_RATING, AppConfig, OAUTH_USERNAME_CLAIM, OAUTH_PICTURE_CLAIM, OAUTH_EMAIL_CLAIM, + CORS_ALLOW_ORIGIN, ) from apps.socket.main import get_event_call, get_event_emitter @@ -60,8 +62,6 @@ from pydantic import BaseModel app = FastAPI() -origins = ["*"] - app.state.config = AppConfig() app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP @@ -83,6 +83,7 @@ app.state.config.WEBHOOK_URL = WEBHOOK_URL app.state.config.BANNERS = WEBUI_BANNERS app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING +app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM @@ -94,7 +95,7 @@ app.state.FUNCTIONS = {} app.add_middleware( CORSMiddleware, - allow_origins=origins, + allow_origins=CORS_ALLOW_ORIGIN, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/backend/apps/webui/routers/auths.py b/backend/apps/webui/routers/auths.py index e2d6a5036f..c1f46293d1 100644 --- a/backend/apps/webui/routers/auths.py +++ b/backend/apps/webui/routers/auths.py @@ -352,6 +352,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)): "DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE, "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN, "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING, + "ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING, } @@ -361,6 +362,7 @@ class AdminConfig(BaseModel): DEFAULT_USER_ROLE: str JWT_EXPIRES_IN: str ENABLE_COMMUNITY_SHARING: bool + ENABLE_MESSAGE_RATING: bool @router.post("/admin/config") @@ -382,6 +384,7 @@ async def update_admin_config( request.app.state.config.ENABLE_COMMUNITY_SHARING = ( form_data.ENABLE_COMMUNITY_SHARING ) + request.app.state.config.ENABLE_MESSAGE_RATING = form_data.ENABLE_MESSAGE_RATING return { "SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS, @@ -389,6 +392,7 @@ async def update_admin_config( "DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE, "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN, "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING, + "ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING, } diff --git a/backend/apps/webui/routers/utils.py b/backend/apps/webui/routers/utils.py index 7a3c339324..8bf8267da1 100644 --- a/backend/apps/webui/routers/utils.py +++ b/backend/apps/webui/routers/utils.py @@ -85,9 +85,10 @@ async def download_chat_as_pdf( pdf.add_font("NotoSans", "i", f"{FONTS_DIR}/NotoSans-Italic.ttf") pdf.add_font("NotoSansKR", "", f"{FONTS_DIR}/NotoSansKR-Regular.ttf") pdf.add_font("NotoSansJP", "", f"{FONTS_DIR}/NotoSansJP-Regular.ttf") + pdf.add_font("NotoSansSC", "", f"{FONTS_DIR}/NotoSansSC-Regular.ttf") pdf.set_font("NotoSans", size=12) - pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP"]) + pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP", "NotoSansSC"]) pdf.set_auto_page_break(auto=True, margin=15) diff --git a/backend/config.py b/backend/config.py index 6d73eec0d9..72f3b5e5a8 100644 --- a/backend/config.py +++ b/backend/config.py @@ -3,6 +3,8 @@ import sys import logging import importlib.metadata import pkgutil +from urllib.parse import urlparse + import chromadb from chromadb import Settings from bs4 import BeautifulSoup @@ -805,10 +807,24 @@ USER_PERMISSIONS_CHAT_DELETION = ( os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true" ) +USER_PERMISSIONS_CHAT_EDITING = ( + os.environ.get("USER_PERMISSIONS_CHAT_EDITING", "True").lower() == "true" +) + +USER_PERMISSIONS_CHAT_TEMPORARY = ( + os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true" +) + USER_PERMISSIONS = PersistentConfig( "USER_PERMISSIONS", "ui.user_permissions", - {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}}, + { + "chat": { + "deletion": USER_PERMISSIONS_CHAT_DELETION, + "editing": USER_PERMISSIONS_CHAT_EDITING, + "temporary": USER_PERMISSIONS_CHAT_TEMPORARY, + } + }, ) ENABLE_MODEL_FILTER = PersistentConfig( @@ -839,6 +855,47 @@ ENABLE_COMMUNITY_SHARING = PersistentConfig( os.environ.get("ENABLE_COMMUNITY_SHARING", "True").lower() == "true", ) +ENABLE_MESSAGE_RATING = PersistentConfig( + "ENABLE_MESSAGE_RATING", + "ui.enable_message_rating", + os.environ.get("ENABLE_MESSAGE_RATING", "True").lower() == "true", +) + + +def validate_cors_origins(origins): + for origin in origins: + if origin != "*": + validate_cors_origin(origin) + + +def validate_cors_origin(origin): + parsed_url = urlparse(origin) + + # Check if the scheme is either http or https + if parsed_url.scheme not in ["http", "https"]: + raise ValueError( + f"Invalid scheme in CORS_ALLOW_ORIGIN: '{origin}'. Only 'http' and 'https' are allowed." + ) + + # Ensure that the netloc (domain + port) is present, indicating it's a valid URL + if not parsed_url.netloc: + raise ValueError(f"Invalid URL structure in CORS_ALLOW_ORIGIN: '{origin}'.") + + +# For production, you should only need one host as +# fastapi serves the svelte-kit built frontend and backend from the same host and port. +# To test CORS_ALLOW_ORIGIN locally, you can set something like +# CORS_ALLOW_ORIGIN=http://localhost:5173;http://localhost:8080 +# in your .env file depending on your frontend port, 5173 in this case. +CORS_ALLOW_ORIGIN = os.environ.get("CORS_ALLOW_ORIGIN", "*").split(";") + +if "*" in CORS_ALLOW_ORIGIN: + log.warning( + "\n\nWARNING: CORS_ALLOW_ORIGIN IS SET TO '*' - NOT RECOMMENDED FOR PRODUCTION DEPLOYMENTS.\n" + ) + +validate_cors_origins(CORS_ALLOW_ORIGIN) + class BannerModel(BaseModel): id: str @@ -894,10 +951,7 @@ TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig( "task.title.prompt_template", os.environ.get( "TITLE_GENERATION_PROMPT_TEMPLATE", - """Here is the query: -{{prompt:middletruncate:8000}} - -Create a concise, 3-5 word phrase with an emoji as a title for the previous query. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT. + """Create a concise, 3-5 word title with an emoji as a title for the prompt in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT. Examples of titles: 📉 Stock Market Trends @@ -905,7 +959,9 @@ Examples of titles: Evolution of Music Streaming Remote Work Productivity Tips Artificial Intelligence in Healthcare -🎮 Video Game Development Insights""", +🎮 Video Game Development Insights + +Prompt: {{prompt:middletruncate:8000}}""", ), ) diff --git a/backend/main.py b/backend/main.py index 3e3d265a28..1557de2b9d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -67,6 +67,7 @@ from utils.utils import ( get_http_authorization_cred, get_password_hash, create_token, + decode_token, ) from utils.task import ( title_generation_template, @@ -120,6 +121,7 @@ from config import ( WEBUI_SESSION_COOKIE_SECURE, ENABLE_ADMIN_CHAT_ACCESS, AppConfig, + CORS_ALLOW_ORIGIN, ) from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES, TASKS @@ -210,8 +212,6 @@ app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = ( app.state.MODELS = {} -origins = ["*"] - ################################## # @@ -754,7 +754,7 @@ app.add_middleware(PipelineMiddleware) app.add_middleware( CORSMiddleware, - allow_origins=origins, + allow_origins=CORS_ALLOW_ORIGIN, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -1881,40 +1881,61 @@ async def update_pipeline_valves( @app.get("/api/config") -async def get_app_config(): +async def get_app_config(request: Request): + user = None + if "token" in request.cookies: + token = request.cookies.get("token") + data = decode_token(token) + if data is not None and "id" in data: + user = Users.get_user_by_id(data["id"]) + return { "status": True, "name": WEBUI_NAME, "version": VERSION, "default_locale": str(DEFAULT_LOCALE), - "default_models": webui_app.state.config.DEFAULT_MODELS, - "default_prompt_suggestions": webui_app.state.config.DEFAULT_PROMPT_SUGGESTIONS, - "features": { - "auth": WEBUI_AUTH, - "auth_trusted_header": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER), - "enable_signup": webui_app.state.config.ENABLE_SIGNUP, - "enable_login_form": webui_app.state.config.ENABLE_LOGIN_FORM, - "enable_web_search": rag_app.state.config.ENABLE_RAG_WEB_SEARCH, - "enable_image_generation": images_app.state.config.ENABLED, - "enable_community_sharing": webui_app.state.config.ENABLE_COMMUNITY_SHARING, - "enable_admin_export": ENABLE_ADMIN_EXPORT, - "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS, - }, - "audio": { - "tts": { - "engine": audio_app.state.config.TTS_ENGINE, - "voice": audio_app.state.config.TTS_VOICE, - }, - "stt": { - "engine": audio_app.state.config.STT_ENGINE, - }, - }, "oauth": { "providers": { name: config.get("name", name) for name, config in OAUTH_PROVIDERS.items() } }, + "features": { + "auth": WEBUI_AUTH, + "auth_trusted_header": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER), + "enable_signup": webui_app.state.config.ENABLE_SIGNUP, + "enable_login_form": webui_app.state.config.ENABLE_LOGIN_FORM, + **( + { + "enable_web_search": rag_app.state.config.ENABLE_RAG_WEB_SEARCH, + "enable_image_generation": images_app.state.config.ENABLED, + "enable_community_sharing": webui_app.state.config.ENABLE_COMMUNITY_SHARING, + "enable_message_rating": webui_app.state.config.ENABLE_MESSAGE_RATING, + "enable_admin_export": ENABLE_ADMIN_EXPORT, + "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS, + } + if user is not None + else {} + ), + }, + **( + { + "default_models": webui_app.state.config.DEFAULT_MODELS, + "default_prompt_suggestions": webui_app.state.config.DEFAULT_PROMPT_SUGGESTIONS, + "audio": { + "tts": { + "engine": audio_app.state.config.TTS_ENGINE, + "voice": audio_app.state.config.TTS_VOICE, + }, + "stt": { + "engine": audio_app.state.config.STT_ENGINE, + }, + }, + "permissions": {**webui_app.state.config.USER_PERMISSIONS}, + } + if user is not None + else {} + ), } diff --git a/backend/requirements.txt b/backend/requirements.txt index 5bb4ce6dac..04b3261916 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,5 @@ fastapi==0.111.0 -uvicorn[standard]==0.22.0 +uvicorn[standard]==0.30.6 pydantic==2.8.2 python-multipart==0.0.9 @@ -13,17 +13,17 @@ passlib[bcrypt]==1.7.4 requests==2.32.3 aiohttp==3.10.2 -sqlalchemy==2.0.31 +sqlalchemy==2.0.32 alembic==1.13.2 peewee==3.17.6 peewee-migrate==1.12.2 psycopg2-binary==2.9.9 PyMySQL==1.1.1 -bcrypt==4.1.3 +bcrypt==4.2.0 pymongo redis -boto3==1.34.153 +boto3==1.35.0 argon2-cffi==23.1.0 APScheduler==3.10.4 @@ -60,7 +60,7 @@ rapidocr-onnxruntime==1.3.24 fpdf2==2.7.9 rank-bm25==0.2.2 -faster-whisper==1.0.2 +faster-whisper==1.0.3 PyJWT[crypto]==2.9.0 authlib==1.3.1 diff --git a/backend/static/fonts/NotoSansSC-Regular.ttf b/backend/static/fonts/NotoSansSC-Regular.ttf new file mode 100644 index 0000000000..7056f5e97a Binary files /dev/null and b/backend/static/fonts/NotoSansSC-Regular.ttf differ diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index fc01c209dd..8432554785 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -665,6 +665,7 @@ export const getBackendConfig = async () => { const res = await fetch(`${WEBUI_BASE_URL}/api/config`, { method: 'GET', + credentials: 'include', headers: { 'Content-Type': 'application/json' } @@ -949,6 +950,7 @@ export interface ModelConfig { export interface ModelMeta { description?: string; capabilities?: object; + profile_image_url?: string; } export interface ModelParams {} diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts index c4c449156c..d4e994312e 100644 --- a/src/lib/apis/ollama/index.ts +++ b/src/lib/apis/ollama/index.ts @@ -396,7 +396,7 @@ export const deleteModel = async (token: string, tagName: string, urlIdx: string return res; }; -export const pullModel = async (token: string, tagName: string, urlIdx: string | null = null) => { +export const pullModel = async (token: string, tagName: string, urlIdx: number | null = null) => { let error = null; const controller = new AbortController(); diff --git a/src/lib/components/admin/Settings.svelte b/src/lib/components/admin/Settings.svelte index afb8736ea1..e242ab632a 100644 --- a/src/lib/components/admin/Settings.svelte +++ b/src/lib/components/admin/Settings.svelte @@ -336,8 +336,11 @@
{code}
+ {code}
{/if}
{:else}