diff --git a/backend/apps/audio/main.py b/backend/apps/audio/main.py index 2121ffe6f..d66a9fa11 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 d2f5ddd5d..401bf5562 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 37b72a105..0fa3abb6d 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 d344c6622..9ad67c40c 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 f9788556b..7b2fbc679 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 2ed35bf17..2dbe7f787 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 e2d6a5036..c1f46293d 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 7a3c33932..8bf8267da 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 6d73eec0d..72f3b5e5a 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 3e3d265a2..1557de2b9 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 5bb4ce6da..04b326191 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 000000000..7056f5e97 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 fc01c209d..843255478 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 c4c449156..d4e994312 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 afb8736ea..e242ab632 100644 --- a/src/lib/components/admin/Settings.svelte +++ b/src/lib/components/admin/Settings.svelte @@ -336,8 +336,11 @@
{code}+
{code}{/if} {:else}