diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 10958583fc..bfbee10a1b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,36 +8,43 @@ assignees: '' # Bug Report -## Description +## Installation Method -**Bug Summary:** -[Provide a brief but clear summary of the bug] - -**Steps to Reproduce:** -[Outline the steps to reproduce the bug. Be as detailed as possible.] - -**Expected Behavior:** -[Describe what you expected to happen.] - -**Actual Behavior:** -[Describe what actually happened.] +[Describe the method you used to install the project, e.g., git clone, Docker, pip, etc.] ## Environment -- **Open WebUI Version:** [e.g., 0.1.120] -- **Ollama (if applicable):** [e.g., 0.1.30, 0.1.32-rc1] +- **Open WebUI Version:** [e.g., v0.3.11] +- **Ollama (if applicable):** [e.g., v0.2.0, v0.1.32-rc1] - **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04] - **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0] -## Reproduction Details - **Confirmation:** - [ ] I have read and followed all the instructions provided in the README.md. - [ ] I am on the latest version of both Open WebUI and Ollama. - [ ] I have included the browser console logs. - [ ] I have included the Docker container logs. +- [ ] I have provided the exact steps to reproduce the bug in the "Steps to Reproduce" section below. + +## Expected Behavior: + +[Describe what you expected to happen.] + +## Actual Behavior: + +[Describe what actually happened.] + +## Description + +**Bug Summary:** +[Provide a brief but clear summary of the bug] + +## Reproduction Details + +**Steps to Reproduce:** +[Outline the steps to reproduce the bug. Be as detailed as possible.] ## Logs and Screenshots @@ -47,13 +54,9 @@ assignees: '' **Docker Container Logs:** [Include relevant Docker container logs, if applicable] -**Screenshots (if applicable):** +**Screenshots/Screen Recordings (if applicable):** [Attach any relevant screenshots to help illustrate the issue] -## Installation Method - -[Describe the method you used to install the project, e.g., manual installation, Docker, package manager, etc.] - ## Additional Information [Include any additional details that may help in understanding and reproducing the issue. This could include specific configurations, error messages, or anything else relevant to the bug.] diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 639ea789fb..41c19d32d9 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -26,6 +26,10 @@ jobs: --file docker-compose.a1111-test.yaml \ up --detach --build + - name: Delete Docker build cache + run: | + docker builder prune --all --force + - name: Wait for Ollama to be up timeout-minutes: 5 run: | @@ -35,10 +39,6 @@ jobs: done echo "Service is up!" - - name: Delete Docker build cache - run: | - docker builder prune --all --force - - name: Preload Ollama model run: | docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K diff --git a/CHANGELOG.md b/CHANGELOG.md index d62360f878..d0b175fbfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.12] - 2024-08-07 + +### Added + +- **🔄 Sidebar Infinite Scroll**: Added an infinite scroll feature in the sidebar for more efficient chat navigation, reducing load times and enhancing user experience. +- **🚀 Enhanced Markdown Rendering**: Support for rendering all code blocks and making images clickable for preview; codespan styling is also enhanced to improve readability and user interaction. +- **🔒 Admin Shared Chat Visibility**: Admins no longer have default visibility over shared chats when ENABLE_ADMIN_CHAT_ACCESS is set to false, tightening security and privacy settings for users. +- **🌍 Language Updates**: Added Malay (Bahasa Malaysia) translation and updated Catalan and Traditional Chinese translations to improve accessibility for more users. + +### Fixed + +- **📊 Markdown Rendering Issues**: Resolved issues with markdown rendering to ensure consistent and correct display across components. +- **🛠️ Styling Issues**: Multiple fixes applied to styling throughout the application, improving the overall visual experience and interface consistency. +- **🗃️ Modal Handling**: Fixed an issue where modals were not closing correctly in various model chat scenarios, enhancing usability and interface reliability. +- **📄 Missing OpenAI Usage Information**: Resolved issues where usage statistics for OpenAI services were not being correctly displayed, ensuring users have access to crucial data for managing and monitoring their API consumption. +- **🔧 Non-Streaming Support for Functions Plugin**: Fixed a functionality issue with the Functions plugin where non-streaming operations were not functioning as intended, restoring full capabilities for async and sync integration within the platform. +- **🔄 Environment Variable Type Correction (COMFYUI_FLUX_FP8_CLIP)**: Corrected the data type of the 'COMFYUI_FLUX_FP8_CLIP' environment variable from string to boolean, ensuring environment settings apply correctly and enhance configuration management. + +### Changed + +- **🔧 Backend Dependency Updates**: Updated several backend dependencies such as boto3, pypdf, python-pptx, validators, and black, ensuring up-to-date security and performance optimizations. + ## [0.3.11] - 2024-08-02 ### Added diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py index c712709a5c..a0d8f37507 100644 --- a/backend/apps/openai/main.py +++ b/backend/apps/openai/main.py @@ -1,6 +1,6 @@ -from fastapi import FastAPI, Request, Response, HTTPException, Depends +from fastapi import FastAPI, Request, HTTPException, Depends from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import StreamingResponse, JSONResponse, FileResponse +from fastapi.responses import StreamingResponse, FileResponse import requests import aiohttp @@ -12,16 +12,12 @@ from pydantic import BaseModel from starlette.background import BackgroundTask from apps.webui.models.models import Models -from apps.webui.models.users import Users from constants import ERROR_MESSAGES from utils.utils import ( - decode_token, - get_verified_user, get_verified_user, get_admin_user, ) -from utils.task import prompt_template -from utils.misc import add_or_update_system_message +from utils.misc import apply_model_params_to_body, apply_model_system_prompt_to_body from config import ( SRC_LOG_LEVELS, @@ -34,7 +30,7 @@ from config import ( MODEL_FILTER_LIST, AppConfig, ) -from typing import List, Optional +from typing import List, Optional, Literal, overload import hashlib @@ -69,8 +65,6 @@ app.state.MODELS = {} async def check_url(request: Request, call_next): if len(app.state.MODELS) == 0: await get_all_models() - else: - pass response = await call_next(request) return response @@ -175,7 +169,7 @@ async def speech(request: Request, user=Depends(get_verified_user)): res = r.json() if "error" in res: error_detail = f"External: {res['error']}" - except: + except Exception: error_detail = f"External: {e}" raise HTTPException( @@ -234,64 +228,68 @@ def merge_models_lists(model_lists): return merged_list -async def get_all_models(raw: bool = False): +def is_openai_api_disabled(): + api_keys = app.state.config.OPENAI_API_KEYS + no_keys = len(api_keys) == 1 and api_keys[0] == "" + return no_keys or not app.state.config.ENABLE_OPENAI_API + + +async def get_all_models_raw() -> list: + if is_openai_api_disabled(): + return [] + + # Check if API KEYS length is same than API URLS length + num_urls = len(app.state.config.OPENAI_API_BASE_URLS) + num_keys = len(app.state.config.OPENAI_API_KEYS) + + if num_keys != num_urls: + # if there are more keys than urls, remove the extra keys + if num_keys > num_urls: + new_keys = app.state.config.OPENAI_API_KEYS[:num_urls] + app.state.config.OPENAI_API_KEYS = new_keys + # if there are more urls than keys, add empty keys + else: + app.state.config.OPENAI_API_KEYS += [""] * (num_urls - num_keys) + + tasks = [ + fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx]) + for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS) + ] + + responses = await asyncio.gather(*tasks) + log.debug(f"get_all_models:responses() {responses}") + + return responses + + +@overload +async def get_all_models(raw: Literal[True]) -> list: ... + + +@overload +async def get_all_models(raw: Literal[False] = False) -> dict[str, list]: ... + + +async def get_all_models(raw=False) -> dict[str, list] | list: log.info("get_all_models()") + if is_openai_api_disabled(): + return [] if raw else {"data": []} - if ( - len(app.state.config.OPENAI_API_KEYS) == 1 - and app.state.config.OPENAI_API_KEYS[0] == "" - ) or not app.state.config.ENABLE_OPENAI_API: - models = {"data": []} - else: - # Check if API KEYS length is same than API URLS length - if len(app.state.config.OPENAI_API_KEYS) != len( - app.state.config.OPENAI_API_BASE_URLS - ): - # if there are more keys than urls, remove the extra keys - if len(app.state.config.OPENAI_API_KEYS) > len( - app.state.config.OPENAI_API_BASE_URLS - ): - app.state.config.OPENAI_API_KEYS = app.state.config.OPENAI_API_KEYS[ - : len(app.state.config.OPENAI_API_BASE_URLS) - ] - # if there are more urls than keys, add empty keys - else: - app.state.config.OPENAI_API_KEYS += [ - "" - for _ in range( - len(app.state.config.OPENAI_API_BASE_URLS) - - len(app.state.config.OPENAI_API_KEYS) - ) - ] + responses = await get_all_models_raw() + if raw: + return responses - tasks = [ - fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx]) - for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS) - ] + def extract_data(response): + if response and "data" in response: + return response["data"] + if isinstance(response, list): + return response + return None - responses = await asyncio.gather(*tasks) - log.debug(f"get_all_models:responses() {responses}") + models = {"data": merge_models_lists(map(extract_data, responses))} - if raw: - return responses - - models = { - "data": merge_models_lists( - list( - map( - lambda response: ( - response["data"] - if (response and "data" in response) - else (response if isinstance(response, list) else None) - ), - responses, - ) - ) - ) - } - - log.debug(f"models: {models}") - app.state.MODELS = {model["id"]: model for model in models["data"]} + log.debug(f"models: {models}") + app.state.MODELS = {model["id"]: model for model in models["data"]} return models @@ -299,7 +297,7 @@ async def get_all_models(raw: bool = False): @app.get("/models") @app.get("/models/{url_idx}") async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)): - if url_idx == None: + if url_idx is None: models = await get_all_models() if app.state.config.ENABLE_MODEL_FILTER: if user.role == "user": @@ -340,7 +338,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us res = r.json() if "error" in res: error_detail = f"External: {res['error']}" - except: + except Exception: error_detail = f"External: {e}" raise HTTPException( @@ -358,8 +356,7 @@ async def generate_chat_completion( ): idx = 0 payload = {**form_data} - if "metadata" in payload: - del payload["metadata"] + payload.pop("metadata") model_id = form_data.get("model") model_info = Models.get_model_by_id(model_id) @@ -368,70 +365,9 @@ async def generate_chat_completion( if model_info.base_model_id: payload["model"] = model_info.base_model_id - model_info.params = model_info.params.model_dump() - - if model_info.params: - if ( - model_info.params.get("temperature", None) is not None - and payload.get("temperature") is None - ): - payload["temperature"] = float(model_info.params.get("temperature")) - - if model_info.params.get("top_p", None) and payload.get("top_p") is None: - payload["top_p"] = int(model_info.params.get("top_p", None)) - - if ( - model_info.params.get("max_tokens", None) - and payload.get("max_tokens") is None - ): - payload["max_tokens"] = int(model_info.params.get("max_tokens", None)) - - if ( - model_info.params.get("frequency_penalty", None) - and payload.get("frequency_penalty") is None - ): - payload["frequency_penalty"] = int( - model_info.params.get("frequency_penalty", None) - ) - - if ( - model_info.params.get("seed", None) is not None - and payload.get("seed") is None - ): - payload["seed"] = model_info.params.get("seed", None) - - if model_info.params.get("stop", None) and payload.get("stop") is None: - payload["stop"] = ( - [ - bytes(stop, "utf-8").decode("unicode_escape") - for stop in model_info.params["stop"] - ] - if model_info.params.get("stop", None) - else None - ) - - system = model_info.params.get("system", None) - if system: - system = prompt_template( - system, - **( - { - "user_name": user.name, - "user_location": ( - user.info.get("location") if user.info else None - ), - } - if user - else {} - ), - ) - if payload.get("messages"): - payload["messages"] = add_or_update_system_message( - system, payload["messages"] - ) - - else: - pass + params = model_info.params.model_dump() + payload = apply_model_params_to_body(params, payload) + payload = apply_model_system_prompt_to_body(params, payload, user) model = app.state.MODELS[payload.get("model")] idx = model["urlIdx"] @@ -444,13 +380,6 @@ async def generate_chat_completion( "role": user.role, } - # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000 - # This is a workaround until OpenAI fixes the issue with this model - if payload.get("model") == "gpt-4-vision-preview": - if "max_tokens" not in payload: - payload["max_tokens"] = 4000 - log.debug("Modified payload:", payload) - # Convert the modified body back to JSON payload = json.dumps(payload) @@ -506,7 +435,7 @@ async def generate_chat_completion( print(res) if "error" in res: error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}" - except: + except Exception: error_detail = f"External: {e}" raise HTTPException(status_code=r.status if r else 500, detail=error_detail) finally: @@ -569,7 +498,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): print(res) if "error" in res: error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}" - except: + except Exception: error_detail = f"External: {e}" raise HTTPException(status_code=r.status if r else 500, detail=error_detail) finally: diff --git a/backend/apps/socket/main.py b/backend/apps/socket/main.py index 1d98d37ff1..fcffca4209 100644 --- a/backend/apps/socket/main.py +++ b/backend/apps/socket/main.py @@ -44,23 +44,26 @@ async def user_join(sid, data): print("user-join", sid, data) auth = data["auth"] if "auth" in data else None + if not auth or "token" not in auth: + return - if auth and "token" in auth: - data = decode_token(auth["token"]) + data = decode_token(auth["token"]) + if data is None or "id" not in data: + return - if data is not None and "id" in data: - user = Users.get_user_by_id(data["id"]) + user = Users.get_user_by_id(data["id"]) + if not user: + return - if user: - SESSION_POOL[sid] = user.id - if user.id in USER_POOL: - USER_POOL[user.id].append(sid) - else: - USER_POOL[user.id] = [sid] + SESSION_POOL[sid] = user.id + if user.id in USER_POOL: + USER_POOL[user.id].append(sid) + else: + USER_POOL[user.id] = [sid] - print(f"user {user.name}({user.id}) connected with session ID {sid}") + print(f"user {user.name}({user.id}) connected with session ID {sid}") - await sio.emit("user-count", {"count": len(set(USER_POOL))}) + await sio.emit("user-count", {"count": len(set(USER_POOL))}) @sio.on("user-count") diff --git a/backend/apps/webui/main.py b/backend/apps/webui/main.py index 972562a04d..a0b9f50085 100644 --- a/backend/apps/webui/main.py +++ b/backend/apps/webui/main.py @@ -22,9 +22,9 @@ from apps.webui.utils import load_function_module_by_id from utils.misc import ( openai_chat_chunk_message_template, openai_chat_completion_message_template, - add_or_update_system_message, + apply_model_params_to_body, + apply_model_system_prompt_to_body, ) -from utils.task import prompt_template from config import ( @@ -269,47 +269,6 @@ def get_function_params(function_module, form_data, user, extra_params={}): return params -# inplace function: form_data is modified -def apply_model_params_to_body(params: dict, form_data: dict) -> dict: - if not params: - return form_data - - mappings = { - "temperature": float, - "top_p": int, - "max_tokens": int, - "frequency_penalty": int, - "seed": lambda x: x, - "stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x], - } - - for key, cast_func in mappings.items(): - if (value := params.get(key)) is not None: - form_data[key] = cast_func(value) - - return form_data - - -# inplace function: form_data is modified -def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict: - system = params.get("system", None) - if not system: - return form_data - - if user: - template_params = { - "user_name": user.name, - "user_location": user.info.get("location") if user.info else None, - } - else: - template_params = {} - system = prompt_template(system, **template_params) - form_data["messages"] = add_or_update_system_message( - system, form_data.get("messages", []) - ) - return form_data - - async def generate_function_chat_completion(form_data, user): model_id = form_data.get("model") model_info = Models.get_model_by_id(model_id) diff --git a/backend/apps/webui/models/chats.py b/backend/apps/webui/models/chats.py index abde4f2b31..d504b18c3f 100644 --- a/backend/apps/webui/models/chats.py +++ b/backend/apps/webui/models/chats.py @@ -250,7 +250,7 @@ class ChatTable: user_id: str, include_archived: bool = False, skip: int = 0, - limit: int = 50, + limit: int = -1, ) -> List[ChatTitleIdResponse]: with get_db() as db: query = db.query(Chat).filter_by(user_id=user_id) @@ -260,9 +260,10 @@ class ChatTable: all_chats = ( query.order_by(Chat.updated_at.desc()) # limit cols - .with_entities( - Chat.id, Chat.title, Chat.updated_at, Chat.created_at - ).all() + .with_entities(Chat.id, Chat.title, Chat.updated_at, Chat.created_at) + .limit(limit) + .offset(skip) + .all() ) # result has to be destrctured from sqlalchemy `row` and mapped to a dict since the `ChatModel`is not the returned dataclass. return [ diff --git a/backend/apps/webui/routers/chats.py b/backend/apps/webui/routers/chats.py index 80308a451b..6e89722d35 100644 --- a/backend/apps/webui/routers/chats.py +++ b/backend/apps/webui/routers/chats.py @@ -28,7 +28,7 @@ from apps.webui.models.tags import ( from constants import ERROR_MESSAGES -from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT +from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT, ENABLE_ADMIN_CHAT_ACCESS log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -43,9 +43,15 @@ router = APIRouter() @router.get("/", response_model=List[ChatTitleIdResponse]) @router.get("/list", response_model=List[ChatTitleIdResponse]) async def get_session_user_chat_list( - user=Depends(get_verified_user), skip: int = 0, limit: int = 50 + user=Depends(get_verified_user), page: Optional[int] = None ): - return Chats.get_chat_title_id_list_by_user_id(user.id, skip=skip, limit=limit) + if page is not None: + limit = 60 + skip = (page - 1) * limit + + return Chats.get_chat_title_id_list_by_user_id(user.id, skip=skip, limit=limit) + else: + return Chats.get_chat_title_id_list_by_user_id(user.id) ############################ @@ -81,6 +87,11 @@ async def get_user_chat_list_by_user_id( skip: int = 0, limit: int = 50, ): + if not ENABLE_ADMIN_CHAT_ACCESS: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) return Chats.get_chat_list_by_user_id( user_id, include_archived=True, skip=skip, limit=limit ) @@ -181,9 +192,9 @@ async def get_shared_chat_by_id(share_id: str, user=Depends(get_verified_user)): status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND ) - if user.role == "user": + if user.role == "user" or (user.role == "admin" and not ENABLE_ADMIN_CHAT_ACCESS): chat = Chats.get_chat_by_share_id(share_id) - elif user.role == "admin": + elif user.role == "admin" and ENABLE_ADMIN_CHAT_ACCESS: chat = Chats.get_chat_by_id(share_id) if chat: diff --git a/backend/apps/webui/routers/tools.py b/backend/apps/webui/routers/tools.py index ea9db8180b..7e60fe4d1e 100644 --- a/backend/apps/webui/routers/tools.py +++ b/backend/apps/webui/routers/tools.py @@ -1,12 +1,8 @@ -from fastapi import Depends, FastAPI, HTTPException, status, Request -from datetime import datetime, timedelta -from typing import List, Union, Optional +from fastapi import Depends, HTTPException, status, Request +from typing import List, Optional from fastapi import APIRouter -from pydantic import BaseModel -import json -from apps.webui.models.users import Users from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse from apps.webui.utils import load_toolkit_module_by_id @@ -14,7 +10,6 @@ from utils.utils import get_admin_user, get_verified_user from utils.tools import get_tools_specs from constants import ERROR_MESSAGES -from importlib import util import os from pathlib import Path @@ -69,7 +64,7 @@ async def create_new_toolkit( form_data.id = form_data.id.lower() toolkit = Tools.get_tool_by_id(form_data.id) - if toolkit == None: + if toolkit is None: toolkit_path = os.path.join(TOOLS_DIR, f"{form_data.id}.py") try: with open(toolkit_path, "w") as tool_file: @@ -98,7 +93,7 @@ async def create_new_toolkit( print(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(e), + detail=ERROR_MESSAGES.DEFAULT(str(e)), ) else: raise HTTPException( @@ -170,7 +165,7 @@ async def update_toolkit_by_id( except Exception as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(e), + detail=ERROR_MESSAGES.DEFAULT(str(e)), ) @@ -210,7 +205,7 @@ async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)): except Exception as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(e), + detail=ERROR_MESSAGES.DEFAULT(str(e)), ) else: raise HTTPException( @@ -233,7 +228,7 @@ async def get_toolkit_valves_spec_by_id( if id in request.app.state.TOOLS: toolkit_module = request.app.state.TOOLS[id] else: - toolkit_module, frontmatter = load_toolkit_module_by_id(id) + toolkit_module, _ = load_toolkit_module_by_id(id) request.app.state.TOOLS[id] = toolkit_module if hasattr(toolkit_module, "Valves"): @@ -261,7 +256,7 @@ async def update_toolkit_valves_by_id( if id in request.app.state.TOOLS: toolkit_module = request.app.state.TOOLS[id] else: - toolkit_module, frontmatter = load_toolkit_module_by_id(id) + toolkit_module, _ = load_toolkit_module_by_id(id) request.app.state.TOOLS[id] = toolkit_module if hasattr(toolkit_module, "Valves"): @@ -276,7 +271,7 @@ async def update_toolkit_valves_by_id( print(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(e), + detail=ERROR_MESSAGES.DEFAULT(str(e)), ) else: raise HTTPException( @@ -306,7 +301,7 @@ async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user) except Exception as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(e), + detail=ERROR_MESSAGES.DEFAULT(str(e)), ) else: raise HTTPException( @@ -324,7 +319,7 @@ async def get_toolkit_user_valves_spec_by_id( if id in request.app.state.TOOLS: toolkit_module = request.app.state.TOOLS[id] else: - toolkit_module, frontmatter = load_toolkit_module_by_id(id) + toolkit_module, _ = load_toolkit_module_by_id(id) request.app.state.TOOLS[id] = toolkit_module if hasattr(toolkit_module, "UserValves"): @@ -348,7 +343,7 @@ async def update_toolkit_user_valves_by_id( if id in request.app.state.TOOLS: toolkit_module = request.app.state.TOOLS[id] else: - toolkit_module, frontmatter = load_toolkit_module_by_id(id) + toolkit_module, _ = load_toolkit_module_by_id(id) request.app.state.TOOLS[id] = toolkit_module if hasattr(toolkit_module, "UserValves"): @@ -365,7 +360,7 @@ async def update_toolkit_user_valves_by_id( print(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(e), + detail=ERROR_MESSAGES.DEFAULT(str(e)), ) else: raise HTTPException( diff --git a/backend/config.py b/backend/config.py index e976b226df..30a970012b 100644 --- a/backend/config.py +++ b/backend/config.py @@ -824,6 +824,10 @@ WEBHOOK_URL = PersistentConfig( ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true" +ENABLE_ADMIN_CHAT_ACCESS = ( + os.environ.get("ENABLE_ADMIN_CHAT_ACCESS", "True").lower() == "true" +) + ENABLE_COMMUNITY_SHARING = PersistentConfig( "ENABLE_COMMUNITY_SHARING", "ui.enable_community_sharing", @@ -1317,7 +1321,7 @@ COMFYUI_FLUX_WEIGHT_DTYPE = PersistentConfig( COMFYUI_FLUX_FP8_CLIP = PersistentConfig( "COMFYUI_FLUX_FP8_CLIP", "image_generation.comfyui.flux_fp8_clip", - os.getenv("COMFYUI_FLUX_FP8_CLIP", ""), + os.environ.get("COMFYUI_FLUX_FP8_CLIP", "").lower() == "true", ) IMAGES_OPENAI_API_BASE_URL = PersistentConfig( diff --git a/backend/main.py b/backend/main.py index a7dd8bc23b..d7bff888e2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -116,6 +116,7 @@ from config import ( WEBUI_SECRET_KEY, WEBUI_SESSION_COOKIE_SAME_SITE, WEBUI_SESSION_COOKIE_SECURE, + ENABLE_ADMIN_CHAT_ACCESS, AppConfig, ) @@ -957,7 +958,7 @@ async def get_all_models(): custom_models = Models.get_all_models() for custom_model in custom_models: - if custom_model.base_model_id == None: + if custom_model.base_model_id is None: for model in models: if ( custom_model.id == model["id"] @@ -1662,7 +1663,7 @@ async def get_pipelines_list(user=Depends(get_admin_user)): urlIdxs = [ idx for idx, response in enumerate(responses) - if response != None and "pipelines" in response + if response is not None and "pipelines" in response ] return { @@ -1723,7 +1724,7 @@ async def upload_pipeline( res = r.json() if "detail" in res: detail = res["detail"] - except: + except Exception: pass raise HTTPException( @@ -1769,7 +1770,7 @@ async def add_pipeline(form_data: AddPipelineForm, user=Depends(get_admin_user)) res = r.json() if "detail" in res: detail = res["detail"] - except: + except Exception: pass raise HTTPException( @@ -1811,7 +1812,7 @@ async def delete_pipeline(form_data: DeletePipelineForm, user=Depends(get_admin_ res = r.json() if "detail" in res: detail = res["detail"] - except: + except Exception: pass raise HTTPException( @@ -1844,7 +1845,7 @@ async def get_pipelines(urlIdx: Optional[int] = None, user=Depends(get_admin_use res = r.json() if "detail" in res: detail = res["detail"] - except: + except Exception: pass raise HTTPException( @@ -1859,7 +1860,6 @@ async def get_pipeline_valves( pipeline_id: str, user=Depends(get_admin_user), ): - models = await get_all_models() r = None try: url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx] @@ -1898,8 +1898,6 @@ async def get_pipeline_valves_spec( pipeline_id: str, user=Depends(get_admin_user), ): - models = await get_all_models() - r = None try: url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx] @@ -1922,7 +1920,7 @@ async def get_pipeline_valves_spec( res = r.json() if "detail" in res: detail = res["detail"] - except: + except Exception: pass raise HTTPException( @@ -1938,8 +1936,6 @@ async def update_pipeline_valves( form_data: dict, user=Depends(get_admin_user), ): - models = await get_all_models() - r = None try: url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx] @@ -1967,7 +1963,7 @@ async def update_pipeline_valves( res = r.json() if "detail" in res: detail = res["detail"] - except: + except Exception: pass raise HTTPException( @@ -2001,6 +1997,7 @@ async def get_app_config(): "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": { @@ -2068,7 +2065,7 @@ async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)): @app.get("/api/version") -async def get_app_config(): +async def get_app_version(): return { "version": VERSION, } @@ -2091,7 +2088,7 @@ async def get_app_latest_release_version(): latest_version = data["tag_name"] return {"current": VERSION, "latest": latest_version[1:]} - except aiohttp.ClientError as e: + except aiohttp.ClientError: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED, diff --git a/backend/requirements.txt b/backend/requirements.txt index c22712abf8..e8466a649a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -23,7 +23,7 @@ bcrypt==4.1.3 pymongo redis -boto3==1.34.110 +boto3==1.34.153 argon2-cffi==23.1.0 APScheduler==3.10.4 @@ -41,9 +41,9 @@ langchain-chroma==0.1.2 fake-useragent==1.5.1 chromadb==0.5.4 sentence-transformers==3.0.1 -pypdf==4.2.0 +pypdf==4.3.1 docx2txt==0.8 -python-pptx==0.6.23 +python-pptx==1.0.0 unstructured==0.15.0 Markdown==3.6 pypandoc==1.13 @@ -51,7 +51,7 @@ pandas==2.2.2 openpyxl==3.1.5 pyxlsb==1.0.10 xlrd==2.0.1 -validators==0.28.1 +validators==0.33.0 psutil opencv-python-headless==4.10.0.84 @@ -65,7 +65,7 @@ faster-whisper==1.0.2 PyJWT[crypto]==2.8.0 authlib==1.3.1 -black==24.4.2 +black==24.8.0 langfuse==2.39.2 youtube-transcript-api==0.6.2 pytube==15.0.0 diff --git a/backend/utils/misc.py b/backend/utils/misc.py index c4e2eda6f0..25dd4dd5b6 100644 --- a/backend/utils/misc.py +++ b/backend/utils/misc.py @@ -6,6 +6,8 @@ from typing import Optional, List, Tuple import uuid import time +from utils.task import prompt_template + def get_last_user_message_item(messages: List[dict]) -> Optional[dict]: for message in reversed(messages): @@ -97,18 +99,60 @@ def openai_chat_message_template(model: str): } -def openai_chat_chunk_message_template(model: str, message: str): +def openai_chat_chunk_message_template(model: str, message: str) -> dict: template = openai_chat_message_template(model) template["object"] = "chat.completion.chunk" template["choices"][0]["delta"] = {"content": message} return template -def openai_chat_completion_message_template(model: str, message: str): +def openai_chat_completion_message_template(model: str, message: str) -> dict: template = openai_chat_message_template(model) template["object"] = "chat.completion" template["choices"][0]["message"] = {"content": message, "role": "assistant"} template["choices"][0]["finish_reason"] = "stop" + return template + + +# inplace function: form_data is modified +def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict: + system = params.get("system", None) + if not system: + return form_data + + if user: + template_params = { + "user_name": user.name, + "user_location": user.info.get("location") if user.info else None, + } + else: + template_params = {} + system = prompt_template(system, **template_params) + form_data["messages"] = add_or_update_system_message( + system, form_data.get("messages", []) + ) + return form_data + + +# inplace function: form_data is modified +def apply_model_params_to_body(params: dict, form_data: dict) -> dict: + if not params: + return form_data + + mappings = { + "temperature": float, + "top_p": int, + "max_tokens": int, + "frequency_penalty": int, + "seed": lambda x: x, + "stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x], + } + + for key, cast_func in mappings.items(): + if (value := params.get(key)) is not None: + form_data[key] = cast_func(value) + + return form_data def get_gravatar_url(email): diff --git a/backend/utils/task.py b/backend/utils/task.py index 053a526a80..1b2276c9c5 100644 --- a/backend/utils/task.py +++ b/backend/utils/task.py @@ -6,7 +6,7 @@ from typing import Optional def prompt_template( - template: str, user_name: str = None, user_location: str = None + template: str, user_name: Optional[str] = None, user_location: Optional[str] = None ) -> str: # Get the current date current_date = datetime.now() @@ -83,7 +83,6 @@ def title_generation_template( def search_query_generation_template( template: str, prompt: str, user: Optional[dict] = None ) -> str: - def replacement_function(match): full_match = match.group(0) start_length = match.group(1) diff --git a/backend/utils/utils.py b/backend/utils/utils.py index fbc539af5c..288db1fb54 100644 --- a/backend/utils/utils.py +++ b/backend/utils/utils.py @@ -1,15 +1,12 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi import HTTPException, status, Depends, Request -from sqlalchemy.orm import Session from apps.webui.models.users import Users -from pydantic import BaseModel from typing import Union, Optional from constants import ERROR_MESSAGES from passlib.context import CryptContext from datetime import datetime, timedelta -import requests import jwt import uuid import logging @@ -54,7 +51,7 @@ def decode_token(token: str) -> Optional[dict]: try: decoded = jwt.decode(token, SESSION_SECRET, algorithms=[ALGORITHM]) return decoded - except Exception as e: + except Exception: return None @@ -71,7 +68,7 @@ def get_http_authorization_cred(auth_header: str): try: scheme, credentials = auth_header.split(" ") return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) - except: + except Exception: raise ValueError(ERROR_MESSAGES.INVALID_TOKEN) @@ -96,7 +93,7 @@ def get_current_user( # auth by jwt token data = decode_token(token) - if data != None and "id" in data: + if data is not None and "id" in data: user = Users.get_user_by_id(data["id"]) if user is None: raise HTTPException( diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 4a0c37e7c8..1cf539b3e7 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -11,10 +11,26 @@ Our primary goal is to ensure the protection and confidentiality of sensitive da ## Reporting a Vulnerability -If you discover a security issue within our system, please notify us immediately via a pull request or contact us on discord. +We appreciate the community's interest in identifying potential vulnerabilities. However, effective immediately, we will **not** accept low-effort vulnerability reports. To ensure that submissions are constructive and actionable, please adhere to the following guidelines: + +1. **No Vague Reports**: Submissions such as "I found a vulnerability" without any details will be treated as spam and will not be accepted. + +2. **In-Depth Understanding Required**: Reports must reflect a clear understanding of the codebase and provide specific details about the vulnerability, including the affected components and potential impacts. + +3. **Proof of Concept (PoC) is Mandatory**: Each submission must include a well-documented proof of concept (PoC) that demonstrates the vulnerability. If confidentiality is a concern, reporters are encouraged to create a private fork of the repository and share access with the maintainers. Reports lacking valid evidence will be disregarded. + +4. **Required Patch Submission**: Along with the PoC, reporters must provide a patch or actionable steps to remediate the identified vulnerability. This helps us evaluate and implement fixes rapidly. + +5. **Streamlined Merging Process**: When vulnerability reports meet the above criteria, we can consider them for immediate merging, similar to regular pull requests. Well-structured and thorough submissions will expedite the process of enhancing our security. + +Submissions that do not meet these criteria will be closed, and repeat offenders may face a ban from future submissions. We aim to create a respectful and constructive reporting environment, where high-quality submissions foster better security for everyone. ## Product Security -We regularly audit our internal processes and system's architecture for vulnerabilities using a combination of automated and manual testing techniques. +We regularly audit our internal processes and system architecture for vulnerabilities using a combination of automated and manual testing techniques. We are also planning to implement SAST and SCA scans in our project soon. -We are planning on implementing SAST and SCA scans in our project soon. +For immediate concerns or detailed reports that meet our guidelines, please create an issue in our [issue tracker](/open-webui/open-webui/issues) or contact us on [Discord](https://discord.gg/5rJgQTnV4s). + +--- + +_Last updated on **2024-08-06**._ diff --git a/package-lock.json b/package-lock.json index f3e8d2e38d..f903771f6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.3.11", + "version": "0.3.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.3.11", + "version": "0.3.12", "dependencies": { "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-python": "^6.1.6", diff --git a/package.json b/package.json index 6687cef755..bbe3ac397c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.3.11", + "version": "0.3.12", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", diff --git a/pyproject.toml b/pyproject.toml index eea77cfd2c..0b7af7f185 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,14 +31,14 @@ dependencies = [ "pymongo", "redis", - "boto3==1.34.110", + "boto3==1.34.153", "argon2-cffi==23.1.0", "APScheduler==3.10.4", "openai", "anthropic", - "google-generativeai==0.5.4", + "google-generativeai==0.7.2", "tiktoken", "langchain==0.2.11", @@ -48,9 +48,9 @@ dependencies = [ "fake-useragent==1.5.1", "chromadb==0.5.4", "sentence-transformers==3.0.1", - "pypdf==4.2.0", + "pypdf==4.3.1", "docx2txt==0.8", - "python-pptx==0.6.23", + "python-pptx==1.0.0", "unstructured==0.15.0", "Markdown==3.6", "pypandoc==1.13", @@ -58,7 +58,7 @@ dependencies = [ "openpyxl==3.1.5", "pyxlsb==1.0.10", "xlrd==2.0.1", - "validators==0.28.1", + "validators==0.33.0", "psutil", "opencv-python-headless==4.10.0.84", @@ -72,7 +72,7 @@ dependencies = [ "PyJWT[crypto]==2.8.0", "authlib==1.3.1", - "black==24.4.2", + "black==24.8.0", "langfuse==2.39.2", "youtube-transcript-api==0.6.2", "pytube==15.0.0", diff --git a/requirements-dev.lock b/requirements-dev.lock index 5380b66b25..da1f66fcce 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -57,13 +57,13 @@ beautifulsoup4==4.12.3 # via unstructured bidict==0.23.1 # via python-socketio -black==24.4.2 +black==24.8.0 # via open-webui blinker==1.8.2 # via flask -boto3==1.34.110 +boto3==1.34.153 # via open-webui -botocore==1.34.110 +botocore==1.34.155 # via boto3 # via s3transfer build==1.2.1 @@ -179,7 +179,7 @@ frozenlist==1.4.1 fsspec==2024.3.1 # via huggingface-hub # via torch -google-ai-generativelanguage==0.6.4 +google-ai-generativelanguage==0.6.6 # via google-generativeai google-api-core==2.19.0 # via google-ai-generativelanguage @@ -196,7 +196,7 @@ google-auth==2.29.0 # via kubernetes google-auth-httplib2==0.2.0 # via google-api-python-client -google-generativeai==0.5.4 +google-generativeai==0.7.2 # via open-webui googleapis-common-protos==1.63.0 # via google-api-core @@ -502,7 +502,7 @@ pypandoc==1.13 pyparsing==2.4.7 # via httplib2 # via oletools -pypdf==4.2.0 +pypdf==4.3.1 # via open-webui # via unstructured-client pypika==0.48.9 @@ -533,7 +533,7 @@ python-magic==0.4.27 python-multipart==0.0.9 # via fastapi # via open-webui -python-pptx==0.6.23 +python-pptx==1.0.0 # via open-webui python-socketio==5.11.3 # via open-webui @@ -684,6 +684,7 @@ typing-extensions==4.11.0 # via opentelemetry-sdk # via pydantic # via pydantic-core + # via python-pptx # via sqlalchemy # via torch # via typer @@ -718,7 +719,7 @@ uvicorn==0.22.0 # via open-webui uvloop==0.19.0 # via uvicorn -validators==0.28.1 +validators==0.33.0 # via open-webui watchfiles==0.21.0 # via uvicorn diff --git a/requirements.lock b/requirements.lock index 5380b66b25..da1f66fcce 100644 --- a/requirements.lock +++ b/requirements.lock @@ -57,13 +57,13 @@ beautifulsoup4==4.12.3 # via unstructured bidict==0.23.1 # via python-socketio -black==24.4.2 +black==24.8.0 # via open-webui blinker==1.8.2 # via flask -boto3==1.34.110 +boto3==1.34.153 # via open-webui -botocore==1.34.110 +botocore==1.34.155 # via boto3 # via s3transfer build==1.2.1 @@ -179,7 +179,7 @@ frozenlist==1.4.1 fsspec==2024.3.1 # via huggingface-hub # via torch -google-ai-generativelanguage==0.6.4 +google-ai-generativelanguage==0.6.6 # via google-generativeai google-api-core==2.19.0 # via google-ai-generativelanguage @@ -196,7 +196,7 @@ google-auth==2.29.0 # via kubernetes google-auth-httplib2==0.2.0 # via google-api-python-client -google-generativeai==0.5.4 +google-generativeai==0.7.2 # via open-webui googleapis-common-protos==1.63.0 # via google-api-core @@ -502,7 +502,7 @@ pypandoc==1.13 pyparsing==2.4.7 # via httplib2 # via oletools -pypdf==4.2.0 +pypdf==4.3.1 # via open-webui # via unstructured-client pypika==0.48.9 @@ -533,7 +533,7 @@ python-magic==0.4.27 python-multipart==0.0.9 # via fastapi # via open-webui -python-pptx==0.6.23 +python-pptx==1.0.0 # via open-webui python-socketio==5.11.3 # via open-webui @@ -684,6 +684,7 @@ typing-extensions==4.11.0 # via opentelemetry-sdk # via pydantic # via pydantic-core + # via python-pptx # via sqlalchemy # via torch # via typer @@ -718,7 +719,7 @@ uvicorn==0.22.0 # via open-webui uvloop==0.19.0 # via uvicorn -validators==0.28.1 +validators==0.33.0 # via open-webui watchfiles==0.21.0 # via uvicorn diff --git a/src/app.css b/src/app.css index 69e107be98..4345bb3777 100644 --- a/src/app.css +++ b/src/app.css @@ -158,3 +158,12 @@ input[type='number'] { .password { -webkit-text-security: disc; } + +.codespan { + color: #eb5757; + border-width: 0px; + padding: 3px 8px; + font-size: 0.8em; + font-weight: 600; + @apply rounded-md dark:bg-gray-800 bg-gray-100 mx-0.5; +} diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index b046f1b10d..8f4f81aea4 100644 --- a/src/lib/apis/chats/index.ts +++ b/src/lib/apis/chats/index.ts @@ -32,10 +32,15 @@ export const createNewChat = async (token: string, chat: object) => { return res; }; -export const getChatList = async (token: string = '') => { +export const getChatList = async (token: string = '', page: number | null = null) => { let error = null; + const searchParams = new URLSearchParams(); - const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, { + if (page !== null) { + searchParams.append('page', `${page}`); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/?${searchParams.toString()}`, { method: 'GET', headers: { Accept: 'application/json', diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 78a22010e5..2c42c20463 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -25,7 +25,8 @@ user, socket, showCallOverlay, - tools + tools, + currentChatPage } from '$lib/stores'; import { convertMessagesToHistory, @@ -421,7 +422,9 @@ params: params, files: chatFiles }); - await chats.set(await getChatList(localStorage.token)); + + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } } }; @@ -467,7 +470,9 @@ params: params, files: chatFiles }); - await chats.set(await getChatList(localStorage.token)); + + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } } }; @@ -627,7 +632,9 @@ tags: [], timestamp: Date.now() }); - await chats.set(await getChatList(localStorage.token)); + + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); await chatId.set(chat.id); } else { await chatId.set('local'); @@ -703,7 +710,9 @@ }) ); - await chats.set(await getChatList(localStorage.token)); + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); + return _responses; }; @@ -803,8 +812,8 @@ ...(params ?? $settings.params ?? {}), stop: params?.stop ?? $settings?.params?.stop ?? undefined - ? (params?.stop ?? $settings.params.stop).map((str) => - decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"')) + ? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map( + (str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"')) ) : undefined, num_predict: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined, @@ -949,7 +958,9 @@ params: params, files: chatFiles }); - await chats.set(await getChatList(localStorage.token)); + + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } } } else { @@ -1103,8 +1114,8 @@ seed: params?.seed ?? $settings?.params?.seed ?? undefined, stop: params?.stop ?? $settings?.params?.stop ?? undefined - ? (params?.stop ?? $settings.params.stop).map((str) => - decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"')) + ? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map( + (str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"')) ) : undefined, temperature: params?.temperature ?? $settings?.params?.temperature ?? undefined, @@ -1128,7 +1139,6 @@ if (res && res.ok && res.body) { const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks); - let lastUsage = null; for await (const update of textStream) { const { value, done, citations, error, usage } = update; @@ -1154,7 +1164,7 @@ } if (usage) { - lastUsage = usage; + responseMessage.info = { ...usage, openai: true }; } if (citations) { @@ -1208,10 +1218,6 @@ document.getElementById(`speak-button-${responseMessage.id}`)?.click(); } - if (lastUsage) { - responseMessage.info = { ...lastUsage, openai: true }; - } - if ($chatId == _chatId) { if ($settings.saveChatHistory ?? true) { chat = await updateChatById(localStorage.token, _chatId, { @@ -1221,7 +1227,9 @@ params: params, files: chatFiles }); - await chats.set(await getChatList(localStorage.token)); + + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } } } else { @@ -1386,7 +1394,9 @@ if ($settings.saveChatHistory ?? true) { chat = await updateChatById(localStorage.token, _chatId, { title: _title }); - await chats.set(await getChatList(localStorage.token)); + + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } }; diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 2e1e4b98ea..bfd5b3001d 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -384,7 +384,7 @@ {#if atSelectedModel !== undefined}
{#each filteredModels as model, modelIdx} {/each}
diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index e46e931432..9e3c147b15 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -1,6 +1,6 @@ -
+
diff --git a/src/lib/components/chat/Messages/CompareMessages.svelte b/src/lib/components/chat/Messages/CompareMessages.svelte index 27fefb6cb4..ffa74e303f 100644 --- a/src/lib/components/chat/Messages/CompareMessages.svelte +++ b/src/lib/components/chat/Messages/CompareMessages.svelte @@ -118,47 +118,47 @@ currentMessageId = message.id; let messageId = message.id; console.log(messageId); - // let messageChildrenIds = history.messages[messageId].childrenIds; while (messageChildrenIds.length !== 0) { messageId = messageChildrenIds.at(-1); messageChildrenIds = history.messages[messageId].childrenIds; } - history.currentId = messageId; dispatch('change'); } }} > - m.id)} - isLastMessage={true} - {updateChatMessages} - {confirmEditResponseMessage} - showPreviousMessage={() => showPreviousMessage(model)} - showNextMessage={() => showNextMessage(model)} - {readOnly} - {rateMessage} - {copyToClipboard} - {continueGeneration} - regenerateResponse={async (message) => { - regenerateResponse(message); - await tick(); - groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1; - }} - on:save={async (e) => { - console.log('save', e); + {#key history.currentId} + m.id)} + isLastMessage={true} + {updateChatMessages} + {confirmEditResponseMessage} + showPreviousMessage={() => showPreviousMessage(model)} + showNextMessage={() => showNextMessage(model)} + {readOnly} + {rateMessage} + {copyToClipboard} + {continueGeneration} + regenerateResponse={async (message) => { + regenerateResponse(message); + await tick(); + groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1; + }} + on:save={async (e) => { + console.log('save', e); - const message = e.detail; - history.messages[message.id] = message; - await updateChatById(localStorage.token, chatId, { - messages: messages, - history: history - }); - }} - /> + const message = e.detail; + history.messages[message.id] = message; + await updateChatById(localStorage.token, chatId, { + messages: messages, + history: history + }); + }} + /> + {/key}
{/if} {/each} diff --git a/src/lib/components/chat/Messages/HTMLRenderer.svelte b/src/lib/components/chat/Messages/HTMLRenderer.svelte new file mode 100644 index 0000000000..53fad80842 --- /dev/null +++ b/src/lib/components/chat/Messages/HTMLRenderer.svelte @@ -0,0 +1,30 @@ + + +{#each parsedHTML as part} + {@const match = rules.find((rule) => rule.regex.test(part))} + {#if match} + + {@html part} + + {:else} + {@html part} + {/if} +{/each} diff --git a/src/lib/components/chat/Messages/MarkdownInlineTokens.svelte b/src/lib/components/chat/Messages/MarkdownInlineTokens.svelte new file mode 100644 index 0000000000..19d22de37c --- /dev/null +++ b/src/lib/components/chat/Messages/MarkdownInlineTokens.svelte @@ -0,0 +1,38 @@ + + +{#each tokens as token} + {#if token.type === 'escape'} + {unescapeHtml(token.text)} + {:else if token.type === 'html'} + {@html token.text} + {:else if token.type === 'link'} + {token.text} + {:else if token.type === 'image'} + {token.text} + {:else if token.type === 'strong'} + + + + {:else if token.type === 'em'} + + + + {:else if token.type === 'codespan'} + {unescapeHtml(token.text.replaceAll('&', '&'))} + {:else if token.type === 'br'} +
+ {:else if token.type === 'del'} + + + + {:else if token.type === 'text'} + {unescapeHtml(token.text)} + {/if} +{/each} diff --git a/src/lib/components/chat/Messages/MarkdownTokens.svelte b/src/lib/components/chat/Messages/MarkdownTokens.svelte new file mode 100644 index 0000000000..33b8984854 --- /dev/null +++ b/src/lib/components/chat/Messages/MarkdownTokens.svelte @@ -0,0 +1,137 @@ + + +
+ {#each tokens as token, tokenIdx (`${id}-${tokenIdx}`)} + {#if token.type === 'code'} + {#if token.lang === 'mermaid'} +
{revertSanitizedResponseContent(token.text)}
+ {:else} + + {/if} + {:else} + {@html marked.parse(token.raw, { + ...defaults, + gfm: true, + breaks: true, + renderer + })} + {/if} + {/each} +
diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index 011d772549..fdc846205b 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -38,6 +38,7 @@ import Spinner from '$lib/components/common/Spinner.svelte'; import WebSearchResults from './ResponseMessage/WebSearchResults.svelte'; import Sparkles from '$lib/components/icons/Sparkles.svelte'; + import MarkdownTokens from './MarkdownTokens.svelte'; export let message; export let siblings; @@ -55,7 +56,6 @@ export let copyToClipboard: Function; export let continueGeneration: Function; export let regenerateResponse: Function; - export let chatActionHandler: Function; let model = null; $: model = $models.find((m) => m.id === message.model); @@ -77,28 +77,16 @@ let selectedCitation = null; - $: tokens = marked.lexer( - replaceTokens(sanitizeResponseContent(message?.content), model?.name, $user?.name) - ); + let tokens; - const renderer = new marked.Renderer(); - - // For code blocks with simple backticks - renderer.codespan = (code) => { - return `${code.replaceAll('&', '&')}`; - }; - - // Open all links in a new tab/window (from https://github.com/markedjs/marked/issues/655#issuecomment-383226346) - const origLinkRenderer = renderer.link; - renderer.link = (href, title, text) => { - const html = origLinkRenderer.call(renderer, href, title, text); - return html.replace(/^ { + if (message?.content) { + tokens = marked.lexer( + replaceTokens(sanitizeResponseContent(message?.content), model?.name, $user?.name) + ); + // console.log(message?.content, tokens); + } + })(); $: if (message) { renderStyling(); @@ -418,294 +406,548 @@ {/if} - {#if (message?.files ?? []).filter((f) => f.type === 'image').length > 0} -
- {#each message.files as file} -
- {#if file.type === 'image'} - - {/if} -
- {/each} -
- {/if} +
+ {#if (message?.files ?? []).filter((f) => f.type === 'image').length > 0} +
+ {#each message.files as file} +
+ {#if file.type === 'image'} + + {/if} +
+ {/each} +
+ {/if} -
-
- {#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0} - {@const status = ( - message?.statusHistory ?? [...(message?.status ? [message?.status] : [])] - ).at(-1)} -
- {#if status.done === false} -
- -
- {/if} +
+
+ {#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0} + {@const status = ( + message?.statusHistory ?? [...(message?.status ? [message?.status] : [])] + ).at(-1)} +
+ {#if status.done === false} +
+ +
+ {/if} - {#if status?.action === 'web_search' && status?.urls} - + {#if status?.action === 'web_search' && status?.urls} + +
+
+ {status?.description} +
+
+
+ {:else}
-
+
{status?.description}
- - {:else} -
-
- {status?.description} -
-
- {/if} -
- {/if} - - {#if edit === true} -
-