Procházet zdrojové kódy

Merge pull request #4322 from open-webui/dev

0.3.12
Timothy Jaeryang Baek před 1 rokem
rodič
revize
c869652ef4
86 změnil soubory, kde provedl 2457 přidání a 1362 odebrání
  1. 24 21
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 4 4
      .github/workflows/integration-test.yml
  3. 22 0
      CHANGELOG.md
  4. 72 143
      backend/apps/openai/main.py
  5. 15 12
      backend/apps/socket/main.py
  6. 2 43
      backend/apps/webui/main.py
  7. 5 4
      backend/apps/webui/models/chats.py
  8. 16 5
      backend/apps/webui/routers/chats.py
  9. 13 18
      backend/apps/webui/routers/tools.py
  10. 5 1
      backend/config.py
  11. 12 15
      backend/main.py
  12. 5 5
      backend/requirements.txt
  13. 46 2
      backend/utils/misc.py
  14. 1 2
      backend/utils/task.py
  15. 3 6
      backend/utils/utils.py
  16. 19 3
      docs/SECURITY.md
  17. 2 2
      package-lock.json
  18. 1 1
      package.json
  19. 6 6
      pyproject.toml
  20. 9 8
      requirements-dev.lock
  21. 9 8
      requirements.lock
  22. 9 0
      src/app.css
  23. 7 2
      src/lib/apis/chats/index.ts
  24. 28 18
      src/lib/components/chat/Chat.svelte
  25. 1 1
      src/lib/components/chat/MessageInput.svelte
  26. 9 4
      src/lib/components/chat/MessageInput/Models.svelte
  27. 17 12
      src/lib/components/chat/Messages.svelte
  28. 1 1
      src/lib/components/chat/Messages/CodeBlock.svelte
  29. 30 30
      src/lib/components/chat/Messages/CompareMessages.svelte
  30. 30 0
      src/lib/components/chat/Messages/HTMLRenderer.svelte
  31. 38 0
      src/lib/components/chat/Messages/MarkdownInlineTokens.svelte
  32. 137 0
      src/lib/components/chat/Messages/MarkdownTokens.svelte
  33. 563 598
      src/lib/components/chat/Messages/ResponseMessage.svelte
  34. 12 4
      src/lib/components/chat/Settings/Chats.svelte
  35. 29 0
      src/lib/components/chat/Settings/Interface.svelte
  36. 11 6
      src/lib/components/chat/Tags.svelte
  37. 6 4
      src/lib/components/common/Image.svelte
  38. 14 11
      src/lib/components/common/ImagePreview.svelte
  39. 30 0
      src/lib/components/common/Loader.svelte
  40. 64 7
      src/lib/components/layout/Sidebar.svelte
  41. 10 4
      src/lib/components/layout/Sidebar/ChatItem.svelte
  42. 1 0
      src/lib/i18n/locales/ar-BH/translation.json
  43. 1 0
      src/lib/i18n/locales/bg-BG/translation.json
  44. 1 0
      src/lib/i18n/locales/bn-BD/translation.json
  45. 9 8
      src/lib/i18n/locales/ca-ES/translation.json
  46. 1 0
      src/lib/i18n/locales/ceb-PH/translation.json
  47. 1 0
      src/lib/i18n/locales/de-DE/translation.json
  48. 1 0
      src/lib/i18n/locales/dg-DG/translation.json
  49. 1 0
      src/lib/i18n/locales/en-GB/translation.json
  50. 1 0
      src/lib/i18n/locales/en-US/translation.json
  51. 1 0
      src/lib/i18n/locales/es-ES/translation.json
  52. 1 0
      src/lib/i18n/locales/fa-IR/translation.json
  53. 1 0
      src/lib/i18n/locales/fi-FI/translation.json
  54. 1 0
      src/lib/i18n/locales/fr-CA/translation.json
  55. 1 0
      src/lib/i18n/locales/fr-FR/translation.json
  56. 1 0
      src/lib/i18n/locales/he-IL/translation.json
  57. 1 0
      src/lib/i18n/locales/hi-IN/translation.json
  58. 1 0
      src/lib/i18n/locales/hr-HR/translation.json
  59. 1 0
      src/lib/i18n/locales/id-ID/translation.json
  60. 1 0
      src/lib/i18n/locales/it-IT/translation.json
  61. 1 0
      src/lib/i18n/locales/ja-JP/translation.json
  62. 1 0
      src/lib/i18n/locales/ka-GE/translation.json
  63. 1 0
      src/lib/i18n/locales/ko-KR/translation.json
  64. 4 0
      src/lib/i18n/locales/languages.json
  65. 1 0
      src/lib/i18n/locales/lt-LT/translation.json
  66. 715 0
      src/lib/i18n/locales/ms-MY/translation.json
  67. 1 0
      src/lib/i18n/locales/nb-NO/translation.json
  68. 1 0
      src/lib/i18n/locales/nl-NL/translation.json
  69. 1 0
      src/lib/i18n/locales/pa-IN/translation.json
  70. 1 0
      src/lib/i18n/locales/pl-PL/translation.json
  71. 1 0
      src/lib/i18n/locales/pt-BR/translation.json
  72. 1 0
      src/lib/i18n/locales/pt-PT/translation.json
  73. 1 0
      src/lib/i18n/locales/ro-RO/translation.json
  74. 1 0
      src/lib/i18n/locales/ru-RU/translation.json
  75. 1 0
      src/lib/i18n/locales/sr-RS/translation.json
  76. 1 0
      src/lib/i18n/locales/sv-SE/translation.json
  77. 1 0
      src/lib/i18n/locales/th-TH/translation.json
  78. 1 0
      src/lib/i18n/locales/tk-TW/translation.json
  79. 1 0
      src/lib/i18n/locales/tr-TR/translation.json
  80. 1 0
      src/lib/i18n/locales/uk-UA/translation.json
  81. 1 0
      src/lib/i18n/locales/vi-VN/translation.json
  82. 5 4
      src/lib/i18n/locales/zh-CN/translation.json
  83. 340 338
      src/lib/i18n/locales/zh-TW/translation.json
  84. 4 0
      src/lib/stores/index.ts
  85. 5 0
      src/lib/utils/index.ts
  86. 1 1
      src/routes/(app)/admin/+page.svelte

+ 24 - 21
.github/ISSUE_TEMPLATE/bug_report.md

@@ -8,36 +8,43 @@ assignees: ''
 
 # Bug Report
 
-## Description
-
-**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.]
+## Installation Method
 
-**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.]

+ 4 - 4
.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

+ 22 - 0
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

+ 72 - 143
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):
-    log.info("get_all_models()")
+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
 
-    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)
-                    )
-                ]
 
-        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}")
-
-        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,
-                    )
-                )
-            )
-        }
+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: ...
 
-        log.debug(f"models: {models}")
-        app.state.MODELS = {model["id"]: model for model in models["data"]}
+
+@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": []}
+
+    responses = await get_all_models_raw()
+    if raw:
+        return responses
+
+    def extract_data(response):
+        if response and "data" in response:
+            return response["data"]
+        if isinstance(response, list):
+            return response
+        return None
+
+    models = {"data": merge_models_lists(map(extract_data, responses))}
+
+    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:

+ 15 - 12
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")

+ 2 - 43
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)

+ 5 - 4
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 [

+ 16 - 5
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:

+ 13 - 18
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(

+ 5 - 1
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(

+ 12 - 15
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,

+ 5 - 5
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

+ 46 - 2
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):

+ 1 - 2
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)

+ 3 - 6
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(

+ 19 - 3
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.
+
+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).
+
+---
 
-We are planning on implementing SAST and SCA scans in our project soon.
+_Last updated on **2024-08-06**._

+ 2 - 2
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",

+ 1 - 1
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",

+ 6 - 6
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",

+ 9 - 8
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

+ 9 - 8
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

+ 9 - 0
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;
+}

+ 7 - 2
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',

+ 28 - 18
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));
 		}
 	};
 

+ 1 - 1
src/lib/components/chat/MessageInput.svelte

@@ -384,7 +384,7 @@
 
 				{#if atSelectedModel !== undefined}
 					<div
-						class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900"
+						class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900 z-50"
 					>
 						<div class="flex items-center gap-2 text-sm dark:text-gray-500">
 							<img

+ 9 - 4
src/lib/components/chat/MessageInput/Models.svelte

@@ -147,8 +147,8 @@
 					<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
 						{#each filteredModels as model, modelIdx}
 							<button
-								class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
-									? '  bg-gray-50 dark:bg-gray-850  selected-command-option-button'
+								class="px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
+									? 'bg-gray-50 dark:bg-gray-850 selected-command-option-button'
 									: ''}"
 								type="button"
 								on:click={() => {
@@ -159,13 +159,18 @@
 								}}
 								on:focus={() => {}}
 							>
-								<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
+								<div class="flex font-medium text-black dark:text-gray-100 line-clamp-1">
+									<img
+										src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
+										alt={model?.name ?? model.id}
+										class="rounded-full size-6 items-center mr-2"
+									/>
 									{model.name}
 								</div>
 
 								<!-- <div class=" text-xs text-gray-600 line-clamp-1">
 								{doc.title}
-							</div> -->
+								</div> -->
 							</button>
 						{/each}
 					</div>

+ 17 - 12
src/lib/components/chat/Messages.svelte

@@ -1,6 +1,6 @@
 <script lang="ts">
 	import { v4 as uuidv4 } from 'uuid';
-	import { chats, config, settings, user as _user, mobile } from '$lib/stores';
+	import { chats, config, settings, user as _user, mobile, currentChatPage } from '$lib/stores';
 	import { tick, getContext, onMount } from 'svelte';
 
 	import { toast } from 'svelte-sonner';
@@ -90,7 +90,8 @@
 			history: history
 		});
 
-		await chats.set(await getChatList(localStorage.token));
+		currentChatPage.set(1);
+		await chats.set(await getChatList(localStorage.token, $currentChatPage));
 	};
 
 	const confirmEditResponseMessage = async (messageId, content) => {
@@ -146,12 +147,14 @@
 
 		await tick();
 
-		const element = document.getElementById('messages-container');
-		autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
+		if ($settings?.scrollOnBranchChange ?? true) {
+			const element = document.getElementById('messages-container');
+			autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
 
-		setTimeout(() => {
-			scrollToBottom();
-		}, 100);
+			setTimeout(() => {
+				scrollToBottom();
+			}, 100);
+		}
 	};
 
 	const showNextMessage = async (message) => {
@@ -195,12 +198,14 @@
 
 		await tick();
 
-		const element = document.getElementById('messages-container');
-		autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
+		if ($settings?.scrollOnBranchChange ?? true) {
+			const element = document.getElementById('messages-container');
+			autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
 
-		setTimeout(() => {
-			scrollToBottom();
-		}, 100);
+			setTimeout(() => {
+				scrollToBottom();
+			}, 100);
+		}
 	};
 
 	const deleteMessageHandler = async (messageId) => {

+ 1 - 1
src/lib/components/chat/Messages/CodeBlock.svelte

@@ -218,7 +218,7 @@ __builtins__.input = input`);
 	}
 </script>
 
-<div class="mb-4" dir="ltr">
+<div class="my-2" dir="ltr">
 	<div
 		class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
 	>

+ 30 - 30
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');
 							}
 						}}
 					>
-						<ResponseMessage
-							message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
-							siblings={groupedMessages[model].messages.map((m) => 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
-								});
-							}}
-						/>
+						{#key history.currentId}
+							<ResponseMessage
+								message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
+								siblings={groupedMessages[model].messages.map((m) => 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
+									});
+								}}
+							/>
+						{/key}
 					</div>
 				{/if}
 			{/each}

+ 30 - 0
src/lib/components/chat/Messages/HTMLRenderer.svelte

@@ -0,0 +1,30 @@
+<script lang="ts">
+	import Image from '$lib/components/common/Image.svelte';
+	import CodeBlock from './CodeBlock.svelte';
+
+	/* The html content of the tag */
+	export let html; //: string;
+	let parsedHTML = [html];
+
+	export let images;
+	export let codes;
+
+	//  all images are in {{IMAGE_0}}, {{IMAGE_1}}....  format
+	// all codes are in {{CODE_0}}, {{CODE_1}}....  format
+
+	const rules = [];
+	rules.forEach((rule) => {
+		parsedHTML = parsedHTML.map((substr) => substr.split(rule.regex)).flat();
+	});
+</script>
+
+{#each parsedHTML as part}
+	{@const match = rules.find((rule) => rule.regex.test(part))}
+	{#if match}
+		<svelte:component this={match.component} {...match.props}>
+			{@html part}
+		</svelte:component>
+	{:else}
+		{@html part}
+	{/if}
+{/each}

+ 38 - 0
src/lib/components/chat/Messages/MarkdownInlineTokens.svelte

@@ -0,0 +1,38 @@
+<script lang="ts">
+	import type { Token } from 'marked';
+	import { unescapeHtml } from '$lib/utils';
+	import Image from '$lib/components/common/Image.svelte';
+
+	export let id: string;
+	export let tokens: Token[];
+</script>
+
+{#each tokens as token}
+	{#if token.type === 'escape'}
+		{unescapeHtml(token.text)}
+	{:else if token.type === 'html'}
+		{@html token.text}
+	{:else if token.type === 'link'}
+		<a href={token.href} target="_blank" rel="nofollow" title={token.title}>{token.text}</a>
+	{:else if token.type === 'image'}
+		<Image src={token.href} alt={token.text} />
+	{:else if token.type === 'strong'}
+		<strong>
+			<svelte:self id={`${id}-strong`} tokens={token.tokens} />
+		</strong>
+	{:else if token.type === 'em'}
+		<em>
+			<svelte:self id={`${id}-em`} tokens={token.tokens} />
+		</em>
+	{:else if token.type === 'codespan'}
+		<code class="codespan">{unescapeHtml(token.text.replaceAll('&amp;', '&'))}</code>
+	{:else if token.type === 'br'}
+		<br />
+	{:else if token.type === 'del'}
+		<del>
+			<svelte:self id={`${id}-del`} tokens={token.tokens} />
+		</del>
+	{:else if token.type === 'text'}
+		{unescapeHtml(token.text)}
+	{/if}
+{/each}

+ 137 - 0
src/lib/components/chat/Messages/MarkdownTokens.svelte

@@ -0,0 +1,137 @@
+<script lang="ts">
+	import { marked } from 'marked';
+	import type { Token } from 'marked';
+	import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
+
+	import { onMount } from 'svelte';
+
+	import Image from '$lib/components/common/Image.svelte';
+	import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
+
+	import MarkdownInlineTokens from '$lib/components/chat/Messages/MarkdownInlineTokens.svelte';
+
+	export let id: string;
+	export let tokens: Token[];
+	export let top = true;
+
+	let containerElement;
+
+	const headerComponent = (depth: number) => {
+		return 'h' + depth;
+	};
+
+	const renderer = new marked.Renderer();
+	// For code blocks with simple backticks
+	renderer.codespan = (code) => {
+		return `<code class="codespan">${code.replaceAll('&amp;', '&')}</code>`;
+	};
+
+	let codes = [];
+	renderer.code = (code, lang) => {
+		codes.push({
+			code: code,
+			lang: lang
+		});
+		codes = codes;
+		const codeId = `${id}-${codes.length}`;
+
+		const interval = setInterval(() => {
+			const codeElement = document.getElementById(`code-${codeId}`);
+			if (codeElement) {
+				clearInterval(interval);
+				// If the code is already loaded, don't load it again
+				if (codeElement.innerHTML) {
+					return;
+				}
+
+				new CodeBlock({
+					target: codeElement,
+					props: {
+						id: `${id}-${codes.length}`,
+						lang: lang,
+						code: revertSanitizedResponseContent(code)
+					},
+					hydrate: true,
+					$$inline: true
+				});
+			}
+		}, 10);
+
+		return `<div id="code-${id}-${codes.length}"></div>`;
+	};
+
+	let images = [];
+	renderer.image = (href, title, text) => {
+		images.push({
+			href: href,
+			title: title,
+			text: text
+		});
+		images = images;
+
+		const imageId = `${id}-${images.length}`;
+		const interval = setInterval(() => {
+			const imageElement = document.getElementById(`image-${imageId}`);
+			if (imageElement) {
+				clearInterval(interval);
+
+				// If the image is already loaded, don't load it again
+				if (imageElement.innerHTML) {
+					return;
+				}
+
+				console.log('image', href, text);
+				new Image({
+					target: imageElement,
+					props: {
+						src: href,
+						alt: text
+					},
+					$$inline: true
+				});
+			}
+		}, 10);
+
+		return `<div id="image-${id}-${images.length}"></div>`;
+	};
+
+	// 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(/^<a /, '<a target="_blank" rel="nofollow" ');
+	};
+
+	const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
+		extensions: any;
+	};
+
+	$: if (tokens) {
+		images = [];
+		codes = [];
+	}
+</script>
+
+<div bind:this={containerElement} class="flex flex-col">
+	{#each tokens as token, tokenIdx (`${id}-${tokenIdx}`)}
+		{#if token.type === 'code'}
+			{#if token.lang === 'mermaid'}
+				<pre class="mermaid">{revertSanitizedResponseContent(token.text)}</pre>
+			{:else}
+				<CodeBlock
+					id={`${id}-${tokenIdx}`}
+					lang={token?.lang ?? ''}
+					code={revertSanitizedResponseContent(token?.text ?? '')}
+				/>
+			{/if}
+		{:else}
+			{@html marked.parse(token.raw, {
+				...defaults,
+				gfm: true,
+				breaks: true,
+				renderer
+			})}
+		{/if}
+	{/each}
+</div>

+ 563 - 598
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>${code.replaceAll('&amp;', '&')}</code>`;
-	};
-
-	// 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(/^<a /, '<a target="_blank" rel="nofollow" ');
-	};
-
-	const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
-		// eslint-disable-next-line @typescript-eslint/no-explicit-any
-		extensions: any;
-	};
+	$: (async () => {
+		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,581 @@
 				{/if}
 			</Name>
 
-			{#if (message?.files ?? []).filter((f) => f.type === 'image').length > 0}
-				<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
-					{#each message.files as file}
-						<div>
-							{#if file.type === 'image'}
-								<Image src={file.url} />
-							{/if}
-						</div>
-					{/each}
-				</div>
-			{/if}
-
-			<div
-				class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-headings:-mb-4 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
-			>
-				<div>
-					{#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0}
-						{@const status = (
-							message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]
-						).at(-1)}
-						<div class="flex items-center gap-2 pt-0.5 pb-1">
-							{#if status.done === false}
-								<div class="">
-									<Spinner className="size-4" />
-								</div>
-							{/if}
+			<div>
+				{#if (message?.files ?? []).filter((f) => f.type === 'image').length > 0}
+					<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
+						{#each message.files as file}
+							<div>
+								{#if file.type === 'image'}
+									<Image src={file.url} />
+								{/if}
+							</div>
+						{/each}
+					</div>
+				{/if}
+
+				<div
+					class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-2 prose-ol:-my-2 prose-li:-my-3 whitespace-pre-line"
+				>
+					<div>
+						{#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0}
+							{@const status = (
+								message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]
+							).at(-1)}
+							<div class="flex items-center gap-2 pt-0.5 pb-1">
+								{#if status.done === false}
+									<div class="">
+										<Spinner className="size-4" />
+									</div>
+								{/if}
 
-							{#if status?.action === 'web_search' && status?.urls}
-								<WebSearchResults {status}>
+								{#if status?.action === 'web_search' && status?.urls}
+									<WebSearchResults {status}>
+										<div class="flex flex-col justify-center -space-y-0.5">
+											<div class="text-base line-clamp-1 text-wrap">
+												{status?.description}
+											</div>
+										</div>
+									</WebSearchResults>
+								{:else}
 									<div class="flex flex-col justify-center -space-y-0.5">
-										<div class="text-base line-clamp-1 text-wrap">
+										<div class=" text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap">
 											{status?.description}
 										</div>
 									</div>
-								</WebSearchResults>
-							{:else}
-								<div class="flex flex-col justify-center -space-y-0.5">
-									<div class=" text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap">
-										{status?.description}
-									</div>
-								</div>
-							{/if}
-						</div>
-					{/if}
-
-					{#if edit === true}
-						<div class="w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 my-2">
-							<textarea
-								id="message-edit-{message.id}"
-								bind:this={editTextAreaElement}
-								class=" bg-transparent outline-none w-full resize-none"
-								bind:value={editedContent}
-								on:input={(e) => {
-									e.target.style.height = '';
-									e.target.style.height = `${e.target.scrollHeight}px`;
-								}}
-								on:keydown={(e) => {
-									if (e.key === 'Escape') {
-										document.getElementById('close-edit-message-button')?.click();
-									}
-
-									const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
-									const isEnterPressed = e.key === 'Enter';
-
-									if (isCmdOrCtrlPressed && isEnterPressed) {
-										document.getElementById('save-edit-message-button')?.click();
-									}
-								}}
-							/>
-
-							<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
-								<button
-									id="close-edit-message-button"
-									class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
-									on:click={() => {
-										cancelEditMessage();
+								{/if}
+							</div>
+						{/if}
+
+						{#if edit === true}
+							<div class="w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 my-2">
+								<textarea
+									id="message-edit-{message.id}"
+									bind:this={editTextAreaElement}
+									class=" bg-transparent outline-none w-full resize-none"
+									bind:value={editedContent}
+									on:input={(e) => {
+										e.target.style.height = '';
+										e.target.style.height = `${e.target.scrollHeight}px`;
 									}}
-								>
-									{$i18n.t('Cancel')}
-								</button>
-
-								<button
-									id="save-edit-message-button"
-									class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
-									on:click={() => {
-										editMessageConfirmHandler();
+									on:keydown={(e) => {
+										if (e.key === 'Escape') {
+											document.getElementById('close-edit-message-button')?.click();
+										}
+
+										const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
+										const isEnterPressed = e.key === 'Enter';
+
+										if (isCmdOrCtrlPressed && isEnterPressed) {
+											document.getElementById('save-edit-message-button')?.click();
+										}
 									}}
-								>
-									{$i18n.t('Save')}
-								</button>
-							</div>
-						</div>
-					{:else}
-						<div class="w-full">
-							{#if message.content === '' && !message.error}
-								<Skeleton />
-							{:else if message.content && message.error !== true}
-								<!-- always show message contents even if there's an error -->
-								<!-- unless message.error === true which is legacy error handling, where the error message is stored in message.content -->
-								{#each tokens as token, tokenIdx}
-									{#if token.type === 'code'}
-										{#if token.lang === 'mermaid'}
-											<pre class="mermaid">{revertSanitizedResponseContent(token.text)}</pre>
-										{:else}
-											<CodeBlock
-												id={`${message.id}-${tokenIdx}`}
-												lang={token?.lang ?? ''}
-												code={revertSanitizedResponseContent(token?.text ?? '')}
-											/>
-										{/if}
-									{:else}
-										{@html marked.parse(token.raw, {
-											...defaults,
-											gfm: true,
-											breaks: true,
-											renderer
-										})}
-									{/if}
-								{/each}
-							{/if}
+								/>
 
-							{#if message.error}
-								<div
-									class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
-								>
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										fill="none"
-										viewBox="0 0 24 24"
-										stroke-width="1.5"
-										stroke="currentColor"
-										class="w-5 h-5 self-center"
+								<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
+									<button
+										id="close-edit-message-button"
+										class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
+										on:click={() => {
+											cancelEditMessage();
+										}}
 									>
-										<path
-											stroke-linecap="round"
-											stroke-linejoin="round"
-											d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
-										/>
-									</svg>
-
-									<div class=" self-center">
-										{message?.error?.content ?? message.content}
-									</div>
+										{$i18n.t('Cancel')}
+									</button>
+
+									<button
+										id="save-edit-message-button"
+										class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
+										on:click={() => {
+											editMessageConfirmHandler();
+										}}
+									>
+										{$i18n.t('Save')}
+									</button>
 								</div>
-							{/if}
-
-							{#if message.citations}
-								<div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
-									{#each message.citations.reduce((acc, citation) => {
-										citation.document.forEach((document, index) => {
-											const metadata = citation.metadata?.[index];
-											const id = metadata?.source ?? 'N/A';
-											let source = citation?.source;
+							</div>
+						{:else}
+							<div class="w-full flex flex-col">
+								{#if message.content === '' && !message.error}
+									<Skeleton />
+								{:else if message.content && message.error !== true}
+									<!-- always show message contents even if there's an error -->
+									<!-- unless message.error === true which is legacy error handling, where the error message is stored in message.content -->
+									{#key message.id}
+										<MarkdownTokens id={message.id} {tokens} />
+									{/key}
+								{/if}
+
+								{#if message.error}
+									<div
+										class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
+									>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											fill="none"
+											viewBox="0 0 24 24"
+											stroke-width="1.5"
+											stroke="currentColor"
+											class="w-5 h-5 self-center"
+										>
+											<path
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
+											/>
+										</svg>
 
-											if (metadata?.name) {
-												source = { ...source, name: metadata.name };
-											}
+										<div class=" self-center">
+											{message?.error?.content ?? message.content}
+										</div>
+									</div>
+								{/if}
+
+								{#if message.citations}
+									<div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
+										{#each message.citations.reduce((acc, citation) => {
+											citation.document.forEach((document, index) => {
+												const metadata = citation.metadata?.[index];
+												const id = metadata?.source ?? 'N/A';
+												let source = citation?.source;
+
+												if (metadata?.name) {
+													source = { ...source, name: metadata.name };
+												}
+
+												// Check if ID looks like a URL
+												if (id.startsWith('http://') || id.startsWith('https://')) {
+													source = { name: id };
+												}
+
+												const existingSource = acc.find((item) => item.id === id);
+
+												if (existingSource) {
+													existingSource.document.push(document);
+													existingSource.metadata.push(metadata);
+												} else {
+													acc.push( { id: id, source: source, document: [document], metadata: metadata ? [metadata] : [] } );
+												}
+											});
+											return acc;
+										}, []) as citation, idx}
+											<div class="flex gap-1 text-xs font-semibold">
+												<button
+													class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl"
+													on:click={() => {
+														showCitationModal = true;
+														selectedCitation = citation;
+													}}
+												>
+													<div class="bg-white dark:bg-gray-700 rounded-full size-4">
+														{idx + 1}
+													</div>
+													<div class="flex-1 mx-2 line-clamp-1">
+														{citation.source.name}
+													</div>
+												</button>
+											</div>
+										{/each}
+									</div>
+								{/if}
+							</div>
+						{/if}
+					</div>
+				</div>
 
-											// Check if ID looks like a URL
-											if (id.startsWith('http://') || id.startsWith('https://')) {
-												source = { name: id };
-											}
+				{#if !edit}
+					{#if message.done || siblings.length > 1}
+						<div
+							class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500"
+						>
+							{#if siblings.length > 1}
+								<div class="flex self-center min-w-fit" dir="ltr">
+									<button
+										class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
+										on:click={() => {
+											showPreviousMessage(message);
+										}}
+									>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											fill="none"
+											viewBox="0 0 24 24"
+											stroke="currentColor"
+											stroke-width="2.5"
+											class="size-3.5"
+										>
+											<path
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												d="M15.75 19.5 8.25 12l7.5-7.5"
+											/>
+										</svg>
+									</button>
 
-											const existingSource = acc.find((item) => item.id === id);
+									<div
+										class="text-sm tracking-widest font-semibold self-center dark:text-gray-100 min-w-fit"
+									>
+										{siblings.indexOf(message.id) + 1}/{siblings.length}
+									</div>
 
-											if (existingSource) {
-												existingSource.document.push(document);
-												existingSource.metadata.push(metadata);
-											} else {
-												acc.push( { id: id, source: source, document: [document], metadata: metadata ? [metadata] : [] } );
-											}
-										});
-										return acc;
-									}, []) as citation, idx}
-										<div class="flex gap-1 text-xs font-semibold">
-											<button
-												class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl"
-												on:click={() => {
-													showCitationModal = true;
-													selectedCitation = citation;
-												}}
-											>
-												<div class="bg-white dark:bg-gray-700 rounded-full size-4">
-													{idx + 1}
-												</div>
-												<div class="flex-1 mx-2 line-clamp-1">
-													{citation.source.name}
-												</div>
-											</button>
-										</div>
-									{/each}
+									<button
+										class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
+										on:click={() => {
+											showNextMessage(message);
+										}}
+									>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											fill="none"
+											viewBox="0 0 24 24"
+											stroke="currentColor"
+											stroke-width="2.5"
+											class="size-3.5"
+										>
+											<path
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												d="m8.25 4.5 7.5 7.5-7.5 7.5"
+											/>
+										</svg>
+									</button>
 								</div>
 							{/if}
 
-							{#if message.done || siblings.length > 1}
-								<div
-									class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500"
-								>
-									{#if siblings.length > 1}
-										<div class="flex self-center min-w-fit" dir="ltr">
-											<button
-												class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
-												on:click={() => {
-													showPreviousMessage(message);
-												}}
+							{#if message.done}
+								{#if !readOnly}
+									<Tooltip content={$i18n.t('Edit')} placement="bottom">
+										<button
+											class="{isLastMessage
+												? 'visible'
+												: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
+											on:click={() => {
+												editMessageHandler();
+											}}
+										>
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												fill="none"
+												viewBox="0 0 24 24"
+												stroke-width="2.3"
+												stroke="currentColor"
+												class="w-4 h-4"
 											>
+												<path
+													stroke-linecap="round"
+													stroke-linejoin="round"
+													d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
+												/>
+											</svg>
+										</button>
+									</Tooltip>
+								{/if}
+
+								<Tooltip content={$i18n.t('Copy')} placement="bottom">
+									<button
+										class="{isLastMessage
+											? 'visible'
+											: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition copy-response-button"
+										on:click={() => {
+											copyToClipboard(message.content);
+										}}
+									>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											fill="none"
+											viewBox="0 0 24 24"
+											stroke-width="2.3"
+											stroke="currentColor"
+											class="w-4 h-4"
+										>
+											<path
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
+											/>
+										</svg>
+									</button>
+								</Tooltip>
+
+								<Tooltip content={$i18n.t('Read Aloud')} placement="bottom">
+									<button
+										id="speak-button-{message.id}"
+										class="{isLastMessage
+											? 'visible'
+											: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
+										on:click={() => {
+											if (!loadingSpeech) {
+												toggleSpeakMessage(message);
+											}
+										}}
+									>
+										{#if loadingSpeech}
+											<svg
+												class=" w-4 h-4"
+												fill="currentColor"
+												viewBox="0 0 24 24"
+												xmlns="http://www.w3.org/2000/svg"
+												><style>
+													.spinner_S1WN {
+														animation: spinner_MGfb 0.8s linear infinite;
+														animation-delay: -0.8s;
+													}
+													.spinner_Km9P {
+														animation-delay: -0.65s;
+													}
+													.spinner_JApP {
+														animation-delay: -0.5s;
+													}
+													@keyframes spinner_MGfb {
+														93.75%,
+														100% {
+															opacity: 0.2;
+														}
+													}
+												</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
+													class="spinner_S1WN spinner_Km9P"
+													cx="12"
+													cy="12"
+													r="3"
+												/><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" /></svg
+											>
+										{:else if speaking}
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												fill="none"
+												viewBox="0 0 24 24"
+												stroke-width="2.3"
+												stroke="currentColor"
+												class="w-4 h-4"
+											>
+												<path
+													stroke-linecap="round"
+													stroke-linejoin="round"
+													d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
+												/>
+											</svg>
+										{:else}
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												fill="none"
+												viewBox="0 0 24 24"
+												stroke-width="2.3"
+												stroke="currentColor"
+												class="w-4 h-4"
+											>
+												<path
+													stroke-linecap="round"
+													stroke-linejoin="round"
+													d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
+												/>
+											</svg>
+										{/if}
+									</button>
+								</Tooltip>
+
+								{#if $config?.features.enable_image_generation && !readOnly}
+									<Tooltip content={$i18n.t('Generate Image')} placement="bottom">
+										<button
+											class="{isLastMessage
+												? 'visible'
+												: 'invisible group-hover:visible'}  p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
+											on:click={() => {
+												if (!generatingImage) {
+													generateImage(message);
+												}
+											}}
+										>
+											{#if generatingImage}
+												<svg
+													class=" w-4 h-4"
+													fill="currentColor"
+													viewBox="0 0 24 24"
+													xmlns="http://www.w3.org/2000/svg"
+													><style>
+														.spinner_S1WN {
+															animation: spinner_MGfb 0.8s linear infinite;
+															animation-delay: -0.8s;
+														}
+														.spinner_Km9P {
+															animation-delay: -0.65s;
+														}
+														.spinner_JApP {
+															animation-delay: -0.5s;
+														}
+														@keyframes spinner_MGfb {
+															93.75%,
+															100% {
+																opacity: 0.2;
+															}
+														}
+													</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
+														class="spinner_S1WN spinner_Km9P"
+														cx="12"
+														cy="12"
+														r="3"
+													/><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" /></svg
+												>
+											{:else}
 												<svg
 													xmlns="http://www.w3.org/2000/svg"
 													fill="none"
 													viewBox="0 0 24 24"
+													stroke-width="2.3"
 													stroke="currentColor"
-													stroke-width="2.5"
-													class="size-3.5"
+													class="w-4 h-4"
 												>
 													<path
 														stroke-linecap="round"
 														stroke-linejoin="round"
-														d="M15.75 19.5 8.25 12l7.5-7.5"
+														d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
 													/>
 												</svg>
-											</button>
-
-											<div
-												class="text-sm tracking-widest font-semibold self-center dark:text-gray-100 min-w-fit"
+											{/if}
+										</button>
+									</Tooltip>
+								{/if}
+
+								{#if message.info}
+									<Tooltip content={$i18n.t('Generation Info')} placement="bottom">
+										<button
+											class=" {isLastMessage
+												? 'visible'
+												: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
+											on:click={() => {
+												console.log(message);
+											}}
+											id="info-{message.id}"
+										>
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												fill="none"
+												viewBox="0 0 24 24"
+												stroke-width="2.3"
+												stroke="currentColor"
+												class="w-4 h-4"
 											>
-												{siblings.indexOf(message.id) + 1}/{siblings.length}
-											</div>
+												<path
+													stroke-linecap="round"
+													stroke-linejoin="round"
+													d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
+												/>
+											</svg>
+										</button>
+									</Tooltip>
+								{/if}
+
+								{#if !readOnly}
+									<Tooltip content={$i18n.t('Good Response')} placement="bottom">
+										<button
+											class="{isLastMessage
+												? 'visible'
+												: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message
+												?.annotation?.rating ?? null) === 1
+												? 'bg-gray-100 dark:bg-gray-800'
+												: ''} dark:hover:text-white hover:text-black transition"
+											on:click={() => {
+												rateMessage(message.id, 1);
+												showRateComment = true;
+
+												window.setTimeout(() => {
+													document
+														.getElementById(`message-feedback-${message.id}`)
+														?.scrollIntoView();
+												}, 0);
+											}}
+										>
+											<svg
+												stroke="currentColor"
+												fill="none"
+												stroke-width="2.3"
+												viewBox="0 0 24 24"
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												class="w-4 h-4"
+												xmlns="http://www.w3.org/2000/svg"
+												><path
+													d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
+												/></svg
+											>
+										</button>
+									</Tooltip>
+
+									<Tooltip content={$i18n.t('Bad Response')} placement="bottom">
+										<button
+											class="{isLastMessage
+												? 'visible'
+												: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message
+												?.annotation?.rating ?? null) === -1
+												? 'bg-gray-100 dark:bg-gray-800'
+												: ''} dark:hover:text-white hover:text-black transition"
+											on:click={() => {
+												rateMessage(message.id, -1);
+												showRateComment = true;
+												window.setTimeout(() => {
+													document
+														.getElementById(`message-feedback-${message.id}`)
+														?.scrollIntoView();
+												}, 0);
+											}}
+										>
+											<svg
+												stroke="currentColor"
+												fill="none"
+												stroke-width="2.3"
+												viewBox="0 0 24 24"
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												class="w-4 h-4"
+												xmlns="http://www.w3.org/2000/svg"
+												><path
+													d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
+												/></svg
+											>
+										</button>
+									</Tooltip>
 
+									{#if isLastMessage}
+										<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
 											<button
-												class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
+												type="button"
+												class="{isLastMessage
+													? 'visible'
+													: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
 												on:click={() => {
-													showNextMessage(message);
+													continueGeneration();
 												}}
 											>
 												<svg
 													xmlns="http://www.w3.org/2000/svg"
 													fill="none"
 													viewBox="0 0 24 24"
+													stroke-width="2.3"
 													stroke="currentColor"
-													stroke-width="2.5"
-													class="size-3.5"
+													class="w-4 h-4"
 												>
 													<path
 														stroke-linecap="round"
 														stroke-linejoin="round"
-														d="m8.25 4.5 7.5 7.5-7.5 7.5"
+														d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
+													/>
+													<path
+														stroke-linecap="round"
+														stroke-linejoin="round"
+														d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
 													/>
 												</svg>
 											</button>
-										</div>
-									{/if}
-
-									{#if message.done}
-										{#if !readOnly}
-											<Tooltip content={$i18n.t('Edit')} placement="bottom">
-												<button
-													class="{isLastMessage
-														? 'visible'
-														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
-													on:click={() => {
-														editMessageHandler();
-													}}
-												>
-													<svg
-														xmlns="http://www.w3.org/2000/svg"
-														fill="none"
-														viewBox="0 0 24 24"
-														stroke-width="2.3"
-														stroke="currentColor"
-														class="w-4 h-4"
-													>
-														<path
-															stroke-linecap="round"
-															stroke-linejoin="round"
-															d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
-														/>
-													</svg>
-												</button>
-											</Tooltip>
-										{/if}
+										</Tooltip>
 
-										<Tooltip content={$i18n.t('Copy')} placement="bottom">
+										<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
 											<button
+												type="button"
 												class="{isLastMessage
 													? 'visible'
-													: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition copy-response-button"
+													: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
 												on:click={() => {
-													copyToClipboard(message.content);
+													showRateComment = false;
+													regenerateResponse(message);
 												}}
 											>
 												<svg
@@ -719,365 +994,55 @@
 													<path
 														stroke-linecap="round"
 														stroke-linejoin="round"
-														d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
+														d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
 													/>
 												</svg>
 											</button>
 										</Tooltip>
 
-										<Tooltip content={$i18n.t('Read Aloud')} placement="bottom">
-											<button
-												id="speak-button-{message.id}"
-												class="{isLastMessage
-													? 'visible'
-													: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
-												on:click={() => {
-													if (!loadingSpeech) {
-														toggleSpeakMessage(message);
-													}
-												}}
-											>
-												{#if loadingSpeech}
-													<svg
-														class=" w-4 h-4"
-														fill="currentColor"
-														viewBox="0 0 24 24"
-														xmlns="http://www.w3.org/2000/svg"
-														><style>
-															.spinner_S1WN {
-																animation: spinner_MGfb 0.8s linear infinite;
-																animation-delay: -0.8s;
-															}
-															.spinner_Km9P {
-																animation-delay: -0.65s;
-															}
-															.spinner_JApP {
-																animation-delay: -0.5s;
-															}
-															@keyframes spinner_MGfb {
-																93.75%,
-																100% {
-																	opacity: 0.2;
-																}
-															}
-														</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
-															class="spinner_S1WN spinner_Km9P"
-															cx="12"
-															cy="12"
-															r="3"
-														/><circle
-															class="spinner_S1WN spinner_JApP"
-															cx="20"
-															cy="12"
-															r="3"
-														/></svg
-													>
-												{:else if speaking}
-													<svg
-														xmlns="http://www.w3.org/2000/svg"
-														fill="none"
-														viewBox="0 0 24 24"
-														stroke-width="2.3"
-														stroke="currentColor"
-														class="w-4 h-4"
-													>
-														<path
-															stroke-linecap="round"
-															stroke-linejoin="round"
-															d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
-														/>
-													</svg>
-												{:else}
-													<svg
-														xmlns="http://www.w3.org/2000/svg"
-														fill="none"
-														viewBox="0 0 24 24"
-														stroke-width="2.3"
-														stroke="currentColor"
-														class="w-4 h-4"
-													>
-														<path
-															stroke-linecap="round"
-															stroke-linejoin="round"
-															d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
-														/>
-													</svg>
-												{/if}
-											</button>
-										</Tooltip>
-
-										{#if $config?.features.enable_image_generation && !readOnly}
-											<Tooltip content={$i18n.t('Generate Image')} placement="bottom">
+										{#each model?.actions ?? [] as action}
+											<Tooltip content={action.name} placement="bottom">
 												<button
+													type="button"
 													class="{isLastMessage
 														? 'visible'
-														: 'invisible group-hover:visible'}  p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
+														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
 													on:click={() => {
-														if (!generatingImage) {
-															generateImage(message);
-														}
+														dispatch('action', action.id);
 													}}
 												>
-													{#if generatingImage}
-														<svg
-															class=" w-4 h-4"
-															fill="currentColor"
-															viewBox="0 0 24 24"
-															xmlns="http://www.w3.org/2000/svg"
-															><style>
-																.spinner_S1WN {
-																	animation: spinner_MGfb 0.8s linear infinite;
-																	animation-delay: -0.8s;
-																}
-																.spinner_Km9P {
-																	animation-delay: -0.65s;
-																}
-																.spinner_JApP {
-																	animation-delay: -0.5s;
-																}
-																@keyframes spinner_MGfb {
-																	93.75%,
-																	100% {
-																		opacity: 0.2;
-																	}
-																}
-															</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
-																class="spinner_S1WN spinner_Km9P"
-																cx="12"
-																cy="12"
-																r="3"
-															/><circle
-																class="spinner_S1WN spinner_JApP"
-																cx="20"
-																cy="12"
-																r="3"
-															/></svg
-														>
+													{#if action.icon_url}
+														<img
+															src={action.icon_url}
+															class="w-4 h-4 {action.icon_url.includes('svg')
+																? 'dark:invert-[80%]'
+																: ''}"
+															style="fill: currentColor;"
+															alt={action.name}
+														/>
 													{:else}
-														<svg
-															xmlns="http://www.w3.org/2000/svg"
-															fill="none"
-															viewBox="0 0 24 24"
-															stroke-width="2.3"
-															stroke="currentColor"
-															class="w-4 h-4"
-														>
-															<path
-																stroke-linecap="round"
-																stroke-linejoin="round"
-																d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
-															/>
-														</svg>
+														<Sparkles strokeWidth="2.1" className="size-4" />
 													{/if}
 												</button>
 											</Tooltip>
-										{/if}
-
-										{#if message.info}
-											<Tooltip content={$i18n.t('Generation Info')} placement="bottom">
-												<button
-													class=" {isLastMessage
-														? 'visible'
-														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
-													on:click={() => {
-														console.log(message);
-													}}
-													id="info-{message.id}"
-												>
-													<svg
-														xmlns="http://www.w3.org/2000/svg"
-														fill="none"
-														viewBox="0 0 24 24"
-														stroke-width="2.3"
-														stroke="currentColor"
-														class="w-4 h-4"
-													>
-														<path
-															stroke-linecap="round"
-															stroke-linejoin="round"
-															d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
-														/>
-													</svg>
-												</button>
-											</Tooltip>
-										{/if}
-
-										{#if !readOnly}
-											<Tooltip content={$i18n.t('Good Response')} placement="bottom">
-												<button
-													class="{isLastMessage
-														? 'visible'
-														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message
-														?.annotation?.rating ?? null) === 1
-														? 'bg-gray-100 dark:bg-gray-800'
-														: ''} dark:hover:text-white hover:text-black transition"
-													on:click={() => {
-														rateMessage(message.id, 1);
-														showRateComment = true;
-
-														window.setTimeout(() => {
-															document
-																.getElementById(`message-feedback-${message.id}`)
-																?.scrollIntoView();
-														}, 0);
-													}}
-												>
-													<svg
-														stroke="currentColor"
-														fill="none"
-														stroke-width="2.3"
-														viewBox="0 0 24 24"
-														stroke-linecap="round"
-														stroke-linejoin="round"
-														class="w-4 h-4"
-														xmlns="http://www.w3.org/2000/svg"
-														><path
-															d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
-														/></svg
-													>
-												</button>
-											</Tooltip>
-
-											<Tooltip content={$i18n.t('Bad Response')} placement="bottom">
-												<button
-													class="{isLastMessage
-														? 'visible'
-														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message
-														?.annotation?.rating ?? null) === -1
-														? 'bg-gray-100 dark:bg-gray-800'
-														: ''} dark:hover:text-white hover:text-black transition"
-													on:click={() => {
-														rateMessage(message.id, -1);
-														showRateComment = true;
-														window.setTimeout(() => {
-															document
-																.getElementById(`message-feedback-${message.id}`)
-																?.scrollIntoView();
-														}, 0);
-													}}
-												>
-													<svg
-														stroke="currentColor"
-														fill="none"
-														stroke-width="2.3"
-														viewBox="0 0 24 24"
-														stroke-linecap="round"
-														stroke-linejoin="round"
-														class="w-4 h-4"
-														xmlns="http://www.w3.org/2000/svg"
-														><path
-															d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
-														/></svg
-													>
-												</button>
-											</Tooltip>
-
-											{#if isLastMessage}
-												<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
-													<button
-														type="button"
-														class="{isLastMessage
-															? 'visible'
-															: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
-														on:click={() => {
-															continueGeneration();
-														}}
-													>
-														<svg
-															xmlns="http://www.w3.org/2000/svg"
-															fill="none"
-															viewBox="0 0 24 24"
-															stroke-width="2.3"
-															stroke="currentColor"
-															class="w-4 h-4"
-														>
-															<path
-																stroke-linecap="round"
-																stroke-linejoin="round"
-																d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
-															/>
-															<path
-																stroke-linecap="round"
-																stroke-linejoin="round"
-																d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
-															/>
-														</svg>
-													</button>
-												</Tooltip>
-
-												<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
-													<button
-														type="button"
-														class="{isLastMessage
-															? 'visible'
-															: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
-														on:click={() => {
-															showRateComment = false;
-															regenerateResponse(message);
-														}}
-													>
-														<svg
-															xmlns="http://www.w3.org/2000/svg"
-															fill="none"
-															viewBox="0 0 24 24"
-															stroke-width="2.3"
-															stroke="currentColor"
-															class="w-4 h-4"
-														>
-															<path
-																stroke-linecap="round"
-																stroke-linejoin="round"
-																d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
-															/>
-														</svg>
-													</button>
-												</Tooltip>
-
-												{#each model?.actions ?? [] as action}
-													<Tooltip content={action.name} placement="bottom">
-														<button
-															type="button"
-															class="{isLastMessage
-																? 'visible'
-																: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
-															on:click={() => {
-																dispatch('action', action.id);
-															}}
-														>
-															{#if action.icon_url}
-																<img
-																	src={action.icon_url}
-																	class="w-4 h-4 {action.icon_url.includes('svg')
-																		? 'dark:invert-[80%]'
-																		: ''}"
-																	style="fill: currentColor;"
-																	alt={action.name}
-																/>
-															{:else}
-																<Sparkles strokeWidth="2.1" className="size-4" />
-															{/if}
-														</button>
-													</Tooltip>
-												{/each}
-											{/if}
-										{/if}
+										{/each}
 									{/if}
-								</div>
-							{/if}
-
-							{#if message.done && showRateComment}
-								<RateComment
-									messageId={message.id}
-									bind:show={showRateComment}
-									bind:message
-									on:submit={() => {
-										updateChatMessages();
-									}}
-								/>
+								{/if}
 							{/if}
 						</div>
 					{/if}
-				</div>
+
+					{#if message.done && showRateComment}
+						<RateComment
+							messageId={message.id}
+							bind:show={showRateComment}
+							bind:message
+							on:submit={() => {
+								updateChatMessages();
+							}}
+						/>
+					{/if}
+				{/if}
 			</div>
 		</div>
 	</div>

+ 12 - 4
src/lib/components/chat/Settings/Chats.svelte

@@ -2,7 +2,7 @@
 	import fileSaver from 'file-saver';
 	const { saveAs } = fileSaver;
 
-	import { chats, user, settings } from '$lib/stores';
+	import { chats, user, settings, scrollPaginationEnabled, currentChatPage } from '$lib/stores';
 
 	import {
 		archiveAllChats,
@@ -62,7 +62,9 @@
 			}
 		}
 
-		await chats.set(await getChatList(localStorage.token));
+		currentChatPage.set(1);
+		await chats.set(await getChatList(localStorage.token, $currentChatPage));
+		scrollPaginationEnabled.set(true);
 	};
 
 	const exportChats = async () => {
@@ -77,7 +79,10 @@
 		await archiveAllChats(localStorage.token).catch((error) => {
 			toast.error(error);
 		});
-		await chats.set(await getChatList(localStorage.token));
+
+		currentChatPage.set(1);
+		await chats.set(await getChatList(localStorage.token, $currentChatPage));
+		scrollPaginationEnabled.set(true);
 	};
 
 	const deleteAllChatsHandler = async () => {
@@ -85,7 +90,10 @@
 		await deleteAllChats(localStorage.token).catch((error) => {
 			toast.error(error);
 		});
-		await chats.set(await getChatList(localStorage.token));
+
+		currentChatPage.set(1);
+		await chats.set(await getChatList(localStorage.token, $currentChatPage));
+		scrollPaginationEnabled.set(true);
 	};
 
 	const toggleSaveChatHistory = async () => {

+ 29 - 0
src/lib/components/chat/Settings/Interface.svelte

@@ -22,6 +22,7 @@
 	let responseAutoCopy = false;
 	let widescreenMode = false;
 	let splitLargeChunks = false;
+	let scrollOnBranchChange = true;
 	let userLocation = false;
 
 	// Interface
@@ -39,6 +40,11 @@
 		saveSettings({ splitLargeChunks: splitLargeChunks });
 	};
 
+	const togglesScrollOnBranchChange = async () => {
+		scrollOnBranchChange = !scrollOnBranchChange;
+		saveSettings({ scrollOnBranchChange: scrollOnBranchChange });
+	};
+
 	const togglewidescreenMode = async () => {
 		widescreenMode = !widescreenMode;
 		saveSettings({ widescreenMode: widescreenMode });
@@ -141,6 +147,7 @@
 		chatBubble = $settings.chatBubble ?? true;
 		widescreenMode = $settings.widescreenMode ?? false;
 		splitLargeChunks = $settings.splitLargeChunks ?? false;
+		scrollOnBranchChange = $settings.scrollOnBranchChange ?? true;
 		chatDirection = $settings.chatDirection ?? 'LTR';
 		userLocation = $settings.userLocation ?? false;
 
@@ -318,6 +325,28 @@
 				</div>
 			</div>
 
+			<div>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs">
+						{$i18n.t('Scroll to bottom when switching between branches')}
+					</div>
+
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						on:click={() => {
+							togglesScrollOnBranchChange();
+						}}
+						type="button"
+					>
+						{#if scrollOnBranchChange === true}
+							<span class="ml-2 self-center">{$i18n.t('On')}</span>
+						{:else}
+							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
+						{/if}
+					</button>
+				</div>
+			</div>
+
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
 					<div class=" self-center text-xs">

+ 11 - 6
src/lib/components/chat/Tags.svelte

@@ -8,7 +8,13 @@
 		getTagsById,
 		updateChatById
 	} from '$lib/apis/chats';
-	import { tags as _tags, chats, pinnedChats } from '$lib/stores';
+	import {
+		tags as _tags,
+		chats,
+		pinnedChats,
+		currentChatPage,
+		scrollPaginationEnabled
+	} from '$lib/stores';
 	import { createEventDispatcher, onMount } from 'svelte';
 
 	const dispatch = createEventDispatcher();
@@ -46,11 +52,7 @@
 			tags: tags
 		});
 
-		console.log($_tags);
 		await _tags.set(await getAllChatTags(localStorage.token));
-
-		console.log($_tags);
-
 		if ($_tags.map((t) => t.name).includes(tagName)) {
 			if (tagName === 'pinned') {
 				await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
@@ -62,8 +64,11 @@
 				dispatch('close');
 			}
 		} else {
-			await chats.set(await getChatList(localStorage.token));
+			// if the tag we deleted is no longer a valid tag, return to main chat list view
+			currentChatPage.set(1);
+			await chats.set(await getChatList(localStorage.token, $currentChatPage));
 			await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
+			await scrollPaginationEnabled.set(true);
 		}
 	};
 

+ 6 - 4
src/lib/components/common/Image.svelte

@@ -5,19 +5,21 @@
 	export let src = '';
 	export let alt = '';
 
-	let _src = '';
+	export let className = '';
 
+	let _src = '';
 	$: _src = src.startsWith('/') ? `${WEBUI_BASE_URL}${src}` : src;
 
 	let showImagePreview = false;
 </script>
 
-<ImagePreview bind:show={showImagePreview} src={_src} {alt} />
 <button
+	class={className}
 	on:click={() => {
-		console.log('image preview');
 		showImagePreview = true;
 	}}
 >
-	<img src={_src} {alt} class=" max-h-96 rounded-lg" draggable="false" data-cy="image" />
+	<img src={_src} {alt} class=" rounded-lg cursor-pointer" draggable="false" data-cy="image" />
 </button>
+
+<ImagePreview bind:show={showImagePreview} src={_src} {alt} />

+ 14 - 11
src/lib/components/common/ImagePreview.svelte

@@ -7,6 +7,8 @@
 
 	let mounted = false;
 
+	let previewElement = null;
+
 	const downloadImage = (url, filename) => {
 		fetch(url)
 			.then((response) => response.blob())
@@ -34,14 +36,14 @@
 		mounted = true;
 	});
 
-	$: if (mounted) {
-		if (show) {
-			window.addEventListener('keydown', handleKeyDown);
-			document.body.style.overflow = 'hidden';
-		} else {
-			window.removeEventListener('keydown', handleKeyDown);
-			document.body.style.overflow = 'unset';
-		}
+	$: if (show && previewElement) {
+		document.body.appendChild(previewElement);
+		window.addEventListener('keydown', handleKeyDown);
+		document.body.style.overflow = 'hidden';
+	} else if (previewElement) {
+		window.removeEventListener('keydown', handleKeyDown);
+		document.body.removeChild(previewElement);
+		document.body.style.overflow = 'unset';
 	}
 </script>
 
@@ -49,9 +51,10 @@
 	<!-- svelte-ignore a11y-click-events-have-key-events -->
 	<!-- svelte-ignore a11y-no-static-element-interactions -->
 	<div
-		class="fixed top-0 right-0 left-0 bottom-0 bg-black text-white w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
+		bind:this={previewElement}
+		class="modal fixed top-0 right-0 left-0 bottom-0 bg-black text-white w-full min-h-screen h-screen flex justify-center z-[9999] overflow-hidden overscroll-contain"
 	>
-		<div class=" absolute left-0 w-full flex justify-between">
+		<div class=" absolute left-0 w-full flex justify-between select-none">
 			<div>
 				<button
 					class=" p-5"
@@ -95,6 +98,6 @@
 				</button>
 			</div>
 		</div>
-		<img {src} {alt} class=" mx-auto h-full object-scale-down" />
+		<img {src} {alt} class=" mx-auto h-full object-scale-down select-none" draggable="false" />
 	</div>
 {/if}

+ 30 - 0
src/lib/components/common/Loader.svelte

@@ -0,0 +1,30 @@
+<script lang="ts">
+	import { createEventDispatcher, onMount } from 'svelte';
+	const dispatch = createEventDispatcher();
+
+	let loaderElement: HTMLElement;
+
+	onMount(() => {
+		const observer = new IntersectionObserver(
+			(entries, observer) => {
+				entries.forEach((entry) => {
+					if (entry.isIntersecting) {
+						dispatch('visible');
+						// observer.unobserve(loaderElement); // Stop observing until content is loaded
+					}
+				});
+			},
+			{
+				root: null, // viewport
+				rootMargin: '0px',
+				threshold: 0.1 // When 10% of the loader is visible
+			}
+		);
+
+		observer.observe(loaderElement);
+	});
+</script>
+
+<div bind:this={loaderElement}>
+	<slot />
+</div>

+ 64 - 7
src/lib/components/layout/Sidebar.svelte

@@ -11,7 +11,9 @@
 		showSidebar,
 		mobile,
 		showArchivedChats,
-		pinnedChats
+		pinnedChats,
+		scrollPaginationEnabled,
+		currentChatPage
 	} from '$lib/stores';
 	import { onMount, getContext, tick } from 'svelte';
 
@@ -34,6 +36,8 @@
 	import UserMenu from './Sidebar/UserMenu.svelte';
 	import ChatItem from './Sidebar/ChatItem.svelte';
 	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
+	import Spinner from '../common/Spinner.svelte';
+	import Loader from '../common/Loader.svelte';
 
 	const BREAKPOINT = 768;
 
@@ -50,6 +54,10 @@
 
 	let filteredChatList = [];
 
+	// Pagination variables
+	let chatListLoading = false;
+	let allChatsLoaded = false;
+
 	$: filteredChatList = $chats.filter((chat) => {
 		if (search === '') {
 			return true;
@@ -70,6 +78,29 @@
 		}
 	});
 
+	const enablePagination = async () => {
+		// Reset pagination variables
+		currentChatPage.set(1);
+		allChatsLoaded = false;
+		await chats.set(await getChatList(localStorage.token, $currentChatPage));
+
+		// Enable pagination
+		scrollPaginationEnabled.set(true);
+	};
+
+	const loadMoreChats = async () => {
+		chatListLoading = true;
+
+		currentChatPage.set($currentChatPage + 1);
+		const newChatList = await getChatList(localStorage.token, $currentChatPage);
+
+		// once the bottom of the list has been reached (no results) there is no need to continue querying
+		allChatsLoaded = newChatList.length === 0;
+		await chats.set([...$chats, ...newChatList]);
+
+		chatListLoading = false;
+	};
+
 	onMount(async () => {
 		mobile.subscribe((e) => {
 			if ($showSidebar && e) {
@@ -82,9 +113,8 @@
 		});
 
 		showSidebar.set(window.innerWidth > BREAKPOINT);
-
 		await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
-		await chats.set(await getChatList(localStorage.token));
+		await enablePagination();
 
 		let touchstart;
 		let touchend;
@@ -185,7 +215,11 @@
 				await tick();
 				goto('/');
 			}
-			await chats.set(await getChatList(localStorage.token));
+
+			allChatsLoaded = false;
+			currentChatPage.set(1);
+			await chats.set(await getChatList(localStorage.token, $currentChatPage));
+
 			await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
 		}
 	};
@@ -410,7 +444,10 @@
 						class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm bg-transparent dark:text-gray-300 outline-none"
 						placeholder={$i18n.t('Search')}
 						bind:value={search}
-						on:focus={() => {
+						on:focus={async () => {
+							// TODO: migrate backend for more scalable search mechanism
+							scrollPaginationEnabled.set(false);
+							await chats.set(await getChatList(localStorage.token)); // when searching, load all chats
 							enrichChatsWithContent($chats);
 						}}
 					/>
@@ -422,7 +459,7 @@
 					<button
 						class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
 						on:click={async () => {
-							await chats.set(await getChatList(localStorage.token));
+							await enablePagination();
 						}}
 					>
 						{$i18n.t('all')}
@@ -431,12 +468,17 @@
 						<button
 							class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
 							on:click={async () => {
+								scrollPaginationEnabled.set(false);
 								let chatIds = await getChatListByTagName(localStorage.token, tag.name);
 								if (chatIds.length === 0) {
 									await tags.set(await getAllChatTags(localStorage.token));
-									chatIds = await getChatList(localStorage.token);
+
+									// if the tag we deleted is no longer a valid tag, return to main chat list view
+									await enablePagination();
 								}
 								await chats.set(chatIds);
+
+								chatListLoading = false;
 							}}
 						>
 							{tag.name}
@@ -527,6 +569,21 @@
 						}}
 					/>
 				{/each}
+
+				{#if $scrollPaginationEnabled && !allChatsLoaded}
+					<Loader
+						on:visible={(e) => {
+							if (!chatListLoading) {
+								loadMoreChats();
+							}
+						}}
+					>
+						<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
+							<Spinner className=" size-4" />
+							<div class=" ">Loading...</div>
+						</div>
+					</Loader>
+				{/if}
 			</div>
 		</div>
 

+ 10 - 4
src/lib/components/layout/Sidebar/ChatItem.svelte

@@ -14,7 +14,7 @@
 		getChatListByTagName,
 		updateChatById
 	} from '$lib/apis/chats';
-	import { chatId, chats, mobile, pinnedChats, showSidebar } from '$lib/stores';
+	import { chatId, chats, mobile, pinnedChats, showSidebar, currentChatPage } from '$lib/stores';
 
 	import ChatMenu from './ChatMenu.svelte';
 	import ShareChatModal from '$lib/components/chat/ShareChatModal.svelte';
@@ -40,7 +40,9 @@
 			await updateChatById(localStorage.token, id, {
 				title: _title
 			});
-			await chats.set(await getChatList(localStorage.token));
+
+			currentChatPage.set(1);
+			await chats.set(await getChatList(localStorage.token, $currentChatPage));
 			await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
 		}
 	};
@@ -53,14 +55,18 @@
 
 		if (res) {
 			goto(`/c/${res.id}`);
-			await chats.set(await getChatList(localStorage.token));
+
+			currentChatPage.set(1);
+			await chats.set(await getChatList(localStorage.token, $currentChatPage));
 			await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
 		}
 	};
 
 	const archiveChatHandler = async (id) => {
 		await archiveChatById(localStorage.token, id);
-		await chats.set(await getChatList(localStorage.token));
+
+		currentChatPage.set(1);
+		await chats.set(await getChatList(localStorage.token, $currentChatPage));
 		await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
 	};
 

+ 1 - 0
src/lib/i18n/locales/ar-BH/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "مسح",
 	"Scan complete!": "تم المسح",
 	"Scan for documents from {{path}}": "{{path}} مسح على الملفات من",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "البحث",
 	"Search a model": "البحث عن موديل",
 	"Search Chats": "البحث في الدردشات",

+ 1 - 0
src/lib/i18n/locales/bg-BG/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Сканиране",
 	"Scan complete!": "Сканиране завършено!",
 	"Scan for documents from {{path}}": "Сканиране за документи в {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Търси",
 	"Search a model": "Търси модел",
 	"Search Chats": "Търсене на чатове",

+ 1 - 0
src/lib/i18n/locales/bn-BD/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "স্ক্যান",
 	"Scan complete!": "স্ক্যান সম্পন্ন হয়েছে!",
 	"Scan for documents from {{path}}": "ডকুমেন্টসমূহের জন্য {{path}} স্ক্যান করুন",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "অনুসন্ধান",
 	"Search a model": "মডেল অনুসন্ধান করুন",
 	"Search Chats": "চ্যাট অনুসন্ধান করুন",

+ 9 - 8
src/lib/i18n/locales/ca-ES/translation.json

@@ -15,7 +15,7 @@
 	"Account": "Compte",
 	"Account Activation Pending": "Activació del compte pendent",
 	"Accurate information": "Informació precisa",
-	"Actions": "",
+	"Actions": "Accions",
 	"Active Users": "Usuaris actius",
 	"Add": "Afegir",
 	"Add a model id": "Afegeix un identificador de model",
@@ -28,7 +28,7 @@
 	"Add Memory": "Afegir memòria",
 	"Add message": "Afegir un missatge",
 	"Add Model": "Afegir un model",
-	"Add Tag": "",
+	"Add Tag": "Afegir etiqueta",
 	"Add Tags": "Afegir etiquetes",
 	"Add User": "Afegir un usuari",
 	"Adjusting these settings will apply changes universally to all users.": "Si ajustes aquesta preferència, els canvis s'aplicaran de manera universal a tots els usuaris.",
@@ -170,7 +170,7 @@
 	"Delete chat": "Eliminar xat",
 	"Delete Chat": "Eliminar xat",
 	"Delete chat?": "Eliminar el xat?",
-	"Delete Doc": "",
+	"Delete Doc": "Eliminar document",
 	"Delete function?": "Eliminar funció?",
 	"Delete prompt?": "Eliminar indicació?",
 	"delete this link": "Eliminar aquest enllaç",
@@ -214,7 +214,7 @@
 	"Edit Doc": "Editar el document",
 	"Edit Memory": "Editar la memòria",
 	"Edit User": "Editar l'usuari",
-	"ElevenLabs": "",
+	"ElevenLabs": "ElevenLabs",
 	"Email": "Correu electrònic",
 	"Embedding Batch Size": "Mida del lot d'incrustació",
 	"Embedding Model": "Model d'incrustació",
@@ -278,7 +278,7 @@
 	"File": "Arxiu",
 	"File Mode": "Mode d'arxiu",
 	"File not found.": "No s'ha trobat l'arxiu.",
-	"Files": "",
+	"Files": "Arxius",
 	"Filter is now globally disabled": "El filtre ha estat desactivat globalment",
 	"Filter is now globally enabled": "El filtre ha estat activat globalment",
 	"Filters": "Filtres",
@@ -375,7 +375,7 @@
 	"Memory deleted successfully": "Memòria eliminada correctament",
 	"Memory updated successfully": "Memòria actualitzada correctament",
 	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Els missatges enviats després de crear el teu enllaç no es compartiran. Els usuaris amb l'URL podran veure el xat compartit.",
-	"Min P": "",
+	"Min P": "Min P",
 	"Minimum Score": "Puntuació mínima",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Eta de Mirostat",
@@ -504,11 +504,12 @@
 	"Save": "Desar",
 	"Save & Create": "Desar i crear",
 	"Save & Update": "Desar i actualitzar",
-	"Save Tag": "",
+	"Save Tag": "Desar l'etiqueta",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Desar els registres de xat directament a l'emmagatzematge del teu navegador ja no està suportat. Si us plau, descarregr i elimina els registres de xat fent clic al botó de sota. No et preocupis, pots tornar a importar fàcilment els teus registres de xat al backend a través de",
 	"Scan": "Escanejar",
 	"Scan complete!": "Escaneigr completat!",
 	"Scan for documents from {{path}}": "Escanejar documents des de {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Cercar",
 	"Search a model": "Cercar un model",
 	"Search Chats": "Cercar xats",
@@ -623,7 +624,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Per accedir a la WebUI, poseu-vos en contacte amb l'administrador. Els administradors poden gestionar els estats dels usuaris des del tauler d'administració.",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "Per afegir documents aquí, puja-ls primer a l'espai de treball \"Documents\".",
 	"to chat input.": "a l'entrada del xat.",
-	"To select actions here, add them to the \"Functions\" workspace first.": "",
+	"To select actions here, add them to the \"Functions\" workspace first.": "Per seleccionar accions aquí, afegeix-los primer a l'espai de treball \"Funcions\".",
 	"To select filters here, add them to the \"Functions\" workspace first.": "Per seleccionar filtres aquí, afegeix-los primer a l'espai de treball \"Funcions\".",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "Per seleccionar kits d'eines aquí, afegeix-los primer a l'espai de treball \"Eines\".",
 	"Today": "Avui",

+ 1 - 0
src/lib/i18n/locales/ceb-PH/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Aron ma-scan",
 	"Scan complete!": "Nakompleto ang pag-scan!",
 	"Scan for documents from {{path}}": "I-scan ang mga dokumento gikan sa {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Pagpanukiduki",
 	"Search a model": "",
 	"Search Chats": "",

+ 1 - 0
src/lib/i18n/locales/de-DE/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Scannen",
 	"Scan complete!": "Scan abgeschlossen!",
 	"Scan for documents from {{path}}": "Dokumente im {{path}} scannen",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Suchen",
 	"Search a model": "Modell suchen",
 	"Search Chats": "Unterhaltungen durchsuchen...",

+ 1 - 0
src/lib/i18n/locales/dg-DG/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Scan much scan",
 	"Scan complete!": "Scan complete! Very wow!",
 	"Scan for documents from {{path}}": "Scan for documents from {{path}} wow",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Search very search",
 	"Search a model": "",
 	"Search Chats": "",

+ 1 - 0
src/lib/i18n/locales/en-GB/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "",
 	"Scan complete!": "",
 	"Scan for documents from {{path}}": "",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "",
 	"Search a model": "",
 	"Search Chats": "",

+ 1 - 0
src/lib/i18n/locales/en-US/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "",
 	"Scan complete!": "",
 	"Scan for documents from {{path}}": "",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "",
 	"Search a model": "",
 	"Search Chats": "",

+ 1 - 0
src/lib/i18n/locales/es-ES/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Escanear",
 	"Scan complete!": "¡Escaneo completado!",
 	"Scan for documents from {{path}}": "Escanear en busca de documentos desde {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Buscar",
 	"Search a model": "Buscar un modelo",
 	"Search Chats": "Chats de búsqueda",

+ 1 - 0
src/lib/i18n/locales/fa-IR/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "اسکن",
 	"Scan complete!": "اسکن کامل شد!",
 	"Scan for documents from {{path}}": "اسکن اسناد از {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "جستجو",
 	"Search a model": "جستجوی مدل",
 	"Search Chats": "جستجو گپ ها",

+ 1 - 0
src/lib/i18n/locales/fi-FI/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Skannaa",
 	"Scan complete!": "Skannaus valmis!",
 	"Scan for documents from {{path}}": "Skannaa asiakirjoja polusta {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Haku",
 	"Search a model": "Hae mallia",
 	"Search Chats": "Etsi chatteja",

+ 1 - 0
src/lib/i18n/locales/fr-CA/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Scanner",
 	"Scan complete!": "Scan terminé !",
 	"Scan for documents from {{path}}": "Scanner des documents depuis {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Recherche",
 	"Search a model": "Rechercher un modèle",
 	"Search Chats": "Rechercher des conversations",

+ 1 - 0
src/lib/i18n/locales/fr-FR/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Scanner",
 	"Scan complete!": "Scan terminé !",
 	"Scan for documents from {{path}}": "Scanner des documents depuis {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Recherche",
 	"Search a model": "Rechercher un modèle",
 	"Search Chats": "Rechercher des conversations",

+ 1 - 0
src/lib/i18n/locales/he-IL/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "סרוק",
 	"Scan complete!": "הסריקה הושלמה!",
 	"Scan for documents from {{path}}": "סרוק מסמכים מ-{{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "חפש",
 	"Search a model": "חפש מודל",
 	"Search Chats": "חיפוש צ'אטים",

+ 1 - 0
src/lib/i18n/locales/hi-IN/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "स्कैन",
 	"Scan complete!": "स्कैन पूरा हुआ!",
 	"Scan for documents from {{path}}": "{{path}} से दस्तावेज़ों को स्कैन करें",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "खोजें",
 	"Search a model": "एक मॉडल खोजें",
 	"Search Chats": "चैट खोजें",

+ 1 - 0
src/lib/i18n/locales/hr-HR/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Skeniraj",
 	"Scan complete!": "Skeniranje dovršeno!",
 	"Scan for documents from {{path}}": "Skeniraj dokumente s {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Pretraga",
 	"Search a model": "Pretraži model",
 	"Search Chats": "Pretraži razgovore",

+ 1 - 0
src/lib/i18n/locales/id-ID/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Pindai",
 	"Scan complete!": "Pindai selesai!",
 	"Scan for documents from {{path}}": "Memindai dokumen dari {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Cari",
 	"Search a model": "Mencari model",
 	"Search Chats": "Cari Obrolan",

+ 1 - 0
src/lib/i18n/locales/it-IT/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Scansione",
 	"Scan complete!": "Scansione completata!",
 	"Scan for documents from {{path}}": "Cerca documenti da {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Cerca",
 	"Search a model": "Cerca un modello",
 	"Search Chats": "Cerca nelle chat",

+ 1 - 0
src/lib/i18n/locales/ja-JP/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "スキャン",
 	"Scan complete!": "スキャン完了!",
 	"Scan for documents from {{path}}": "{{path}} からドキュメントをスキャン",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "検索",
 	"Search a model": "モデルを検索",
 	"Search Chats": "チャットの検索",

+ 1 - 0
src/lib/i18n/locales/ka-GE/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "სკანირება",
 	"Scan complete!": "სკანირება დასრულდა!",
 	"Scan for documents from {{path}}": "დოკუმენტების სკანირება {{ path}}-დან",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "ძიება",
 	"Search a model": "მოდელის ძიება",
 	"Search Chats": "ჩატების ძებნა",

+ 1 - 0
src/lib/i18n/locales/ko-KR/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "스캔",
 	"Scan complete!": "스캔 완료!",
 	"Scan for documents from {{path}}": "{{path}} 에서 문서 스캔",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "검색",
 	"Search a model": "모델 검색",
 	"Search Chats": "채팅 검색",

+ 4 - 0
src/lib/i18n/locales/languages.json

@@ -87,6 +87,10 @@
 		"code": "lt-LT",
 		"title": "Lithuanian (Lietuvių)"
 	},
+	{
+		"code": "ms-MY",
+		"title": "Malay (Bahasa Malaysia)"
+	},
 	{
 		"code": "nb-NO",
 		"title": "Norwegian Bokmål (Norway)"

+ 1 - 0
src/lib/i18n/locales/lt-LT/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Skenuoti",
 	"Scan complete!": "Skenavimas baigtas!",
 	"Scan for documents from {{path}}": "Skenuoti dokumentus iš {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Ieškoti",
 	"Search a model": "Ieškoti modelio",
 	"Search Chats": "",

+ 715 - 0
src/lib/i18n/locales/ms-MY/translation.json

@@ -0,0 +1,715 @@
+{
+	"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' or '-1' untuk tiada tempoh luput.",
+	"(Beta)": "(Beta)",
+	"(e.g. `sh webui.sh --api --api-auth username_password`)": "(contoh `sh webui.sh --api --api-auth username_password`)",
+	"(e.g. `sh webui.sh --api`)": "(contoh `sh webui.sh --api`)",
+	"(latest)": "(terkini)",
+	"{{ models }}": "{{ models }}",
+	"{{ owner }}: You cannot delete a base model": "{{ owner }}: Anda tidak boleh memadamkan model asas",
+	"{{modelName}} is thinking...": "{{modelName}} sedang berfikir...",
+	"{{user}}'s Chats": "Perbualan {{user}}",
+	"{{webUIName}} Backend Required": "{{webUIName}} Backend diperlukan",
+	"A task model is used when performing tasks such as generating titles for chats and web search queries": "Model tugas digunakan semasa melaksanakan tugas seperti menjana tajuk untuk perbualan dan pertanyaan carian web.",
+	"a user": "seorang pengguna",
+	"About": "Mengenai",
+	"Account": "Akaun",
+	"Account Activation Pending": "Pengaktifan Akaun belum selesai",
+	"Accurate information": "Informasi tepat",
+	"Actions": "Tindakan",
+	"Active Users": "Pengguna Aktif",
+	"Add": "Tambah",
+	"Add a model id": "Tambah id model",
+	"Add a short description about what this model does": "Tambah penerangan ringkas tentang apa yang model ini boleh lakukan",
+	"Add a short title for this prompt": "Tambah tajuk pendek untuk arahan ini",
+	"Add a tag": "Tambah tag",
+	"Add custom prompt": "Tambah arahan khusus",
+	"Add Docs": "Tambah Dokumen",
+	"Add Files": "Tambah Fail",
+	"Add Memory": "Tambah Memori",
+	"Add message": "Tambah Mesej",
+	"Add Model": "Tambah Model",
+	"Add Tag": "Tambah Tag",
+	"Add Tags": "Tambah Tag",
+	"Add User": "Tambah Pengguna",
+	"Adjusting these settings will apply changes universally to all users.": "Melaraskan tetapan ini akan menggunakan perubahan secara universal kepada semua pengguna.",
+	"admin": "pentadbir",
+	"Admin": "Pentadbir",
+	"Admin Panel": "Panel Pentadbir",
+	"Admin Settings": "Tetapan Pentadbir",
+	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Pentadbir mempunyai akses kepada semua alat pada setiap masa; pengguna memerlukan alat yang ditetapkan mengikut model dalam ruang kerja.",
+	"Advanced Parameters": "Parameter Lanjutan",
+	"Advanced Params": "Parameter Lanjutan",
+	"all": "semua",
+	"All Documents": "Semua Dokumen",
+	"All Users": "Semua Pengguna",
+	"Allow": "Benarkan",
+	"Allow Chat Deletion": "Benarkan Penghapusan Perbualan",
+	"Allow non-local voices": "Benarkan suara bukan tempatan ",
+	"Allow User Location": "Benarkan Lokasi Pengguna",
+	"Allow Voice Interruption in Call": "Benarkan gangguan suara dalam panggilan",
+	"alphanumeric characters and hyphens": "aksara alfanumerik dan tanda sempang",
+	"Already have an account?": "Telah mempunyai akaun?",
+	"an assistant": "seorang pembantu",
+	"and": "dan",
+	"and create a new shared link.": "dan cipta pautan kongsi baharu",
+	"API Base URL": "URL Asas API",
+	"API Key": "Kunci API",
+	"API Key created.": "Kunci API dicipta",
+	"API keys": "Kekunci API",
+	"April": "April",
+	"Archive": "Arkib",
+	"Archive All Chats": "Arkibkan Semua Perbualan",
+	"Archived Chats": "Perbualan yang diarkibkan",
+	"are allowed - Activate this command by typing": "adalah dibenarkan - Aktifkan arahan in dengan menaip",
+	"Are you sure?": "Adakah anda pasti",
+	"Attach file": "Kepilkan Fail",
+	"Attention to detail": "Perincian",
+	"Audio": "Audio",
+	"Audio settings updated successfully": "Tetapan audio berjaya dikemas kini",
+	"August": "Ogos",
+	"Auto-playback response": "Main semula respons secara automatik",
+	"AUTOMATIC1111 Api Auth String": "AUTOMATIC1111 Api Auth String",
+	"AUTOMATIC1111 Base URL": "URL Asas AUTOMATIC1111",
+	"AUTOMATIC1111 Base URL is required.": "URL Asas AUTOMATIC1111 diperlukan.",
+	"available!": "tersedia!",
+	"Back": "Kembali",
+	"Bad Response": "Maklumbalas Salah",
+	"Banners": "Sepanduk",
+	"Base Model (From)": "Model Asas (Dari)",
+	"Batch Size (num_batch)": "Saiz Kumpulan (num_batch)",
+	"before": "sebelum,",
+	"Being lazy": "Menjadi Malas",
+	"Brave Search API Key": "Kunci API Carian Brave",
+	"Bypass SSL verification for Websites": "Pintas pengesahan SSL untuk Laman Web",
+	"Call": "Hubungi",
+	"Call feature is not supported when using Web STT engine": "Ciri panggilan tidak disokong apabila menggunakan enjin Web STT",
+	"Camera": "Kamera",
+	"Cancel": "Batal",
+	"Capabilities": "Keupayaan",
+	"Change Password": "Tukar Kata Laluan",
+	"Chat": "Perbualan",
+	"Chat Background Image": "Imej Latar Belakang Perbualan",
+	"Chat Bubble UI": "Antaramuka Buih Perbualan",
+	"Chat Controls": "Kawalan Perbualan",
+	"Chat direction": "Arah Perbualan",
+	"Chat History": "Sejarah Perbualan",
+	"Chat History is off for this browser.": "Sejarah perbualan dimatikan untuk pelayan web ini",
+	"Chats": "Perbualan",
+	"Check Again": "Semak Kembali",
+	"Check for updates": "Semak kemas kini",
+	"Checking for updates...": "Kemas kini sedang disemak",
+	"Choose a model before saving...": "Pilih model sebelum menyimpan",
+	"Chunk Overlap": "Tindihan 'Çhunk'",
+	"Chunk Params": "Parameter 'Çhunk'",
+	"Chunk Size": "Saiz 'Chunk'",
+	"Citation": "Petikan",
+	"Clear memory": "Kosongkan memori",
+	"Click here for help.": "Klik disini untuk mendapatkan bantuan",
+	"Click here to": "Klik disini untuk",
+	"Click here to download user import template file.": "Klik disini untuk memuat turun fail templat import pengguna",
+	"Click here to select": "Klik disini untuk memilih",
+	"Click here to select a csv file.": "Klik disini untuk memilih fail csv",
+	"Click here to select a py file.": "Klik disini untuk memilih fail py",
+	"Click here to select documents.": "Klik disini untuk memilih dokumen",
+	"click here.": "klik disini.",
+	"Click on the user role button to change a user's role.": "Klik pada butang peranan pengguna untuk menukar peranan pengguna",
+	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Kebenaran untuk menulis di papan klip ditolak. Sila semak tetapan pelayan web anda untuk memberikan akses yang diperlukan",
+	"Clone": "Klon",
+	"Close": "Tutup",
+	"Code formatted successfully": "Kod berjaya diformatkan",
+	"Collection": "Koleksi",
+	"ComfyUI": "ComfyUI",
+	"ComfyUI Base URL": "URL asas ComfyUI",
+	"ComfyUI Base URL is required.": "URL asas ComfyUI diperlukan",
+	"Command": "Arahan",
+	"Concurrent Requests": "Permintaan Serentak",
+	"Confirm": "Sahkan",
+	"Confirm Password": "Sahkan kata laluan",
+	"Confirm your action": "Sahkan tindakan anda",
+	"Connections": "Sambungan",
+	"Contact Admin for WebUI Access": "Hubungi admin untuk akses WebUI",
+	"Content": "Kandungan",
+	"Content Extraction": "Pengekstrakan Kandungan",
+	"Context Length": "Panjang Konteks",
+	"Continue Response": "Teruskan Respons",
+	"Continue with {{provider}}": "Teruskan dengan {{provider}}",
+	"Controls": "Kawalan",
+	"Copied shared chat URL to clipboard!": "Menyalin URL sembang kongsi ke papan klip",
+	"Copy": "Salin",
+	"Copy last code block": "Salin Blok Kod Terakhir",
+	"Copy last response": "Salin Respons Terakhir",
+	"Copy Link": "Salin Pautan",
+	"Copying to clipboard was successful!": "Menyalin ke papan klip berjaya!",
+	"Create a model": "Cipta model",
+	"Create Account": "Cipta Akaun",
+	"Create new key": "Cipta kekunci baharu",
+	"Create new secret key": "Cipta kekunci rahsia baharu",
+	"Created at": "Dicipta di",
+	"Created At": "Dicipta Di",
+	"Created by": "Dicipta oleh",
+	"CSV Import": "Import CSV",
+	"Current Model": "Model Semasa",
+	"Current Password": "Kata laluan semasa",
+	"Custom": "Tersuai",
+	"Customize models for a specific purpose": "Sesuaikan model untuk tujuan tertentu",
+	"Dark": "Gelap",
+	"Dashboard": "Papan Pemuka",
+	"Database": "Pangkalan Data",
+	"December": "Disember",
+	"Default": "Lalai",
+	"Default (Automatic1111)": "Lalai (Automatic1111)",
+	"Default (SentenceTransformers)": "Lalai (SentenceTransformers)",
+	"Default Model": "Model Lalai",
+	"Default model updated": "Model lalai dikemas kini",
+	"Default Prompt Suggestions": "Cadangan Gesaan Lalai",
+	"Default User Role": "Peranan Pengguna Lalai",
+	"delete": "padam",
+	"Delete": "Padam",
+	"Delete a model": "Padam Model",
+	"Delete All Chats": "Padam Semua Perbualan",
+	"Delete chat": "Padam perbualan",
+	"Delete Chat": "Padam Perbualan",
+	"Delete chat?": "Padam perbualan?",
+	"Delete Doc": "Padam Dokumen",
+	"Delete function?": "Padam fungsi?",
+	"Delete prompt?": "Padam Gesaan?",
+	"delete this link": "Padam pautan ini?",
+	"Delete tool?": "Padam alat?",
+	"Delete User": "Padam Pengguna",
+	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} dipadam",
+	"Deleted {{name}}": "{{name}} dipadam",
+	"Description": "Penerangan",
+	"Didn't fully follow instructions": "Tidak mengikut arahan sepenuhnya",
+	"Disabled": "Dilumpuhkan",
+	"Discover a function": "Temui fungsi",
+	"Discover a model": "Temui model",
+	"Discover a prompt": "Temui gesaan",
+	"Discover a tool": "Temui alat",
+	"Discover, download, and explore custom functions": "Temui, muat turun dan teroka fungsi tersuai",
+	"Discover, download, and explore custom prompts": "Temui, muat turun dan teroka gesaan tersuai",
+	"Discover, download, and explore custom tools": "Temui, muat turun dan teroka alat tersuai",
+	"Discover, download, and explore model presets": "Temui, muat turun dan teroka model pratetap",
+	"Dismissible": "Diketepikan",
+	"Display Emoji in Call": "Paparkan Emoji dalam Panggilan",
+	"Display the username instead of You in the Chat": "aparkan nama pengguna dan bukannya 'Anda' dalam Sembang",
+	"Do not install functions from sources you do not fully trust.": "Jangan pasang fungsi daripada sumber yang anda tidak percayai sepenuhnya.",
+	"Do not install tools from sources you do not fully trust.": "Jangan pasang alat daripada sumber yang anda tidak percaya sepenuhnya.",
+	"Document": "Dokumen",
+	"Document Settings": "Tetapan Dokumen",
+	"Documentation": "Dokumentasi",
+	"Documents": "Dokumen",
+	"does not make any external connections, and your data stays securely on your locally hosted server.": "tidak membuat sebarang sambungan luaran, dan data anda kekal selamat pada pelayan yang dihoskan ditempat anda",
+	"Don't Allow": "Tidak Dibenarkan",
+	"Don't have an account?": "Anda tidak mempunyai akaun?",
+	"don't install random functions from sources you don't trust.": "jangan pasang mana-mana fungsi daripada sumber yang anda tidak percayai.",
+	"don't install random tools from sources you don't trust.": "jangan pasang mana-mana alat daripada sumber yang anda tidak percayai.",
+	"Don't like the style": "Tidak suka gaya ini",
+	"Done": "Selesai",
+	"Download": "Muat Turun",
+	"Download canceled": "Muat Turun dibatalkan",
+	"Download Database": "Muat turun Pangkalan Data",
+	"Drop any files here to add to the conversation": "Letakkan mana-mana fail di sini untuk ditambahkan pada perbualan",
+	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "cth '30s','10m'. Unit masa yang sah ialah 's', 'm', 'h'.",
+	"Edit": "Edit",
+	"Edit Doc": "Edit Dokumen",
+	"Edit Memory": "Edit Memori",
+	"Edit User": "Edit Penggunal",
+	"ElevenLabs": "ElevenLabs",
+	"Email": "E-mel",
+	"Embedding Batch Size": "Membenamkan Saiz Kelompok",
+	"Embedding Model": "Model Benamkan",
+	"Embedding Model Engine": "Enjin Model Benamkan",
+	"Embedding model set to \"{{embedding_model}}\"": "Model Benamkan ditetapkan kepada \"{{embedding_model}}\"",
+	"Enable Chat History": "Benarkan Sejarah Perbualan",
+	"Enable Community Sharing": "Benarkan Perkongsian Komunity",
+	"Enable New Sign Ups": "Benarkan Pendaftaran Baharu",
+	"Enable Web Search": "Benarkan Carian Web",
+	"Enabled": "Dibenarkan",
+	"Engine": "Enjin",
+	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "astikan fail CSV anda mengandungi 4 lajur dalam susunan ini: Nama, E-mel, Kata Laluan, Peranan.",
+	"Enter {{role}} message here": "Masukkan mesej {{role}} di sini",
+	"Enter a detail about yourself for your LLMs to recall": "Masukkan butiran tentang diri anda untuk diingati oleh LLM anda",
+	"Enter api auth string (e.g. username:password)": "Masukkan kekunci auth api ( cth nama pengguna:kata laluan )",
+	"Enter Brave Search API Key": "Masukkan Kekunci API carian Brave",
+	"Enter Chunk Overlap": "Masukkan Tindihan 'Chunk'",
+	"Enter Chunk Size": "Masukkan Saiz 'Chunk'",
+	"Enter Github Raw URL": "Masukkan URL 'Github Raw'",
+	"Enter Google PSE API Key": "Masukkan kunci API Google PSE",
+	"Enter Google PSE Engine Id": "Masukkan Id Enjin Google PSE",
+	"Enter Image Size (e.g. 512x512)": "Masukkan Saiz Imej (cth 512x512)",
+	"Enter language codes": "Masukkan kod bahasa",
+	"Enter model tag (e.g. {{modelTag}})": "Masukkan tag model (cth {{ modelTag }})",
+	"Enter Number of Steps (e.g. 50)": "Masukkan Bilangan Langkah (cth 50)",
+	"Enter Score": "Masukkan Skor",
+	"Enter Searxng Query URL": "Masukkan URL 'Searxng Query'",
+	"Enter Serper API Key": "Masukkan Kunci API Serper",
+	"Enter Serply API Key": "Masukkan Kunci API Serply",
+	"Enter Serpstack API Key": "Masukkan Kunci API Serpstack",
+	"Enter stop sequence": "Masukkan urutan hentian",
+	"Enter system prompt": "Masukkan gesaan sistem",
+	"Enter Tavily API Key": "Masukkan Kunci API Tavily",
+	"Enter Tika Server URL": "Masukkan URL Pelayan Tika",
+	"Enter Top K": "Masukkan 'Top K'",
+	"Enter URL (e.g. http://127.0.0.1:7860/)": "Masukkan URL (cth http://127.0.0.1:7860/)",
+	"Enter URL (e.g. http://localhost:11434)": "Masukkan URL (cth http://localhost:11434)",
+	"Enter Your Email": "Masukkan E-mel Anda",
+	"Enter Your Full Name": "Masukkan Nama Penuh Anda",
+	"Enter your message": "Masukkan mesej anda",
+	"Enter Your Password": "Masukkan Kata Laluan Anda",
+	"Enter Your Role": "Masukkan Peranan Anda",
+	"Error": "Ralat",
+	"Experimental": "Percubaan",
+	"Export": "Eksport",
+	"Export All Chats (All Users)": "Eksport Semua Perbualan (Semua Pengguna)",
+	"Export chat (.json)": "Eksport perbualan (.json)",
+	"Export Chats": "Eksport Perbualan",
+	"Export Documents Mapping": "Eksport Pemetaan Dokumen",
+	"Export Functions": "Eksport 'Fungsi'",
+	"Export LiteLLM config.yaml": "Eksport LiteLLM config.yaml",
+	"Export Models": "Eksport Model",
+	"Export Prompts": "Eksport Gesaan",
+	"Export Tools": "Eksport Alat",
+	"External Models": "Model Luaran",
+	"Failed to create API Key.": "Gagal mencipta kekunci API",
+	"Failed to read clipboard contents": "Gagal membaca konten papan klip",
+	"Failed to update settings": "Gagal mengemaskini tetapan",
+	"February": "Febuari",
+	"Feel free to add specific details": "Jangan ragu untuk menambah butiran khusus",
+	"File": "Fail",
+	"File Mode": "Mod Fail",
+	"File not found.": "Fail tidak dijumpai",
+	"Files": "Fail-Fail",
+	"Filter is now globally disabled": "Tapisan kini dilumpuhkan secara global",
+	"Filter is now globally enabled": "Tapisan kini dibenarkan secara global",
+	"Filters": "Tapisan",
+	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Peniruan cap jari dikesan, tidak dapat menggunakan nama pendek sebagai avatar. Lalai kepada imej profail asal",
+	"Fluidly stream large external response chunks": "Strim 'chunks' respons luaran yang besar dengan lancar",
+	"Focus chat input": "Fokus kepada input perbualan",
+	"Followed instructions perfectly": "Mengikut arahan dengan sempurna",
+	"Form": "Borang",
+	"Format your variables using square brackets like this:": "Formatkan pembolehubah anda menggunakan kurungan segi empat sama seperti ini:",
+	"Frequency Penalty": "Penalti Kekerapan",
+	"Function created successfully": "Fungsi berjaya dibuat",
+	"Function deleted successfully": "Fungsi berjaya dipadamkan",
+	"Function Description (e.g. A filter to remove profanity from text)": "Perihalan Fungsi (cth. Tapisan untuk membuang kata-kata kotor daripada teks)",
+	"Function ID (e.g. my_filter)": "ID Fungsi (cth. my_filter)",
+	"Function is now globally disabled": "Fungsi kini dilumpuhkan secara global",
+	"Function is now globally enabled": "Fungsi kini dibenarkan secara global",
+	"Function Name (e.g. My Filter)": "Nama Fungsi (cth. 'My Filter')",
+	"Function updated successfully": "Fungsi berjaya dikemaskini",
+	"Functions": "Fungsi",
+	"Functions allow arbitrary code execution": "Fungsi membenarkan pelaksanaan kod sewenang-wenangnya",
+	"Functions allow arbitrary code execution.": "Fungsi membenarkan pelaksanaan kod sewenang-wenangnya.",
+	"Functions imported successfully": "Fungsi berjaya diimport",
+	"General": "Umum",
+	"General Settings": "Tetapan Umum",
+	"Generate Image": "Jana Imej",
+	"Generating search query": "Jana pertanyaan carian",
+	"Generation Info": "Maklumat Penjanaan",
+	"Get up and running with": "Bangun dan berlari dengan",
+	"Global": "Global",
+	"Good Response": "Respons Baik",
+	"Google PSE API Key": "Kunci API Google PSE",
+	"Google PSE Engine Id": "ID Enjin Google PSE",
+	"h:mm a": "h:mm a",
+	"has no conversations.": "tidak mempunyai perbualan.",
+	"Hello, {{name}}": "Hello, {{name}}",
+	"Help": "Bantuan",
+	"Hide": "Sembunyi",
+	"Hide Model": "Sembunyikan Model",
+	"How can I help you today?": "Bagaimana saya boleh membantu anda hari ini?",
+	"Hybrid Search": "Carian Hibrid",
+	"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "Saya mengakui bahawa saya telah membaca dan saya memahami implikasi tindakan saya. Saya sedar tentang risiko yang berkaitan dengan melaksanakan kod sewenang-wenangnya dan saya telah mengesahkan kebolehpercayaan sumber tersebut.",
+	"Image Generation (Experimental)": "Penjanaan Imej (Percubaan)",
+	"Image Generation Engine": "Enjin Penjanaan Imej",
+	"Image Settings": "Tetapan Imej",
+	"Images": "Imej",
+	"Import Chats": "Import Perbualan",
+	"Import Documents Mapping": "Import Pemetaan Dokumen",
+	"Import Functions": "Import Fungsi",
+	"Import Models": "Import Model",
+	"Import Prompts": "Import Gesaan",
+	"Import Tools": "Import Alat",
+	"Include `--api-auth` flag when running stable-diffusion-webui": "Sertakan bendera `-- api -auth` semasa menjalankan stable-diffusion-webui ",
+	"Include `--api` flag when running stable-diffusion-webui": "Sertakan bendera `-- api ` semasa menjalankan stable-diffusion-webui",
+	"Info": "Maklumat",
+	"Input commands": "Masukkan Arahan",
+	"Install from Github URL": "Pasang daripada URL Github",
+	"Instant Auto-Send After Voice Transcription": "Hantar Secara Automatik Dengan Segera Selepas Transkripsi Suara",
+	"Interface": "Ataramuka",
+	"Invalid Tag": "Tag tidak sah",
+	"January": "Januari",
+	"join our Discord for help.": "sertai Discord kami untuk mendapatkan bantuan.",
+	"JSON": "JSON",
+	"JSON Preview": "Pratonton JSON",
+	"July": "Julai",
+	"June": "Jun",
+	"JWT Expiration": "Tempoh Tamat JWT",
+	"JWT Token": "Token JWT",
+	"Keep Alive": "Kekalkan Hidup",
+	"Keyboard shortcuts": "Pintasan papan kekunci",
+	"Knowledge": "Pengetahuan",
+	"Language": "Bahasa",
+	"large language models, locally.": "model bahasa besar, tempatan.",
+	"Last Active": "Dilihat aktif terakhir pada",
+	"Last Modified": "Kemaskini terakhir pada",
+	"Light": "Cerah",
+	"Listening...": "Mendengar...",
+	"LLMs can make mistakes. Verify important information.": "LLM boleh membuat kesilapan. Sahkan maklumat penting",
+	"Local Models": "Model Tempatan",
+	"LTR": "LTR",
+	"Made by OpenWebUI Community": "Dicipta oleh Komunity OpenWebUI",
+	"Make sure to enclose them with": "Pastikan untuk melampirkannya dengan",
+	"Manage": "Mengurus",
+	"Manage Models": "Urus Model",
+	"Manage Ollama Models": "Urus Model Ollama",
+	"Manage Pipelines": "Urus 'Pipelines'",
+	"March": "Mac",
+	"Max Tokens (num_predict)": "Token Maksimum ( num_predict )",
+	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Maksimum 3 model boleh dimuat turun serentak. Sila cuba sebentar lagi.",
+	"May": "Mei",
+	"Memories accessible by LLMs will be shown here.": "Memori yang boleh diakses oleh LLM akan ditunjukkan di sini.",
+	"Memory": "Memori",
+	"Memory added successfully": "Memori berjaya ditambah",
+	"Memory cleared successfully": "Memori berjaya dikosongkan",
+	"Memory deleted successfully": "Memori berjaya dihapuskan",
+	"Memory updated successfully": "Memori berjaya dikemaskini",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Mesej yang anda hantar selepas membuat pautan anda tidak akan dikongsi. Pengguna dengan URL akan dapat melihat perbualan yang dikongsi.",
+	"Min P": "P Minimum",
+	"Minimum Score": "Skor Minimum",
+	"Mirostat": "Mirostat",
+	"Mirostat Eta": "Mirostat Eta",
+	"Mirostat Tau": "Mirostat Tau",
+	"MMMM DD, YYYY": "DD MMMM YYYY",
+	"MMMM DD, YYYY HH:mm": "DD MMMM YYYY HH:mm",
+	"MMMM DD, YYYY hh:mm:ss A": "DD MMMM YYYY HH:mm:ss A",
+	"Model '{{modelName}}' has been successfully downloaded.": "Model '{{ modelName }}' telah berjaya dimuat turun.",
+	"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{ modelTag }}' sudah dalam baris gilir untuk dimuat turun.",
+	"Model {{modelId}} not found": "Model {{ modelId }} tidak dijumpai",
+	"Model {{modelName}} is not vision capable": "Model {{ modelName }} tidak mempunyai keupayaan penglihatan",
+	"Model {{name}} is now {{status}}": "Model {{name}} kini {{status}}",
+	"Model created successfully!": "Model berjaya dibuat!",
+	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Laluan sistem fail model dikesan. Nama pendek model diperlukan untuk kemas kini, tidak boleh diteruskan.",
+	"Model ID": "ID Model",
+	"Model not selected": "Model tidak dipilih",
+	"Model Params": "Model Params",
+	"Model updated successfully": "Model berjaya dikemas kini",
+	"Model Whitelisting": "Senarai Putih Model",
+	"Model(s) Whitelisted": "Model Disenarai Putih",
+	"Modelfile Content": "Kandungan Modelfail",
+	"Models": "Model",
+	"More": "Lagi",
+	"Name": "Nama",
+	"Name Tag": "Nama Tag",
+	"Name your model": "Namakan Model Anda",
+	"New Chat": "Perbualan Baru",
+	"New Password": "Kata Laluan Baru",
+	"No content to speak": "Tiada kandungan untuk bercakap",
+	"No documents found": "Tiada dokumen ditemui",
+	"No file selected": "Tiada fail dipilih",
+	"No results found": "Tiada keputusan dijumpai",
+	"No search query generated": "Tiada pertanyaan carian dijana",
+	"No source available": "Tiada sumber tersedia",
+	"No valves to update": "Tiada 'valve' untuk dikemas kini",
+	"None": "Tiada",
+	"Not factually correct": "Tidak tepat secara fakta",
+	"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Jika anda menetapkan skor minimum, carian hanya akan mengembalikan dokumen dengan skor lebih besar daripada atau sama dengan skor minimum.",
+	"Notifications": "Pemberitahuan",
+	"November": "November",
+	"num_thread (Ollama)": "num_thread (Ollama)",
+	"OAuth ID": "ID OAuth",
+	"October": "Oktober",
+	"Off": "Mati",
+	"Okay, Let's Go!": "Baiklah, Jom!",
+	"OLED Dark": "OLED Gelap",
+	"Ollama": "Ollama",
+	"Ollama API": "API Ollama",
+	"Ollama API disabled": "API Ollama dilumpuhkan",
+	"Ollama API is disabled": "API Ollama telah dilumpuhkan",
+	"Ollama Version": "Versi Ollama",
+	"On": "Hidup",
+	"Only": "Hanya",
+	"Only alphanumeric characters and hyphens are allowed in the command string.": "Hanya aksara alfanumerik dan sempang dibenarkan dalam rentetan arahan.",
+	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Maaf! Harap Bersabar. Fail anda masih dalam ketuhar pemprosesan. Kami sedang memasaknya dengan sempurna. Harap bersabar dan kami akan memberitahu anda sebaik sahaja ia siap.",
+	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Maaf, didapati URL tidak sah. Sila semak semula dan cuba lagi.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "Maaf, terdapat ralat dalam respons sebelumnya. Sila cuba lagi atau hubungi pentadbir",
+	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Maaf, Anda menggunakan kaedah yang tidak disokong (bahagian 'frontend' sahaja). Sila sediakan WebUI dari 'backend'.",
+	"Open AI (Dall-E)": "Open AI (Dall-E)",
+	"Open new chat": "Buka perbualan baru",
+	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) adalah lebih rendah daripada versi yang diperlukan iaitu (v{{REQUIRED_VERSION}})",
+	"OpenAI": "OpenAI",
+	"OpenAI API": "API OpenAI",
+	"OpenAI API Config": "Tetapan API OpenAI",
+	"OpenAI API Key is required.": "Kekunci API OpenAI diperlukan",
+	"OpenAI URL/Key required.": "URL/Kekunci OpenAI diperlukan",
+	"or": "atau",
+	"Other": "Lain-lain",
+	"Password": "Kata Laluan",
+	"PDF document (.pdf)": "Dokumen PDF (.pdf)",
+	"PDF Extract Images (OCR)": "Imej Ekstrak PDF (OCR)",
+	"pending": "tertunda",
+	"Permission denied when accessing media devices": "Tidak mendapat kebenaran apabila cuba mengakses peranti media",
+	"Permission denied when accessing microphone": "Tidak mendapat kebenaran apabila cuba mengakses mikrofon",
+	"Permission denied when accessing microphone: {{error}}": "Tidak mendapat kebenaran apabila cuba mengakses mikrofon: {{error}}",
+	"Personalization": "Personalisasi",
+	"Pin": "Pin",
+	"Pinned": "Disemat",
+	"Pipeline deleted successfully": "'Pipeline' berjaya dipadam",
+	"Pipeline downloaded successfully": "'Pipeline' berjaya dimuat turun",
+	"Pipelines": "'Pipeline'",
+	"Pipelines Not Detected": "'Pipeline' tidak ditemui",
+	"Pipelines Valves": "'Pipeline Valves'",
+	"Plain text (.txt)": "Teks biasa (.txt)",
+	"Playground": "Taman Permainan",
+	"Please carefully review the following warnings:": "Sila semak dengan teliti amaran berikut:",
+	"Positive attitude": "Sikap positif",
+	"Previous 30 days": "30 hari sebelumnya",
+	"Previous 7 days": "7 hari sebelumnya",
+	"Profile Image": "Imej Profail",
+	"Prompt": "Gesaan",
+	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Gesaan (cth Beritahu saya fakta yang menyeronokkan tentang Kesultanan Melaka)",
+	"Prompt Content": "Kandungan Gesaan",
+	"Prompt suggestions": "Cadangan Gesaan",
+	"Prompts": "Gesaan",
+	"Pull \"{{searchValue}}\" from Ollama.com": "Tarik \"{{ searchValue }}\" daripada Ollama.com",
+	"Pull a model from Ollama.com": "Tarik model dari Ollama.com",
+	"Query Params": "'Query Params'",
+	"RAG Template": "Templat RAG",
+	"Read Aloud": "Baca dengan lantang",
+	"Record voice": "Rakam suara",
+	"Redirecting you to OpenWebUI Community": "Membawa anda ke Komuniti OpenWebUI",
+	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Rujuk diri anda sebagai \"User\" (cth, \"Pengguna sedang belajar bahasa Sepanyol\")",
+	"Refused when it shouldn't have": "Menolak dimana ia tidak sepatutnya",
+	"Regenerate": "Jana semula",
+	"Release Notes": "Nota Keluaran",
+	"Remove": "Hapuskan",
+	"Remove Model": "Hapuskan Model",
+	"Rename": "Namakan Semula",
+	"Repeat Last N": "Ulang N Terakhir",
+	"Request Mode": "Mod Permintaan",
+	"Reranking Model": "Model 'Reranking'",
+	"Reranking model disabled": "Model 'Reranking' dilumpuhkan",
+	"Reranking model set to \"{{reranking_model}}\"": "Model 'Reranking' ditetapkan kepada \"{{reranking_model}}\"",
+	"Reset": "Tetapkan Semula",
+	"Reset Upload Directory": "Tetapkan Semula Direktori Muat Naik",
+	"Reset Vector Storage": "Tetapkan Semula Storan Vektor",
+	"Response AutoCopy to Clipboard": "Salin Response secara Automatik ke Papan Klip",
+	"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Pemberitahuan respons tidak boleh diaktifkan kerana kebenaran tapak web tidak diberi. Sila lawati tetapan pelayar web anda untuk memberikan akses yang diperlukan.",
+	"Role": "Peranan",
+	"Rosé Pine": "Rosé Pine",
+	"Rosé Pine Dawn": "Rosé Pine Dawn",
+	"RTL": "RTL",
+	"Run Llama 2, Code Llama, and other models. Customize and create your own.": "Jalankan Llama 2, Code Llama dan model lain. Sesuaikan dan buat sendiri.",
+	"Running": "Sedang dijalankan",
+	"Save": "Simpan",
+	"Save & Create": "Simpan & Cipta",
+	"Save & Update": "Simpan & Kemas Kini",
+	"Save Tag": "Simpan Tag",
+	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Penyimpanan log perbualan terus ke storan pelayan web anda tidak lagi disokong. Sila luangkan sedikit masa untuk memuat turun dan memadam log perbualan anda dengan mengklik butang di bawah. Jangan risau, anda boleh mengimport semula log perbualan anda dengan mudah melalui 'backend'",
+	"Scan": "Imbas",
+	"Scan complete!": "Imbasan selesai!",
+	"Scan for documents from {{path}}": "Imbas untuk dokumen dari {{path}}",
+	"Scroll to bottom when switching between branches": "",
+	"Search": "Carian",
+	"Search a model": "Cari Model",
+	"Search Chats": "Cari Perbualan",
+	"Search Documents": "Carian Dokumen",
+	"Search Functions": "Carian Fungsi",
+	"Search Models": "Carian Model",
+	"Search Prompts": "Carian Gesaan",
+	"Search Query Generation Prompt": "Carian Penjanaan Pertanyaan Gesaan",
+	"Search Query Generation Prompt Length Threshold": "Had Panjang Gesaan Penjanaan Pertanyaan Carian",
+	"Search Result Count": "Kiraan Hasil Carian",
+	"Search Tools": "Alat Carian",
+	"Searched {{count}} sites_one": "Mencari {{count}} sites_one",
+	"Searched {{count}} sites_other": "Mencari {{count}} tapak_lain",
+	"Searching \"{{searchQuery}}\"": "encari \"{{ searchQuery }}\"",
+	"Searxng Query URL": "URL Pertanyaan Searxng",
+	"See readme.md for instructions": "Lihat readme.md untuk arahan",
+	"See what's new": "Lihat apa yang terbaru",
+	"Seed": "Benih",
+	"Select a base model": "Pilih model asas",
+	"Select a engine": "Pilih enjin",
+	"Select a function": "Pilih fungsi",
+	"Select a mode": "Pilih Mod",
+	"Select a model": "Pilih model",
+	"Select a pipeline": "Pilih 'pipeline'",
+	"Select a pipeline url": "Pilih url 'pipeline'",
+	"Select a tool": "Pilih alat",
+	"Select an Ollama instance": "Pilih Ollama contoh",
+	"Select Documents": "Pilih Dokumen",
+	"Select model": "Pilih model",
+	"Select only one model to call": "Pilih hanya satu model untuk dipanggil",
+	"Selected model(s) do not support image inputs": "Model dipilih tidak menyokong input imej",
+	"Send": "Hantar",
+	"Send a Message": "Hantar Pesanan",
+	"Send message": "Hantar pesanan",
+	"September": "September",
+	"Serper API Key": "Kunci API Serper",
+	"Serply API Key": "Kunci API Serply",
+	"Serpstack API Key": "Kunci API Serpstack",
+	"Server connection verified": "Sambungan pelayan disahkan",
+	"Set as default": "Tetapkan sebagai lalai",
+	"Set Default Model": "Tetapkan Model Lalai",
+	"Set embedding model (e.g. {{model}})": "Tetapkan model benamkan (cth {{model}})",
+	"Set Image Size": "Tetapkan saiz imej",
+	"Set reranking model (e.g. {{model}})": "Tetapkan model 'reranking' (cth {{model}})",
+	"Set Steps": "tapkan Langkah",
+	"Set Task Model": "Tetapkan Model Tugasan",
+	"Set Voice": "Tetapan Suara",
+	"Settings": "Tetapan",
+	"Settings saved successfully!": "Tetapan berjaya disimpan!",
+	"Settings updated successfully": "Tetapan berjaya dikemas kini",
+	"Share": "Kongsi",
+	"Share Chat": "Kongsi Perbualan",
+	"Share to OpenWebUI Community": "Kongsi kepada Komuniti OpenWebUI",
+	"short-summary": "ringkasan",
+	"Show": "Tunjukkan",
+	"Show Admin Details in Account Pending Overlay": "Tunjukkan Butiran Pentadbir dalam Akaun Menunggu Tindanan",
+	"Show Model": "Tunjukkan Model",
+	"Show shortcuts": "Tunjukkan pintasan",
+	"Show your support!": "Tunjukkan sokongan anda!",
+	"Showcased creativity": "eativiti yang dipamerkan",
+	"Sign in": "Daftar masuk",
+	"Sign Out": "Daftar keluar",
+	"Sign up": "Daftar",
+	"Signing in": "Sedang Mendaftar",
+	"Source": "Sumber",
+	"Speech recognition error: {{error}}": "Ralat pengecaman pertuturan: {{error}}",
+	"Speech-to-Text Engine": "Enjin Ucapan-ke-Teks",
+	"Stop Sequence": "Jujukan Henti",
+	"STT Model": "Model STT",
+	"STT Settings": "Tetapan STT",
+	"Submit": "Hantar",
+	"Subtitle (e.g. about the Roman Empire)": "Sari kata (cth tentang Kesultanan Melaka)",
+	"Success": "Berjaya",
+	"Successfully updated.": "Berjaya Dikemaskini",
+	"Suggested": "Cadangan",
+	"Support": "Sokongan",
+	"Support this plugin:": "Sokong plugin ini",
+	"System": "Sistem",
+	"System Prompt": "Gesaan Sistem",
+	"Tags": "Tag",
+	"Tap to interrupt": "Sentuh untuk mengganggu",
+	"Tavily API Key": "Kunci API Tavily",
+	"Tell us more:": "Beritahu kami lebih lanjut",
+	"Temperature": "Suhu",
+	"Template": "Templat",
+	"Text Completion": "Penyiapan Teks",
+	"Text-to-Speech Engine": "Enjin Teks-ke-Ucapan",
+	"Tfs Z": "Tfs Z",
+	"Thanks for your feedback!": "Terima kasih atas maklum balas anda!",
+	"The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "Pembangun di sebalik 'plugin' ini adalah sukarelawan yang bersemangat daripada komuniti. Jika anda mendapati 'plugin' ini membantu, sila pertimbangkan untuk menyumbang kepada pembangunannya.",
+	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "Skor hendaklah berada diantara 0.0 (0%) dan 1.0 (100%).",
+	"Theme": "Tema",
+	"Thinking...": "Berfikir...",
+	"This action cannot be undone. Do you wish to continue?": "Tindakan ini tidak boleh diubah semula kepada asal. Adakah anda ingin teruskan",
+	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Ini akan memastikan bahawa perbualan berharga anda disimpan dengan selamat ke pangkalan data 'backend' anda. Terima kasih!",
+	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "ni adalah ciri percubaan, ia mungkin tidak berfungsi seperti yang diharapkan dan tertakluk kepada perubahan pada bila-bila masa.",
+	"This setting does not sync across browsers or devices.": "Tetapan ini tidak menyegerak merentas pelayar web atau peranti.",
+	"This will delete": "Ini akan memadam",
+	"Thorough explanation": "Penjelasan menyeluruh",
+	"Tika": "Tika",
+	"Tika Server URL required.": "URL Pelayan Tika diperlukan.",
+	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Petua: Kemas kini berbilang slot pembolehubah secara berturut-turut dengan menekan kekunci tab dalam input perbualan selepas setiap penggantian.",
+	"Title": "Tajuk",
+	"Title (e.g. Tell me a fun fact)": "Tajuk (cth Beritahu saya fakta yang menyeronokkan)",
+	"Title Auto-Generation": "Penjanaan Auto Tajuk",
+	"Title cannot be an empty string.": "Tajuk tidak boleh menjadi rentetan kosong",
+	"Title Generation Prompt": "Gesaan Penjanaan Tajuk",
+	"to": "ke",
+	"To access the available model names for downloading,": "Untuk mengakses nama model yang tersedia untuk dimuat turun,",
+	"To access the GGUF models available for downloading,": "Untuk mengakses model GGUF yang tersedia untuk dimuat turun,",
+	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Untuk mengakses WebUI , sila hubungi pentadbir. Pentadbir boleh menguruskan status pengguna daripada Panel Pentadbiran",
+	"To add documents here, upload them to the \"Documents\" workspace first.": "Untuk menambah dokumen di sini, muat naiknya ke ruang kerja \"Documents\" dahulu.",
+	"to chat input.": "untuk input perbualan.",
+	"To select actions here, add them to the \"Functions\" workspace first.": "Untuk memilih tindakan di sini, tambahkannya pada ruang kerja \"Functions\" dahulu.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "Untuk memilih tapisan di sini, tambahkannya pada ruang kerja \"Functions\" dahulu.",
+	"To select toolkits here, add them to the \"Tools\" workspace first.": "Untuk memilih kit alatan di sini, tambahkannya pada ruang kerja \"Tools\" dahulu.",
+	"Today": "Hari Ini",
+	"Toggle settings": "Suis Tetapan ",
+	"Toggle sidebar": "Suis Bar Sisi",
+	"Tokens To Keep On Context Refresh (num_keep)": "Token Untuk Teruskan Dalam Muat Semula Konteks ( num_keep )",
+	"Tool created successfully": "Alat berjaya dibuat",
+	"Tool deleted successfully": "Alat berjaya dipadamkan",
+	"Tool imported successfully": "Alat berjaya diimport",
+	"Tool updated successfully": "Alat berjaya dikemas kini",
+	"Toolkit Description (e.g. A toolkit for performing various operations)": "Perihalan Kit Alatan (cth. Kit alatan untuk melaksanakan pelbagai operasi)",
+	"Toolkit ID (e.g. my_toolkit)": "ID Kit Alatan (cth my_toolkit)",
+	"Toolkit Name (e.g. My ToolKit)": "Nama Kit Alatan (cth My ToolKit)",
+	"Tools": "Alatan",
+	"Tools are a function calling system with arbitrary code execution": "Alatan ialah sistem panggilan fungsi dengan pelaksanaan kod sewenang-wenangnya",
+	"Tools have a function calling system that allows arbitrary code execution": "Alatan mempunyai sistem panggilan fungsi yang membolehkan pelaksanaan kod sewenang-wenangnya",
+	"Tools have a function calling system that allows arbitrary code execution.": "Alatan mempunyai sistem panggilan fungsi yang membolehkan pelaksanaan kod sewenang-wenangnya.",
+	"Top K": "'Top K'",
+	"Top P": "'Top P'",
+	"Trouble accessing Ollama?": "Masalah mengakses Ollama?",
+	"TTS Model": "Model TTS",
+	"TTS Settings": "Tetapan TTS",
+	"TTS Voice": "Suara TTS",
+	"Type": "jenis",
+	"Type Hugging Face Resolve (Download) URL": "Taip URL 'Hugging Face Resolve (Download)'",
+	"Uh-oh! There was an issue connecting to {{provider}}.": "Maaf! Terdapat masalah menyambung ke {{provider}}.",
+	"UI": "UI",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "Jenis fail tidak diketahui '{{ file_type }}'. Teruskan dengan muat naik fail juga.",
+	"Unpin": "Nyahsematkan",
+	"Update": "Kemaskini",
+	"Update and Copy Link": "Kemaskini dan salin pautan",
+	"Update password": "Kemaskini Kata Laluan",
+	"Updated at": "Dikemaskini pada",
+	"Upload": "Muatnaik",
+	"Upload a GGUF model": "Muatnaik model GGUF",
+	"Upload Files": "Muatnaik fail",
+	"Upload Pipeline": "Muatnaik 'Pipeline'",
+	"Upload Progress": "Kemajuan Muatnaik",
+	"URL Mode": "Mod URL",
+	"Use '#' in the prompt input to load and select your documents.": "Gunakan '#' dalam input gesaan untuk memuatkan dan memilih dokumen anda",
+	"Use Gravatar": "Gunakan Gravatar",
+	"Use Initials": "Gunakan nama pendek",
+	"use_mlock (Ollama)": "use_mlock (Ollama)",
+	"use_mmap (Ollama)": "se_mmap (Ollama)",
+	"user": "pengguna",
+	"User location successfully retrieved.": "Lokasi pengguna berjaya diambil.",
+	"User Permissions": "Kebenaran Pengguna",
+	"Users": "Pengguna",
+	"Utilize": "Gunakan",
+	"Valid time units:": "Unit masa yang sah:",
+	"Valves": "'Valves'",
+	"Valves updated": "'Valves' dikemaskini",
+	"Valves updated successfully": "'Valves' berjaya dikemaskini",
+	"variable": "pembolehubah",
+	"variable to have them replaced with clipboard content.": "pembolehubah untuk ia digantikan dengan kandungan papan klip.",
+	"Version": "Versi",
+	"Voice": "Suara",
+	"Warning": "Amaran",
+	"Warning:": "Amaran:",
+	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Amaran: Jika anda mengemas kini atau menukar model benam anda, anda perlu mengimport semula semua dokumen.",
+	"Web": "Web",
+	"Web API": "API Web",
+	"Web Loader Settings": "Tetapan Pemuat Web",
+	"Web Params": "Parameter Web",
+	"Web Search": "Carian Web",
+	"Web Search Engine": "Enjin Carian Web",
+	"Webhook URL": "URL 'Webhook'",
+	"WebUI Settings": "Tetapan WebUI",
+	"WebUI will make requests to": "WebUI akan membuat permintaan kepada",
+	"What’s New in": "Apakah yang terbaru dalam",
+	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Apabila sejarah dimatikan, perbualan baharu pada pelayan web ini tidak akan muncul dalam sejarah pada mana-mana peranti anda.",
+	"Whisper (Local)": "Whisper (Local)",
+	"Widescreen Mode": "Mod Skrin Lebar",
+	"Workspace": "Ruang Kerja",
+	"Write a prompt suggestion (e.g. Who are you?)": "Tulis cadangan gesaan (cth Siapakah anda?)",
+	"Write a summary in 50 words that summarizes [topic or keyword].": "Tulis ringkasan dalam 50 patah perkataan yang meringkaskan [topik atau kata kunci].",
+	"Yesterday": "Semalam",
+	"You": "Anda",
+	"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Anda boleh memperibadikan interaksi anda dengan LLM dengan menambahkan memori melalui butang 'Urus' di bawah, menjadikannya lebih membantu dan disesuaikan dengan anda.",
+	"You cannot clone a base model": "Anda tidak boleh mengklon model asas",
+	"You have no archived conversations.": "Anda tidak mempunyai perbualan yang diarkibkan",
+	"You have shared this chat": "Anda telah berkongsi perbualan ini",
+	"You're a helpful assistant.": "Anda seorang pembantu yang bagus",
+	"You're now logged in.": "Anda kini telah log masuk.",
+	"Your account status is currently pending activation.": "Status akaun anda ialah sedang menunggu pengaktifan.",
+	"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "Seluruh sumbangan anda akan dihantar terus kepada pembangun 'plugin'; Open WebUI tidak mengambil sebarang peratusan keuntungan daripadanya. Walau bagaimanapun, platform pembiayaan yang dipilih mungkin mempunyai caj tersendiri.",
+	"Youtube": "Youtube",
+	"Youtube Loader Settings": "Tetapan Pemuat Youtube"
+}

+ 1 - 0
src/lib/i18n/locales/nb-NO/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Skann",
 	"Scan complete!": "Skanning fullført!",
 	"Scan for documents from {{path}}": "Skann etter dokumenter fra {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Søk",
 	"Search a model": "Søk en modell",
 	"Search Chats": "Søk chatter",

+ 1 - 0
src/lib/i18n/locales/nl-NL/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Scan",
 	"Scan complete!": "Scan voltooid!",
 	"Scan for documents from {{path}}": "Scan voor documenten van {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Zoeken",
 	"Search a model": "Zoek een model",
 	"Search Chats": "Chats zoeken",

+ 1 - 0
src/lib/i18n/locales/pa-IN/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "ਸਕੈਨ ਕਰੋ",
 	"Scan complete!": "ਸਕੈਨ ਪੂਰਾ!",
 	"Scan for documents from {{path}}": "{{path}} ਤੋਂ ਡਾਕੂਮੈਂਟਾਂ ਲਈ ਸਕੈਨ ਕਰੋ",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "ਖੋਜ",
 	"Search a model": "ਇੱਕ ਮਾਡਲ ਖੋਜੋ",
 	"Search Chats": "ਖੋਜ ਚੈਟਾਂ",

+ 1 - 0
src/lib/i18n/locales/pl-PL/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Skanuj",
 	"Scan complete!": "Skanowanie zakończone!",
 	"Scan for documents from {{path}}": "Skanuj dokumenty z {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Szukaj",
 	"Search a model": "Szukaj modelu",
 	"Search Chats": "Szukaj w czatach",

+ 1 - 0
src/lib/i18n/locales/pt-BR/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Escanear",
 	"Scan complete!": "Escaneamento concluído!",
 	"Scan for documents from {{path}}": "Escanear documentos de {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Pesquisar",
 	"Search a model": "Pesquisar um modelo",
 	"Search Chats": "Pesquisar Chats",

+ 1 - 0
src/lib/i18n/locales/pt-PT/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Digitalizar",
 	"Scan complete!": "Digitalização concluída!",
 	"Scan for documents from {{path}}": "Digitalizar documentos de {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Pesquisar",
 	"Search a model": "Pesquisar um modelo",
 	"Search Chats": "Pesquisar Conversas",

+ 1 - 0
src/lib/i18n/locales/ro-RO/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Scanează",
 	"Scan complete!": "Scanare completă!",
 	"Scan for documents from {{path}}": "Scanează pentru documente din {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Caută",
 	"Search a model": "Caută un model",
 	"Search Chats": "Caută în Conversații",

+ 1 - 0
src/lib/i18n/locales/ru-RU/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Сканировать",
 	"Scan complete!": "Сканирование завершено!",
 	"Scan for documents from {{path}}": "Сканирование документов из {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Поиск",
 	"Search a model": "Поиск модели",
 	"Search Chats": "Поиск в чатах",

+ 1 - 0
src/lib/i18n/locales/sr-RS/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Скенирај",
 	"Scan complete!": "Скенирање завршено!",
 	"Scan for documents from {{path}}": "Скенирај документе из {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Претражи",
 	"Search a model": "Претражи модел",
 	"Search Chats": "Претражи ћаскања",

+ 1 - 0
src/lib/i18n/locales/sv-SE/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Skanna",
 	"Scan complete!": "Skanning klar!",
 	"Scan for documents from {{path}}": "Skanna efter dokument från {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Sök",
 	"Search a model": "Sök efter en modell",
 	"Search Chats": "Sök i chattar",

+ 1 - 0
src/lib/i18n/locales/th-TH/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "สแกน",
 	"Scan complete!": "การสแกนเสร็จสมบูรณ์!",
 	"Scan for documents from {{path}}": "สแกนหาเอกสารจาก {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "ค้นหา",
 	"Search a model": "ค้นหาโมเดล",
 	"Search Chats": "ค้นหาแชท",

+ 1 - 0
src/lib/i18n/locales/tk-TW/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "",
 	"Scan complete!": "",
 	"Scan for documents from {{path}}": "",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "",
 	"Search a model": "",
 	"Search Chats": "",

+ 1 - 0
src/lib/i18n/locales/tr-TR/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Tarama",
 	"Scan complete!": "Tarama tamamlandı!",
 	"Scan for documents from {{path}}": "{{path}} dizininden belgeleri tarayın",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Ara",
 	"Search a model": "Bir model ara",
 	"Search Chats": "Sohbetleri Ara",

+ 1 - 0
src/lib/i18n/locales/uk-UA/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Сканування",
 	"Scan complete!": "Сканування завершено!",
 	"Scan for documents from {{path}}": "Сканування документів з {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Пошук",
 	"Search a model": "Шукати модель",
 	"Search Chats": "Пошук в чатах",

+ 1 - 0
src/lib/i18n/locales/vi-VN/translation.json

@@ -509,6 +509,7 @@
 	"Scan": "Quét tài liệu",
 	"Scan complete!": "Quét hoàn tất!",
 	"Scan for documents from {{path}}": "Quét tài liệu từ đường dẫn: {{path}}",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "Tìm kiếm",
 	"Search a model": "Tìm model",
 	"Search Chats": "Tìm kiếm các cuộc Chat",

+ 5 - 4
src/lib/i18n/locales/zh-CN/translation.json

@@ -15,7 +15,7 @@
 	"Account": "账号",
 	"Account Activation Pending": "账号待激活",
 	"Accurate information": "提供的信息很准确",
-	"Actions": "",
+	"Actions": "自动化",
 	"Active Users": "当前在线用户",
 	"Add": "添加",
 	"Add a model id": "添加一个模型 ID",
@@ -28,7 +28,7 @@
 	"Add Memory": "添加记忆",
 	"Add message": "添加消息",
 	"Add Model": "添加模型",
-	"Add Tag": "",
+	"Add Tag": "添加标签",
 	"Add Tags": "添加标签",
 	"Add User": "添加用户",
 	"Adjusting these settings will apply changes universally to all users.": "调整这些设置将会对所有用户应用更改。",
@@ -375,7 +375,7 @@
 	"Memory deleted successfully": "记忆删除成功",
 	"Memory updated successfully": "记忆更新成功",
 	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "创建链接后发送的消息不会被共享。具有 URL 的用户将能够查看共享对话。",
-	"Min P": "",
+	"Min P": "Min P",
 	"Minimum Score": "最低分",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
@@ -509,6 +509,7 @@
 	"Scan": "立即扫描",
 	"Scan complete!": "扫描完成!",
 	"Scan for documents from {{path}}": "从 {{path}} 扫描文档",
+	"Scroll to bottom when switching between branches": "",
 	"Search": "搜索",
 	"Search a model": "搜索模型",
 	"Search Chats": "搜索对话",
@@ -621,7 +622,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "请联系管理员以访问。管理员可以在后台管理面板中管理用户状态。",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "要在此处添加文档,请先将它们上传到工作空间中的“文档”内。",
 	"to chat input.": "到对话输入。",
-	"To select actions here, add them to the \"Functions\" workspace first.": "",
+	"To select actions here, add them to the \"Functions\" workspace first.": "要在这里选择自动化,请先将它们添加到工作空间中的“函数”。",
 	"To select filters here, add them to the \"Functions\" workspace first.": "要在这里选择过滤器,请先将它们添加到工作空间中的“函数”。",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "要在这里选择工具包,请先将它们添加到工作空间中的“工具”。",
 	"Today": "今天",

+ 340 - 338
src/lib/i18n/locales/zh-TW/translation.json

@@ -1,25 +1,25 @@
 {
-	"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' 或 '-1' 表示無期限。",
-	"(Beta)": "(測試版)",
-	"(e.g. `sh webui.sh --api --api-auth username_password`)": "",
-	"(e.g. `sh webui.sh --api`)": "(例如 `sh webui.sh --api`)",
-	"(latest)": "(最新版)",
+	"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s'、'm'、'h'、'd'、'w' 或 '-1' 表示無到期時間。",
+	"(Beta)": "(測試版)",
+	"(e.g. `sh webui.sh --api --api-auth username_password`)": "(例如 `sh webui.sh --api --api-auth username_password`)",
+	"(e.g. `sh webui.sh --api`)": "(例如 `sh webui.sh --api`)",
+	"(latest)": "(最新版)",
 	"{{ models }}": "{{ models }}",
 	"{{ owner }}: You cannot delete a base model": "{{ owner }}:您無法刪除基礎模型",
 	"{{modelName}} is thinking...": "{{modelName}} 正在思考...",
-	"{{user}}'s Chats": "{{user}} 的聊天",
+	"{{user}}'s Chats": "{{user}} 的對話",
 	"{{webUIName}} Backend Required": "需要 {{webUIName}} 後端",
-	"A task model is used when performing tasks such as generating titles for chats and web search queries": "執行任務時使用任務模型,例如為聊天和網頁搜尋查詢生成標題",
-	"a user": "使用者",
+	"A task model is used when performing tasks such as generating titles for chats and web search queries": "執行產生對話標題和網頁搜尋查詢等任務時使用任務模型",
+	"a user": "一位使用者",
 	"About": "關於",
 	"Account": "帳號",
-	"Account Activation Pending": "帳號啟用",
-	"Accurate information": "確資訊",
-	"Actions": "",
+	"Account Activation Pending": "帳號啟用",
+	"Accurate information": "確資訊",
+	"Actions": "動作",
 	"Active Users": "活躍使用者",
 	"Add": "新增",
 	"Add a model id": "新增模型 ID",
-	"Add a short description about what this model does": "為這個模型新增一個簡短描述",
+	"Add a short description about what this model does": "新增這個模型的簡短描述",
 	"Add a short title for this prompt": "為這個提示詞新增一個簡短的標題",
 	"Add a tag": "新增標籤",
 	"Add custom prompt": "新增自訂提示詞",
@@ -28,131 +28,131 @@
 	"Add Memory": "新增記憶",
 	"Add message": "新增訊息",
 	"Add Model": "新增模型",
-	"Add Tag": "",
+	"Add Tag": "新增標籤",
 	"Add Tags": "新增標籤",
 	"Add User": "新增使用者",
-	"Adjusting these settings will apply changes universally to all users.": "調整這些設定將對所有使用者進行更改。",
+	"Adjusting these settings will apply changes universally to all users.": "調整這些設定將會全面套用到所有使用者。",
 	"admin": "管理員",
 	"Admin": "管理員",
 	"Admin Panel": "管理員控制台",
-	"Admin Settings": "管理設定",
-	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "管理員隨時可以使用所有工具;使用者需要在工作區中為每個模型分配工具。",
+	"Admin Settings": "管理設定",
+	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "管理員可以隨時使用所有工具;使用者需要在工作區中為每個模型分配工具。",
 	"Advanced Parameters": "進階參數",
 	"Advanced Params": "進階參數",
-	"all": "所有",
+	"all": "全部",
 	"All Documents": "所有文件",
 	"All Users": "所有使用者",
 	"Allow": "允許",
-	"Allow Chat Deletion": "允許刪除聊天紀錄",
+	"Allow Chat Deletion": "允許刪除對話紀錄",
 	"Allow non-local voices": "允許非本機語音",
 	"Allow User Location": "允許使用者位置",
-	"Allow Voice Interruption in Call": "",
-	"alphanumeric characters and hyphens": "英文字母、數字(0~9)和連字元(-)",
+	"Allow Voice Interruption in Call": "允許在通話中打斷語音",
+	"alphanumeric characters and hyphens": "英文字母、數字和連字號",
 	"Already have an account?": "已經有帳號了嗎?",
-	"an assistant": "助手",
+	"an assistant": "一位助手",
 	"and": "和",
-	"and create a new shared link.": "並建立一個新的共享連結。",
-	"API Base URL": "API 基 URL",
+	"and create a new shared link.": "並建立新的共用連結。",
+	"API Base URL": "API 基 URL",
 	"API Key": "API 金鑰",
 	"API Key created.": "API 金鑰已建立。",
 	"API keys": "API 金鑰",
 	"April": "4 月",
 	"Archive": "封存",
-	"Archive All Chats": "封存所有聊天紀錄",
-	"Archived Chats": "已封存的聊天紀錄",
-	"are allowed - Activate this command by typing": "是允許的 - 透過輸入",
+	"Archive All Chats": "封存所有對話紀錄",
+	"Archived Chats": "封存的對話紀錄",
+	"are allowed - Activate this command by typing": "已允許 - 輸入此命令來啟用",
 	"Are you sure?": "您確定嗎?",
 	"Attach file": "附加檔案",
-	"Attention to detail": "細節精確",
+	"Attention to detail": "注重細節",
 	"Audio": "音訊",
-	"Audio settings updated successfully": "",
+	"Audio settings updated successfully": "成功更新音訊設定",
 	"August": "8 月",
-	"Auto-playback response": "自動播放回",
-	"AUTOMATIC1111 Api Auth String": "",
-	"AUTOMATIC1111 Base URL": "AUTOMATIC1111 基 URL",
-	"AUTOMATIC1111 Base URL is required.": "需要 AUTOMATIC1111 基本 URL",
+	"Auto-playback response": "自動播放回",
+	"AUTOMATIC1111 Api Auth String": "AUTOMATIC1111 API 驗證字串",
+	"AUTOMATIC1111 Base URL": "AUTOMATIC1111 基 URL",
+	"AUTOMATIC1111 Base URL is required.": "需要 AUTOMATIC1111 基礎 URL。",
 	"available!": "可用!",
 	"Back": "返回",
 	"Bad Response": "錯誤回應",
 	"Banners": "橫幅",
 	"Base Model (From)": "基礎模型(來自)",
 	"Batch Size (num_batch)": "批次大小(num_batch)",
-	"before": "前",
-	"Being lazy": "懶模式",
+	"before": "前",
+	"Being lazy": "懶模式",
 	"Brave Search API Key": "Brave 搜尋 API 金鑰",
-	"Bypass SSL verification for Websites": "過網站的 SSL 驗證",
-	"Call": "呼叫",
-	"Call feature is not supported when using Web STT engine": "使用 Web STT 引擎時不支援呼叫功能",
+	"Bypass SSL verification for Websites": "過網站的 SSL 驗證",
+	"Call": "通話",
+	"Call feature is not supported when using Web STT engine": "使用網頁語音辨識 (Web STT) 引擎時不支援通話功能",
 	"Camera": "相機",
 	"Cancel": "取消",
 	"Capabilities": "功能",
 	"Change Password": "修改密碼",
-	"Chat": "聊天",
-	"Chat Background Image": "",
-	"Chat Bubble UI": "聊天氣泡介面",
-	"Chat Controls": "",
-	"Chat direction": "聊天方向",
-	"Chat History": "聊天紀錄",
-	"Chat History is off for this browser.": "此瀏覽器已關閉聊天紀錄。",
-	"Chats": "聊天",
-	"Check Again": "重新檢查",
+	"Chat": "對話",
+	"Chat Background Image": "對話背景圖片",
+	"Chat Bubble UI": "對話氣泡介面",
+	"Chat Controls": "對話控制項",
+	"Chat direction": "對話方向",
+	"Chat History": "對話紀錄",
+	"Chat History is off for this browser.": "此瀏覽器已關閉對話紀錄。",
+	"Chats": "對話",
+	"Check Again": "再次檢查",
 	"Check for updates": "檢查更新",
 	"Checking for updates...": "正在檢查更新...",
-	"Choose a model before saving...": "儲存前選擇一個模型...",
+	"Choose a model before saving...": "儲存前選擇一個模型...",
 	"Chunk Overlap": "區塊重疊",
 	"Chunk Params": "區塊參數",
 	"Chunk Size": "區塊大小",
-	"Citation": "引",
+	"Citation": "引",
 	"Clear memory": "清除記憶",
-	"Click here for help.": "點選這裡尋求幫助。",
+	"Click here for help.": "點選這裡取得協助。",
 	"Click here to": "點選這裡",
-	"Click here to download user import template file.": "點選這裡下載使用者匯入範本",
+	"Click here to download user import template file.": "點選這裡下載使用者匯入範本檔案。",
 	"Click here to select": "點選這裡選擇",
-	"Click here to select a csv file.": "點選這裡選擇 csv 檔案。",
-	"Click here to select a py file.": "點選這裡選擇 py 檔案。",
+	"Click here to select a csv file.": "點選這裡選擇 CSV 檔案。",
+	"Click here to select a py file.": "點選這裡選擇 Python 檔案。",
 	"Click here to select documents.": "點選這裡選擇文件。",
 	"click here.": "點選這裡。",
-	"Click on the user role button to change a user's role.": "點選使用者角色按鈕以更使用者的角色。",
-	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "",
+	"Click on the user role button to change a user's role.": "點選使用者角色按鈕以更使用者的角色。",
+	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "剪貼簿寫入權限遭拒。請檢查您的瀏覽器設定,以授予必要的存取權限。",
 	"Clone": "複製",
 	"Close": "關閉",
-	"Code formatted successfully": "",
+	"Code formatted successfully": "成功格式化程式碼",
 	"Collection": "收藏",
 	"ComfyUI": "ComfyUI",
-	"ComfyUI Base URL": "ComfyUI 基 URL",
-	"ComfyUI Base URL is required.": "需要 ComfyUI 基本 URL",
+	"ComfyUI Base URL": "ComfyUI 基 URL",
+	"ComfyUI Base URL is required.": "需要 ComfyUI 基礎 URL。",
 	"Command": "命令",
-	"Concurrent Requests": "同時請求",
-	"Confirm": "",
+	"Concurrent Requests": "平行請求",
+	"Confirm": "確認",
 	"Confirm Password": "確認密碼",
-	"Confirm your action": "",
+	"Confirm your action": "確認您的動作",
 	"Connections": "連線",
-	"Contact Admin for WebUI Access": "聯絡管理員以取得 WebUI 存取權",
+	"Contact Admin for WebUI Access": "聯絡管理員以取得 WebUI 存取權",
 	"Content": "內容",
-	"Content Extraction": "",
+	"Content Extraction": "內容提取",
 	"Context Length": "上下文長度",
-	"Continue Response": "繼續回",
-	"Continue with {{provider}}": "",
-	"Controls": "",
-	"Copied shared chat URL to clipboard!": "已複製共享聊天連結到剪貼簿!",
+	"Continue Response": "繼續回",
+	"Continue with {{provider}}": "使用 {{provider}} 繼續",
+	"Controls": "控制項",
+	"Copied shared chat URL to clipboard!": "已複製共用對話 URL 到剪貼簿!",
 	"Copy": "複製",
 	"Copy last code block": "複製最後一個程式碼區塊",
-	"Copy last response": "複製最後一個回",
+	"Copy last response": "複製最後一個回",
 	"Copy Link": "複製連結",
 	"Copying to clipboard was successful!": "成功複製到剪貼簿!",
 	"Create a model": "建立模型",
 	"Create Account": "建立帳號",
-	"Create new key": "建立新金鑰",
-	"Create new secret key": "建立新金鑰",
+	"Create new key": "建立新金鑰",
+	"Create new secret key": "建立新金鑰",
 	"Created at": "建立於",
 	"Created At": "建立於",
-	"Created by": "",
-	"CSV Import": "",
+	"Created by": "建立者",
+	"CSV Import": "CSV 匯入",
 	"Current Model": "目前模型",
 	"Current Password": "目前密碼",
 	"Custom": "自訂",
-	"Customize models for a specific purpose": "為特定目的自訂模型",
-	"Dark": "色",
+	"Customize models for a specific purpose": "自訂模型以用於特定目的",
+	"Dark": "色",
 	"Dashboard": "儀表板",
 	"Database": "資料庫",
 	"December": "12 月",
@@ -165,107 +165,107 @@
 	"Default User Role": "預設使用者角色",
 	"delete": "刪除",
 	"Delete": "刪除",
-	"Delete a model": "刪除一個模型",
-	"Delete All Chats": "刪除所有聊天紀錄",
-	"Delete chat": "刪除聊天紀錄",
-	"Delete Chat": "刪除聊天紀錄",
-	"Delete chat?": "",
-	"Delete Doc": "",
-	"Delete function?": "",
-	"Delete prompt?": "",
+	"Delete a model": "刪除模型",
+	"Delete All Chats": "刪除所有對話紀錄",
+	"Delete chat": "刪除對話紀錄",
+	"Delete Chat": "刪除對話紀錄",
+	"Delete chat?": "刪除對話紀錄?",
+	"Delete Doc": "刪除文件",
+	"Delete function?": "刪除函式?",
+	"Delete prompt?": "刪除提示詞?",
 	"delete this link": "刪除此連結",
-	"Delete tool?": "",
+	"Delete tool?": "刪除工具?",
 	"Delete User": "刪除使用者",
 	"Deleted {{deleteModelTag}}": "已刪除 {{deleteModelTag}}",
 	"Deleted {{name}}": "已刪除 {{name}}",
 	"Description": "描述",
 	"Didn't fully follow instructions": "未完全遵循指示",
-	"Disabled": "",
-	"Discover a function": "",
-	"Discover a model": "發現新模型",
-	"Discover a prompt": "發現新提示詞",
-	"Discover a tool": "",
-	"Discover, download, and explore custom functions": "",
-	"Discover, download, and explore custom prompts": "發現、下載並探索自訂提示詞",
-	"Discover, download, and explore custom tools": "",
-	"Discover, download, and explore model presets": "發現、下載並探索模型預設值",
+	"Disabled": "已停用",
+	"Discover a function": "發掘函式",
+	"Discover a model": "發模型",
+	"Discover a prompt": "發提示詞",
+	"Discover a tool": "發掘工具",
+	"Discover, download, and explore custom functions": "發掘、下載及探索自訂函式",
+	"Discover, download, and explore custom prompts": "發掘、下載及探索自訂提示詞",
+	"Discover, download, and explore custom tools": "發掘、下載及探索自訂工具",
+	"Discover, download, and explore model presets": "發掘、下載及探索模型預設集",
 	"Dismissible": "可忽略",
-	"Display Emoji in Call": "在呼叫中顯示表情符號",
-	"Display the username instead of You in the Chat": "在聊天中顯示使用者名稱而不是「您」",
-	"Do not install functions from sources you do not fully trust.": "",
-	"Do not install tools from sources you do not fully trust.": "",
+	"Display Emoji in Call": "在通話中顯示表情符號",
+	"Display the username instead of You in the Chat": "在對話中顯示使用者名稱,而非「您」",
+	"Do not install functions from sources you do not fully trust.": "請勿從您無法完全信任的來源安裝函式。",
+	"Do not install tools from sources you do not fully trust.": "請勿從您無法完全信任的來源安裝工具。",
 	"Document": "文件",
 	"Document Settings": "文件設定",
 	"Documentation": "文件",
 	"Documents": "文件",
-	"does not make any external connections, and your data stays securely on your locally hosted server.": "不會與外部連線,您的資料會安全地留在您的本機伺服器上。",
+	"does not make any external connections, and your data stays securely on your locally hosted server.": "不會建立任何外部連線,而且您的資料會安全地儲存在您本機伺服器上。",
 	"Don't Allow": "不允許",
-	"Don't have an account?": "還沒註冊帳號?",
-	"don't install random functions from sources you don't trust.": "",
-	"don't install random tools from sources you don't trust.": "",
-	"Don't like the style": "不喜歡這個樣式",
-	"Done": "",
+	"Don't have an account?": "還沒註冊帳號?",
+	"don't install random functions from sources you don't trust.": "請勿從您無法信任的來源安裝隨機函式。",
+	"don't install random tools from sources you don't trust.": "請勿從您無法信任的來源安裝隨機工具。",
+	"Don't like the style": "不喜歡這個樣式",
+	"Done": "完成",
 	"Download": "下載",
-	"Download canceled": "下載已取消",
+	"Download canceled": "已取消下載",
 	"Download Database": "下載資料庫",
 	"Drop any files here to add to the conversation": "拖拽任意檔案到此處以新增至對話",
-	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "例如 '30s', '10m'。有效的時間單位為 's', 'm', 'h'。",
+	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "例如:'30s'、'10m'。有效的時間單位為 's'、'm'、'h'。",
 	"Edit": "編輯",
 	"Edit Doc": "編輯文件",
 	"Edit Memory": "編輯記憶",
 	"Edit User": "編輯使用者",
-	"ElevenLabs": "",
+	"ElevenLabs": "ElevenLabs",
 	"Email": "電子郵件",
 	"Embedding Batch Size": "嵌入批次大小",
 	"Embedding Model": "嵌入模型",
 	"Embedding Model Engine": "嵌入模型引擎",
 	"Embedding model set to \"{{embedding_model}}\"": "嵌入模型已設定為 \"{{embedding_model}}\"",
-	"Enable Chat History": "啟用聊天紀錄",
+	"Enable Chat History": "啟用對話紀錄",
 	"Enable Community Sharing": "啟用社群分享",
-	"Enable New Sign Ups": "允許註冊新帳號",
+	"Enable New Sign Ups": "允許新使用者註冊",
 	"Enable Web Search": "啟用網頁搜尋",
-	"Enabled": "",
-	"Engine": "",
-	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "請確保您的 CSV 檔案包含這四個欄位,並按照此順序:名稱、電子郵件、密碼、角色。",
-	"Enter {{role}} message here": "在這裡輸入 {{role}} 訊息",
-	"Enter a detail about yourself for your LLMs to recall": "輸入 LLM 記憶的詳細內容",
-	"Enter api auth string (e.g. username:password)": "",
+	"Enabled": "已啟用",
+	"Engine": "引擎",
+	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "請確認您的 CSV 檔案包含以下 4 個欄位,並按照此順序排列:名稱、電子郵件、密碼、角色。",
+	"Enter {{role}} message here": "在輸入 {{role}} 訊息",
+	"Enter a detail about yourself for your LLMs to recall": "輸入有關您的詳細資訊,讓您的大型語言模型可以回想起來",
+	"Enter api auth string (e.g. username:password)": "輸入 API 驗證字串(例如:username:password)",
 	"Enter Brave Search API Key": "輸入 Brave 搜尋 API 金鑰",
 	"Enter Chunk Overlap": "輸入區塊重疊",
 	"Enter Chunk Size": "輸入區塊大小",
-	"Enter Github Raw URL": "輸入 Github Raw URL",
+	"Enter Github Raw URL": "輸入 GitHub Raw URL",
 	"Enter Google PSE API Key": "輸入 Google PSE API 金鑰",
 	"Enter Google PSE Engine Id": "輸入 Google PSE 引擎 ID",
-	"Enter Image Size (e.g. 512x512)": "輸入圖片大小(例如 512x512)",
+	"Enter Image Size (e.g. 512x512)": "輸入圖片大小(例如512x512)",
 	"Enter language codes": "輸入語言代碼",
-	"Enter model tag (e.g. {{modelTag}})": "輸入模型標籤(例如 {{modelTag}})",
-	"Enter Number of Steps (e.g. 50)": "輸入步數(例如 50)",
+	"Enter model tag (e.g. {{modelTag}})": "輸入模型標籤(例如{{modelTag}})",
+	"Enter Number of Steps (e.g. 50)": "輸入步驟數(例如:50)",
 	"Enter Score": "輸入分數",
-	"Enter Searxng Query URL": "輸入 Searxng 查詢 URL",
+	"Enter Searxng Query URL": "輸入 SearXNG 查詢 URL",
 	"Enter Serper API Key": "輸入 Serper API 金鑰",
 	"Enter Serply API Key": "輸入 Serply API 金鑰",
 	"Enter Serpstack API Key": "輸入 Serpstack API 金鑰",
 	"Enter stop sequence": "輸入停止序列",
-	"Enter system prompt": "",
+	"Enter system prompt": "輸入系統提示詞",
 	"Enter Tavily API Key": "輸入 Tavily API 金鑰",
-	"Enter Tika Server URL": "",
-	"Enter Top K": "輸入 Top K",
-	"Enter URL (e.g. http://127.0.0.1:7860/)": "輸入 URL(例如 http://127.0.0.1:7860/)",
-	"Enter URL (e.g. http://localhost:11434)": "輸入 URL(例如 http://localhost:11434)",
+	"Enter Tika Server URL": "輸入 Tika 伺服器 URL",
+	"Enter Top K": "輸入 Top K",
+	"Enter URL (e.g. http://127.0.0.1:7860/)": "輸入 URL(例如http://127.0.0.1:7860/)",
+	"Enter URL (e.g. http://localhost:11434)": "輸入 URL(例如http://localhost:11434)",
 	"Enter Your Email": "輸入您的電子郵件",
 	"Enter Your Full Name": "輸入您的全名",
-	"Enter your message": "",
+	"Enter your message": "輸入您的訊息",
 	"Enter Your Password": "輸入您的密碼",
 	"Enter Your Role": "輸入您的角色",
 	"Error": "錯誤",
 	"Experimental": "實驗性功能",
 	"Export": "匯出",
-	"Export All Chats (All Users)": "匯出所有聊天紀錄(所有使用者)",
-	"Export chat (.json)": "匯出聊天紀錄(.json)",
-	"Export Chats": "匯出聊天紀錄",
-	"Export Documents Mapping": "匯出文件對",
-	"Export Functions": "匯出功能",
-	"Export LiteLLM config.yaml": "",
+	"Export All Chats (All Users)": "匯出所有對話紀錄(所有使用者)",
+	"Export chat (.json)": "匯出對話紀錄(.json)",
+	"Export Chats": "匯出對話紀錄",
+	"Export Documents Mapping": "匯出文件對",
+	"Export Functions": "匯出函式",
+	"Export LiteLLM config.yaml": "匯出 LiteLLM config.yaml",
 	"Export Models": "匯出模型",
 	"Export Prompts": "匯出提示詞",
 	"Export Tools": "匯出工具",
@@ -274,153 +274,153 @@
 	"Failed to read clipboard contents": "無法讀取剪貼簿內容",
 	"Failed to update settings": "無法更新設定",
 	"February": "2 月",
-	"Feel free to add specific details": "請隨意新增詳細內容。",
+	"Feel free to add specific details": "歡迎自由新增特定細節",
 	"File": "檔案",
 	"File Mode": "檔案模式",
 	"File not found.": "找不到檔案。",
-	"Files": "",
-	"Filter is now globally disabled": "",
-	"Filter is now globally enabled": "",
+	"Files": "檔案",
+	"Filter is now globally disabled": "篩選器現在已全域停用",
+	"Filter is now globally enabled": "篩選器現在已全域啟用",
 	"Filters": "篩選器",
-	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "偽造偵測:無法使用初始頭像。預設為預設個人影象。",
-	"Fluidly stream large external response chunks": "流暢地傳輸大型外部回應區塊",
-	"Focus chat input": "聚焦聊天輸入框",
+	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "偵測到指紋偽造:無法使用姓名縮寫作為大頭貼。將預設為預設個人檔案圖片。",
+	"Fluidly stream large external response chunks": "流暢地串流大型外部回應區塊",
+	"Focus chat input": "聚焦對話輸入",
 	"Followed instructions perfectly": "完全遵循指示",
 	"Form": "表單",
-	"Format your variables using square brackets like this:": "像這樣使用方括號來格式化您的變數:",
+	"Format your variables using square brackets like this:": "使用方括號來格式化您的變數,如下所示:",
 	"Frequency Penalty": "頻率懲罰",
-	"Function created successfully": "",
-	"Function deleted successfully": "",
-	"Function Description (e.g. A filter to remove profanity from text)": "",
-	"Function ID (e.g. my_filter)": "",
-	"Function is now globally disabled": "",
-	"Function is now globally enabled": "",
-	"Function Name (e.g. My Filter)": "",
-	"Function updated successfully": "",
-	"Functions": "功能",
-	"Functions allow arbitrary code execution": "",
-	"Functions allow arbitrary code execution.": "",
-	"Functions imported successfully": "",
-	"General": "常用",
-	"General Settings": "常用設定",
-	"Generate Image": "生圖片",
-	"Generating search query": "生搜尋查詢",
+	"Function created successfully": "成功建立函式",
+	"Function deleted successfully": "成功刪除函式",
+	"Function Description (e.g. A filter to remove profanity from text)": "函式描述(例如:用於從文字中移除不雅詞彙的篩選器)",
+	"Function ID (e.g. my_filter)": "函式 ID(例如:my_filter)",
+	"Function is now globally disabled": "現在已在全域停用函式",
+	"Function is now globally enabled": "現在已在全域啟用函式",
+	"Function Name (e.g. My Filter)": "函式名稱(例如:我的篩選器)",
+	"Function updated successfully": "成功更新函式",
+	"Functions": "函式",
+	"Functions allow arbitrary code execution": "函式允許執行任意程式碼",
+	"Functions allow arbitrary code execution.": "函式允許執行任意程式碼。",
+	"Functions imported successfully": "成功匯入函式",
+	"General": "一般",
+	"General Settings": "一般設定",
+	"Generate Image": "生圖片",
+	"Generating search query": "正在產生搜尋查詢",
 	"Generation Info": "生成資訊",
-	"Get up and running with": "",
-	"Global": "",
-	"Good Response": "優秀的回應",
+	"Get up and running with": "開始使用",
+	"Global": "全域",
+	"Good Response": "良好回應",
 	"Google PSE API Key": "Google PSE API 金鑰",
 	"Google PSE Engine Id": "Google PSE 引擎 ID",
 	"h:mm a": "h:mm a",
-	"has no conversations.": "沒有對話",
+	"has no conversations.": "沒有對話",
 	"Hello, {{name}}": "您好,{{name}}",
-	"Help": "幫助",
+	"Help": "說明",
 	"Hide": "隱藏",
 	"Hide Model": "隱藏模型",
-	"How can I help you today?": "今天能為您做些什麼?",
+	"How can I help you today?": "今天能為您做些什麼?",
 	"Hybrid Search": "混合搜尋",
-	"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "",
-	"Image Generation (Experimental)": "影像生成(實驗性功能)",
-	"Image Generation Engine": "影像生成引擎",
+	"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "我確認已閱讀並理解我的操作所帶來的影響。我了解執行任意程式碼的相關風險,並已驗證來源的可信度。",
+	"Image Generation (Experimental)": "圖片生成(實驗性功能)",
+	"Image Generation Engine": "圖片生成引擎",
 	"Image Settings": "圖片設定",
 	"Images": "圖片",
-	"Import Chats": "匯入聊天紀錄",
-	"Import Documents Mapping": "匯入文件對",
-	"Import Functions": "匯入功能",
+	"Import Chats": "匯入對話紀錄",
+	"Import Documents Mapping": "匯入文件對",
+	"Import Functions": "匯入函式",
 	"Import Models": "匯入模型",
 	"Import Prompts": "匯入提示詞",
 	"Import Tools": "匯入工具",
-	"Include `--api-auth` flag when running stable-diffusion-webui": "",
-	"Include `--api` flag when running stable-diffusion-webui": "在執行 stable-diffusion-webui 時加上 `--api` 標誌",
+	"Include `--api-auth` flag when running stable-diffusion-webui": "執行 stable-diffusion-webui 時包含 `--api-auth` 參數",
+	"Include `--api` flag when running stable-diffusion-webui": "執行 stable-diffusion-webui 時包含 `--api` 參數",
 	"Info": "資訊",
 	"Input commands": "輸入命令",
-	"Install from Github URL": "從 Github URL 安裝",
+	"Install from Github URL": "從 GitHub URL 安裝",
 	"Instant Auto-Send After Voice Transcription": "語音轉錄後立即自動傳送",
 	"Interface": "介面",
 	"Invalid Tag": "無效標籤",
 	"January": "1 月",
-	"join our Discord for help.": "加入我們的 Discord 尋求幫助。",
+	"join our Discord for help.": "加入我們的 Discord 以尋求協助。",
 	"JSON": "JSON",
 	"JSON Preview": "JSON 預覽",
 	"July": "7 月",
 	"June": "6 月",
 	"JWT Expiration": "JWT 過期時間",
 	"JWT Token": "JWT Token",
-	"Keep Alive": "保持活躍",
-	"Keyboard shortcuts": "鍵盤快鍵",
+	"Keep Alive": "保持連線",
+	"Keyboard shortcuts": "鍵盤快鍵",
 	"Knowledge": "知識",
 	"Language": "語言",
-	"large language models, locally.": "",
-	"Last Active": "最後活動",
-	"Last Modified": "最後修改",
-	"Light": "色",
+	"large language models, locally.": "在本機執行大型語言模型。",
+	"Last Active": "上次活動時間",
+	"Last Modified": "上次修改時間",
+	"Light": "色",
 	"Listening...": "正在聆聽...",
-	"LLMs can make mistakes. Verify important information.": "LLM 可能會產生錯誤。請驗證重要資訊。",
+	"LLMs can make mistakes. Verify important information.": "大型語言模型可能會出錯。請驗證重要資訊。",
 	"Local Models": "本機模型",
-	"LTR": "LTR",
+	"LTR": "從左到右",
 	"Made by OpenWebUI Community": "由 OpenWebUI 社群製作",
-	"Make sure to enclose them with": "請確保變數有被以下符號框住:",
+	"Make sure to enclose them with": "請務必將它們放在",
 	"Manage": "管理",
 	"Manage Models": "管理模型",
 	"Manage Ollama Models": "管理 Ollama 模型",
 	"Manage Pipelines": "管理管線",
 	"March": "3 月",
-	"Max Tokens (num_predict)": "最大 Token(num_predict)",
-	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "最多可同時下載 3 個模型。請稍後再試。",
+	"Max Tokens (num_predict)": "最大 token 數(num_predict)",
+	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "最多可同時下載 3 個模型。請稍後再試。",
 	"May": "5 月",
-	"Memories accessible by LLMs will be shown here.": "LLM 記憶將會顯示在此處。",
+	"Memories accessible by LLMs will be shown here.": "可被大型語言模型存取的記憶將顯示在這裡。",
 	"Memory": "記憶",
-	"Memory added successfully": "",
-	"Memory cleared successfully": "",
-	"Memory deleted successfully": "",
-	"Memory updated successfully": "",
-	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "建立連結後傳送的訊息將不會被共享。具有 URL 的使用者將會能夠檢視共享的聊天。",
-	"Min P": "",
+	"Memory added successfully": "成功新增記憶",
+	"Memory cleared successfully": "成功清除記憶",
+	"Memory deleted successfully": "成功刪除記憶",
+	"Memory updated successfully": "成功更新記憶",
+	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "建立連結後傳送的訊息不會被分享。擁有網址的使用者將能夠檢視分享的對話內容。",
+	"Min P": "最小 P 值",
 	"Minimum Score": "最低分數",
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
 	"Mirostat Tau": "Mirostat Tau",
-	"MMMM DD, YYYY": "MMMM DD, YYYY",
-	"MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm",
-	"MMMM DD, YYYY hh:mm:ss A": "MMMM DD, YYYY hh:mm:ss A",
-	"Model '{{modelName}}' has been successfully downloaded.": "'{{modelName}}' 模型已成功下載。",
-	"Model '{{modelTag}}' is already in queue for downloading.": "'{{modelTag}}' 模型已經在下載佇列中。",
-	"Model {{modelId}} not found": "找不到 {{modelId}} 模型",
-	"Model {{modelName}} is not vision capable": "{{modelName}} 模型不適用於視覺",
-	"Model {{name}} is now {{status}}": "{{name}} 模型現在是 {{status}}",
-	"Model created successfully!": "",
-	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "偵測到模型檔案系統路徑。需要更新模型簡稱,無法繼續。",
+	"MMMM DD, YYYY": "YYYY 年 MMMM DD 日",
+	"MMMM DD, YYYY HH:mm": "YYYY 年 MMMM DD 日 HH:mm",
+	"MMMM DD, YYYY hh:mm:ss A": "YYYY 年 MMMM DD 日 hh:mm:ss A",
+	"Model '{{modelName}}' has been successfully downloaded.": "模型「{{modelName}}」已成功下載。",
+	"Model '{{modelTag}}' is already in queue for downloading.": "模型「{{modelTag}}」已在下載佇列中。",
+	"Model {{modelId}} not found": "找不到模型 {{modelId}}",
+	"Model {{modelName}} is not vision capable": "模型 {{modelName}} 不具備視覺能力",
+	"Model {{name}} is now {{status}}": "模型 {{name}} 現在狀態為 {{status}}",
+	"Model created successfully!": "成功建立模型!",
+	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "偵測到模型檔案系統路徑。更新需要模型簡稱,因此無法繼續。",
 	"Model ID": "模型 ID",
-	"Model not selected": "未選模型",
+	"Model not selected": "未選模型",
 	"Model Params": "模型參數",
-	"Model updated successfully": "",
-	"Model Whitelisting": "白名單模型",
+	"Model updated successfully": "成功更新模型",
+	"Model Whitelisting": "模型白名單",
 	"Model(s) Whitelisted": "模型已加入白名單",
-	"Modelfile Content": "Modelfile 內容",
+	"Modelfile Content": "模型檔案內容",
 	"Models": "模型",
 	"More": "更多",
 	"Name": "名稱",
 	"Name Tag": "名稱標籤",
-	"Name your model": "請輸入模型名稱",
-	"New Chat": "新增聊天",
-	"New Password": "新密碼",
-	"No content to speak": "",
+	"Name your model": "為您的模型命名",
+	"New Chat": "新增對話",
+	"New Password": "新密碼",
+	"No content to speak": "沒有要朗讀的內容",
 	"No documents found": "找不到文件",
-	"No file selected": "",
-	"No results found": "沒有找到結果",
-	"No search query generated": "沒有生成搜尋查詢",
+	"No file selected": "未選取檔案",
+	"No results found": "找任何結果",
+	"No search query generated": "未產生搜尋查詢",
 	"No source available": "沒有可用的來源",
-	"No valves to update": "",
+	"No valves to update": "沒有要更新的閥門",
 	"None": "無",
 	"Not factually correct": "與真實資訊不符",
-	"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "註:如果設定最低分數,則搜尋將只返回分數大於或等於最低分數的文件。",
+	"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "注意:如果您設定了最低分數,則搜尋只會回傳分數大於或等於最低分數的文件。",
 	"Notifications": "通知",
 	"November": "11 月",
-	"num_thread (Ollama)": "num_thread(Ollama)",
-	"OAuth ID": "",
+	"num_thread (Ollama)": "num_thread (Ollama)",
+	"OAuth ID": "OAuth ID",
 	"October": "10 月",
 	"Off": "關閉",
-	"Okay, Let's Go!": "好的,啟動吧!",
+	"Okay, Let's Go!": "好的,我們開始吧!",
 	"OLED Dark": "OLED 深色",
 	"Ollama": "Ollama",
 	"Ollama API": "Ollama API",
@@ -428,15 +428,15 @@
 	"Ollama API is disabled": "Ollama API 已停用",
 	"Ollama Version": "Ollama 版本",
 	"On": "開啟",
-	"Only": "僅",
-	"Only alphanumeric characters and hyphens are allowed in the command string.": "命令字串中只能包含英文字母、數字(0~9)和連字元(-)。",
-	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "哎呀!請稍等!您的文件還在處理中。我們正最佳化文件,請耐心等待,一旦準備好,我們會通知您。",
-	"Oops! Looks like the URL is invalid. Please double-check and try again.": "哎呀!看起來 URL 無效。請仔細檢查後再試一次。",
-	"Oops! There was an error in the previous response. Please try again or contact admin.": "哎呀!先前的回應發生錯誤。請重試或聯絡管理員",
-	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "哎呀!您正在使用不支援的方法(僅有前端)。請從後端提供 WebUI。",
+	"Only": "僅",
+	"Only alphanumeric characters and hyphens are allowed in the command string.": "命令字串中只允許使用英文字母、數字和連字號。",
+	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "哎呀!請稍等!您的檔案仍在處理中。我們正在完善它們。請耐心等待,我們會在它們準備好時通知您。",
+	"Oops! Looks like the URL is invalid. Please double-check and try again.": "哎呀!這個 URL 似乎無效。請仔細檢查並再試一次。",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "哎呀!先前的回應發生錯誤。請重試或聯絡管理員",
+	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "哎呀!您使用了不支援的方法(僅限前端)。請從後端提供 WebUI。",
 	"Open AI (Dall-E)": "Open AI (Dall-E)",
-	"Open new chat": "開啟新聊天",
-	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
+	"Open new chat": "開啟新的對話",
+	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "Open WebUI 版本 (v{{OPEN_WEBUI_VERSION}}) 低於所需版本 (v{{REQUIRED_VERSION}})",
 	"OpenAI": "OpenAI",
 	"OpenAI API": "OpenAI API",
 	"OpenAI API Config": "OpenAI API 設定",
@@ -447,25 +447,25 @@
 	"Password": "密碼",
 	"PDF document (.pdf)": "PDF 文件 (.pdf)",
 	"PDF Extract Images (OCR)": "PDF 影像擷取(OCR 光學文字辨識)",
-	"pending": "待審查",
-	"Permission denied when accessing media devices": "存取媒體裝置時被拒絕權限",
-	"Permission denied when accessing microphone": "存取麥克風時被拒絕權限",
-	"Permission denied when accessing microphone: {{error}}": "存取麥克風時被拒絕權限:{{error}}",
+	"pending": "待處理",
+	"Permission denied when accessing media devices": "存取媒體裝置時權限遭拒",
+	"Permission denied when accessing microphone": "存取麥克風時權限遭拒",
+	"Permission denied when accessing microphone: {{error}}": "存取麥克風時權限遭拒:{{error}}",
 	"Personalization": "個人化",
-	"Pin": "",
-	"Pinned": "",
-	"Pipeline deleted successfully": "",
-	"Pipeline downloaded successfully": "",
+	"Pin": "釘選",
+	"Pinned": "已釘選",
+	"Pipeline deleted successfully": "成功刪除管線",
+	"Pipeline downloaded successfully": "成功下載管線",
 	"Pipelines": "管線",
-	"Pipelines Not Detected": "",
+	"Pipelines Not Detected": "未偵測到管線",
 	"Pipelines Valves": "管線閥門",
 	"Plain text (.txt)": "純文字 (.txt)",
-	"Playground": "AI 對話遊樂場",
-	"Please carefully review the following warnings:": "",
-	"Positive attitude": "積極態度",
-	"Previous 30 days": " 30 天",
-	"Previous 7 days": " 7 天",
-	"Profile Image": "個人影像",
+	"Playground": "遊樂場",
+	"Please carefully review the following warnings:": "請仔細閱讀以下警告:",
+	"Positive attitude": "積極態度",
+	"Previous 30 days": "過去 30 天",
+	"Previous 7 days": "過去 7 天",
+	"Profile Image": "個人檔案圖片",
 	"Prompt": "提示詞",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "提示詞(例如:告訴我關於羅馬帝國的一些趣事)",
 	"Prompt Content": "提示詞內容",
@@ -474,71 +474,73 @@
 	"Pull \"{{searchValue}}\" from Ollama.com": "從 Ollama.com 下載 \"{{searchValue}}\"",
 	"Pull a model from Ollama.com": "從 Ollama.com 下載模型",
 	"Query Params": "查詢參數",
-	"RAG Template": "RAG 範",
-	"Read Aloud": "讀",
+	"RAG Template": "RAG 範",
+	"Read Aloud": "讀",
 	"Record voice": "錄音",
-	"Redirecting you to OpenWebUI Community": "將您重新導向到 OpenWebUI 社群",
-	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "將自己稱為「使用者」(例如,「使用者正在學習西班牙語」)",
-	"Refused when it shouldn't have": "不拒絕時拒絕了",
-	"Regenerate": "重新生",
+	"Redirecting you to OpenWebUI Community": "正在將您重新導向到 OpenWebUI 社群",
+	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "以「使用者」稱呼自己(例如:「使用者正在學習西班牙文」)",
+	"Refused when it shouldn't have": "不拒絕時拒絕了",
+	"Regenerate": "重新生",
 	"Release Notes": "發布說明",
 	"Remove": "移除",
 	"Remove Model": "移除模型",
 	"Rename": "重新命名",
-	"Repeat Last N": "重複最後 N ",
+	"Repeat Last N": "重複最後 N ",
 	"Request Mode": "請求模式",
 	"Reranking Model": "重新排序模型",
-	"Reranking model disabled": "重新排序模型已停用",
-	"Reranking model set to \"{{reranking_model}}\"": "重新排序模型設定為 \"{{reranking_model}}\"",
+	"Reranking model disabled": "已停用重新排序模型",
+	"Reranking model set to \"{{reranking_model}}\"": "重新排序模型設定為 \"{{reranking_model}}\"",
 	"Reset": "重設",
 	"Reset Upload Directory": "重設上傳目錄",
 	"Reset Vector Storage": "重設向量儲存空間",
-	"Response AutoCopy to Clipboard": "自動複製回答到剪貼簿",
-	"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
+	"Response AutoCopy to Clipboard": "自動將回應複製到剪貼簿",
+	"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "無法啟用回應通知,因為網站權限已遭拒。請前往瀏覽器設定以授予必要存取權限。",
 	"Role": "角色",
 	"Rosé Pine": "玫瑰松",
 	"Rosé Pine Dawn": "黎明玫瑰松",
-	"RTL": "RTL",
-	"Run Llama 2, Code Llama, and other models. Customize and create your own.": "",
+	"RTL": "從右到左",
+	"Run Llama 2, Code Llama, and other models. Customize and create your own.": "執行 Llama 2、Code Llama 和其他模型。自訂並建立您自己的模型。",
 	"Running": "運作中",
 	"Save": "儲存",
 	"Save & Create": "儲存並建立",
 	"Save & Update": "儲存並更新",
-	"Save Tag": "",
-	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "現已不支援將聊天紀錄儲存到瀏覽器儲存空間中。請點選下面的按鈕下載並刪除您的聊天記錄。別擔心,您可以透過以下方式輕鬆地重新匯入您的聊天記錄到後端",
+	"Save Tag": "儲存標籤",
+	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "不再支援直接將對話記錄儲存到您的瀏覽器儲存空間。請點選下方按鈕來下載並刪除您的對話記錄。別擔心,您可以透過以下方式輕鬆地將對話記錄重新匯入後端",
 	"Scan": "掃描",
 	"Scan complete!": "掃描完成!",
 	"Scan for documents from {{path}}": "從 {{path}} 掃描文件",
+	"Scroll to bottom when switching between branches": "切換分支時捲動到底端",
 	"Search": "搜尋",
 	"Search a model": "搜尋模型",
-	"Search Chats": "搜尋聊天",
+	"Search Chats": "搜尋對話",
 	"Search Documents": "搜尋文件",
-	"Search Functions": "搜尋功能",
+	"Search Functions": "搜尋函式",
 	"Search Models": "搜尋模型",
 	"Search Prompts": "搜尋提示詞",
 	"Search Query Generation Prompt": "搜尋查詢生成提示詞",
 	"Search Query Generation Prompt Length Threshold": "搜尋查詢生成提示詞長度閾值",
 	"Search Result Count": "搜尋結果數量",
 	"Search Tools": "搜尋工具",
-	"Searched {{count}} sites_other": "搜尋了 {{count}} 個網站",
+	"Searched {{count}} sites_one": "已搜尋 {{count}} 個網站",
+	"Searched {{count}} sites_other": "已搜尋 {{count}} 個網站",
 	"Searching \"{{searchQuery}}\"": "正在搜尋 \"{{searchQuery}}\"",
 	"Searxng Query URL": "Searxng 查詢 URL",
-	"See readme.md for instructions": "檢視 readme.md 取得指南",
-	"See what's new": "檢視最新內容",
+	"See readme.md for instructions": "檢視 readme.md 以取得說明",
+	"See what's new": "查看新功能",
 	"Seed": "種子",
 	"Select a base model": "選擇基礎模型",
 	"Select a engine": "選擇引擎",
-	"Select a function": "",
+	"Select a function": "選擇函式",
 	"Select a mode": "選擇模式",
-	"Select a model": "選擇一個模型",
+	"Select a model": "選擇模型",
 	"Select a pipeline": "選擇管線",
 	"Select a pipeline url": "選擇管線 URL",
-	"Select a tool": "",
+	"Select a tool": "選擇工具",
 	"Select an Ollama instance": "選擇 Ollama 執行個體",
 	"Select Documents": "選擇文件",
 	"Select model": "選擇模型",
 	"Select only one model to call": "僅選擇一個模型來呼叫",
-	"Selected model(s) do not support image inputs": "已選擇模型不支援影像輸入",
+	"Selected model(s) do not support image inputs": "選取的模型不支援圖片輸入",
 	"Send": "傳送",
 	"Send a Message": "傳送訊息",
 	"Send message": "傳送訊息",
@@ -546,7 +548,7 @@
 	"Serper API Key": "Serper API 金鑰",
 	"Serply API Key": "Serply API 金鑰",
 	"Serpstack API Key": "Serpstack API 金鑰",
-	"Server connection verified": "已驗證伺服器連線",
+	"Server connection verified": "伺服器連線已驗證",
 	"Set as default": "設為預設",
 	"Set Default Model": "設定預設模型",
 	"Set embedding model (e.g. {{model}})": "設定嵌入模型(例如:{{model}})",
@@ -556,35 +558,35 @@
 	"Set Task Model": "設定任務模型",
 	"Set Voice": "設定語音",
 	"Settings": "設定",
-	"Settings saved successfully!": "成功儲存設定",
-	"Settings updated successfully": "設定更新成功",
+	"Settings saved successfully!": "設定已成功儲存",
+	"Settings updated successfully": "設定已成功更新",
 	"Share": "分享",
-	"Share Chat": "分享聊天",
+	"Share Chat": "分享對話",
 	"Share to OpenWebUI Community": "分享到 OpenWebUI 社群",
 	"short-summary": "簡短摘要",
 	"Show": "顯示",
 	"Show Admin Details in Account Pending Overlay": "在帳號待審覆蓋層中顯示管理員詳細資訊",
 	"Show Model": "顯示模型",
-	"Show shortcuts": "顯示快鍵",
-	"Show your support!": "",
-	"Showcased creativity": "展示創造性",
+	"Show shortcuts": "顯示快鍵",
+	"Show your support!": "表示您的支持!",
+	"Showcased creativity": "展現創意",
 	"Sign in": "登入",
 	"Sign Out": "登出",
 	"Sign up": "註冊",
 	"Signing in": "正在登入",
 	"Source": "來源",
-	"Speech recognition error: {{error}}": "語音識錯誤:{{error}}",
-	"Speech-to-Text Engine": "語音轉文字引擎",
+	"Speech recognition error: {{error}}": "語音識錯誤:{{error}}",
+	"Speech-to-Text Engine": "語音轉文字 (STT) 引擎",
 	"Stop Sequence": "停止序列",
-	"STT Model": "STT 模型",
-	"STT Settings": "語音轉文字設定",
+	"STT Model": "語音轉文字 (STT) 模型",
+	"STT Settings": "語音轉文字 (STT) 設定",
 	"Submit": "提交",
 	"Subtitle (e.g. about the Roman Empire)": "副標題(例如:關於羅馬帝國)",
 	"Success": "成功",
 	"Successfully updated.": "更新成功。",
 	"Suggested": "建議",
-	"Support": "",
-	"Support this plugin:": "",
+	"Support": "支援",
+	"Support this plugin:": "支持這個外掛:",
 	"System": "系統",
 	"System Prompt": "系統提示詞",
 	"Tags": "標籤",
@@ -594,94 +596,94 @@
 	"Temperature": "溫度",
 	"Template": "範本",
 	"Text Completion": "文字補全",
-	"Text-to-Speech Engine": "文字轉語音引擎",
+	"Text-to-Speech Engine": "文字轉語音 (TTS) 引擎",
 	"Tfs Z": "Tfs Z",
 	"Thanks for your feedback!": "感謝您的回饋!",
-	"The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "",
-	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "分數應該介於 0.0(0%)和 1.0(100%)之間。",
+	"The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "這個外掛背後的開發者是來自社群的熱情志願者。如果您覺得這個外掛很有幫助,請考慮為其開發做出貢獻。",
+	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "分數應該介於 0.0(0%)和 1.0(100%)之間的值。",
 	"Theme": "主題",
 	"Thinking...": "正在思考...",
-	"This action cannot be undone. Do you wish to continue?": "此動作無法被復原。您想要繼續進行嗎?",
-	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "這確保您寶貴的對話安全地儲存到您的後端資料庫。謝謝!",
-	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "這是一個實驗性功能,可能無法如預期運作,並且隨時可能更改。",
+	"This action cannot be undone. Do you wish to continue?": "此操作無法復原。您確定要繼續進行嗎?",
+	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "這確保您寶貴的對話安全地儲存到您的後端資料庫。謝謝!",
+	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "這是一個實驗性功能,它有可能無法按預期運作,並且可能會隨時變更。",
 	"This setting does not sync across browsers or devices.": "此設定不會在瀏覽器或裝置間同步。",
-	"This will delete": "",
-	"Thorough explanation": "詳細說明",
-	"Tika": "",
-	"Tika Server URL required.": "",
-	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "提示:透過在每次替換後在聊天輸入框中按 Tab 鍵連續更新多個變數。",
+	"This will delete": "這將會刪除",
+	"Thorough explanation": "詳細解釋",
+	"Tika": "Tika",
+	"Tika Server URL required.": "需要 Tika 伺服器 URL。",
+	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "提示:在每次替換後按下對話輸入框中的 Tab 鍵,即可連續更新多個變數欄位。",
 	"Title": "標題",
-	"Title (e.g. Tell me a fun fact)": "標題(例如:告訴我一個有趣的事)",
+	"Title (e.g. Tell me a fun fact)": "標題(例如:告訴我一個有趣的事)",
 	"Title Auto-Generation": "自動產生標題",
-	"Title cannot be an empty string.": "標題不能為空字串",
+	"Title cannot be an empty string.": "標題不能是空字串。",
 	"Title Generation Prompt": "自動產生標題的提示詞",
 	"to": "到",
-	"To access the available model names for downloading,": "若想檢視可供下載的模型名稱,",
-	"To access the GGUF models available for downloading,": "若想檢視可供下載的 GGUF 模型名稱,",
+	"To access the available model names for downloading,": "若要存取可供下載的模型名稱,",
+	"To access the GGUF models available for downloading,": "若要存取可供下載的 GGUF 模型,",
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "若要存取 WebUI,請聯絡管理員。管理員可以從管理面板管理使用者狀態。",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "若要在此新增文件,請先將它們上傳到「文件」工作區。",
-	"to chat input.": "到聊天輸入框來啟動此命令。",
-	"To select actions here, add them to the \"Functions\" workspace first.": "",
-	"To select filters here, add them to the \"Functions\" workspace first.": "若要在此選擇篩選器,請先將它們新增到「功能」工作區。",
+	"to chat input.": "到對話輸入。",
+	"To select actions here, add them to the \"Functions\" workspace first.": "若要在此選擇動作,請先將它們新增到「函式」工作區。",
+	"To select filters here, add them to the \"Functions\" workspace first.": "若要在此選擇篩選器,請先將它們新增到「函式」工作區。",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "若要在此選擇工具包,請先將它們新增到「工具」工作區。",
 	"Today": "今天",
 	"Toggle settings": "切換設定",
 	"Toggle sidebar": "切換側邊欄",
-	"Tokens To Keep On Context Refresh (num_keep)": "上下文重新整理時保留的 Token 數量(num_keep)",
-	"Tool created successfully": "",
-	"Tool deleted successfully": "",
-	"Tool imported successfully": "",
-	"Tool updated successfully": "",
-	"Toolkit Description (e.g. A toolkit for performing various operations)": "",
-	"Toolkit ID (e.g. my_toolkit)": "",
-	"Toolkit Name (e.g. My ToolKit)": "",
+	"Tokens To Keep On Context Refresh (num_keep)": "上下文重新整理時要保留的 token 數 (num_keep)",
+	"Tool created successfully": "成功建立工具",
+	"Tool deleted successfully": "成功刪除工具",
+	"Tool imported successfully": "成功匯入工具",
+	"Tool updated successfully": "成功更新工具",
+	"Toolkit Description (e.g. A toolkit for performing various operations)": "工具包描述(例如:用於執行各種操作的工具包)",
+	"Toolkit ID (e.g. my_toolkit)": "工具包 ID(例如:my_toolkit)",
+	"Toolkit Name (e.g. My ToolKit)": "工具包名稱(例如:我的工具包)",
 	"Tools": "工具",
-	"Tools are a function calling system with arbitrary code execution": "",
-	"Tools have a function calling system that allows arbitrary code execution": "",
-	"Tools have a function calling system that allows arbitrary code execution.": "",
+	"Tools are a function calling system with arbitrary code execution": "工具是一個具有任意程式碼執行功能的函式呼叫系統",
+	"Tools have a function calling system that allows arbitrary code execution": "工具具有允許執行任意程式碼的函式呼叫系統",
+	"Tools have a function calling system that allows arbitrary code execution.": "工具具有允許執行任意程式碼的函式呼叫系統。",
 	"Top K": "Top K",
 	"Top P": "Top P",
 	"Trouble accessing Ollama?": "存取 Ollama 時遇到問題?",
-	"TTS Model": "文字轉語音(TTS)模型",
-	"TTS Settings": "文字轉語音(TTS)設定",
-	"TTS Voice": "文字轉語音(TTS)聲調",
+	"TTS Model": "文字轉語音 (TTS) 模型",
+	"TTS Settings": "文字轉語音 (TTS) 設定",
+	"TTS Voice": "文字轉語音 (TTS) 聲音",
 	"Type": "類型",
-	"Type Hugging Face Resolve (Download) URL": "輸入 Hugging Face 解析後的(下載)URL",
+	"Type Hugging Face Resolve (Download) URL": "輸入 Hugging Face 解析(下載)URL",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "哎呀!連線到 {{provider}} 時出現問題。",
-	"UI": "使用者面",
-	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "未知的檔案類型 '{{file_type}}'。但仍會繼續上傳。",
-	"Unpin": "",
+	"UI": "使用者面",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "未知的檔案類型 '{{file_type}}'。仍然繼續上傳檔案。",
+	"Unpin": "取消釘選",
 	"Update": "更新",
 	"Update and Copy Link": "更新並複製連結",
 	"Update password": "更新密碼",
 	"Updated at": "更新於",
 	"Upload": "上傳",
-	"Upload a GGUF model": "上傳一個 GGUF 模型",
+	"Upload a GGUF model": "上傳 GGUF 模型",
 	"Upload Files": "上傳檔案",
 	"Upload Pipeline": "上傳管線",
 	"Upload Progress": "上傳進度",
 	"URL Mode": "URL 模式",
-	"Use '#' in the prompt input to load and select your documents.": "在輸入框中輸入 '#' 以載入並選擇您的文件。",
+	"Use '#' in the prompt input to load and select your documents.": "在提示詞輸入中使用 '#' 來載入和選擇您的文件。",
 	"Use Gravatar": "使用 Gravatar",
-	"Use Initials": "使用初始頭像",
-	"use_mlock (Ollama)": "use_mlock(Ollama)",
-	"use_mmap (Ollama)": "use_mmap(Ollama)",
+	"Use Initials": "使用姓名縮寫",
+	"use_mlock (Ollama)": "使用 mlock (Ollama)",
+	"use_mmap (Ollama)": "使用 mmap (Ollama)",
 	"user": "使用者",
-	"User location successfully retrieved.": "",
+	"User location successfully retrieved.": "成功取得使用者位置。",
 	"User Permissions": "使用者權限",
 	"Users": "使用者",
 	"Utilize": "使用",
-	"Valid time units:": "有效時間單位:",
-	"Valves": "",
-	"Valves updated": "",
-	"Valves updated successfully": "",
+	"Valid time units:": "有效時間單位:",
+	"Valves": "閥門",
+	"Valves updated": "閥門已更新",
+	"Valves updated successfully": "閥門更新成功",
 	"variable": "變數",
-	"variable to have them replaced with clipboard content.": "變數將替換為剪貼簿內容",
+	"variable to have them replaced with clipboard content.": "變數,以便替換為剪貼簿內容",
 	"Version": "版本",
-	"Voice": "",
+	"Voice": "語音",
 	"Warning": "警告",
-	"Warning:": "",
-	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "警告:如果更新或更改您的嵌入模型,則需要重新匯入所有文件",
+	"Warning:": "警告:",
+	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "警告:如果您更新或更改嵌入模型,您將需要重新匯入所有文件。",
 	"Web": "網頁",
 	"Web API": "網頁 API",
 	"Web Loader Settings": "網頁載入器設定",
@@ -690,24 +692,24 @@
 	"Web Search Engine": "網頁搜尋引擎",
 	"Webhook URL": "Webhook URL",
 	"WebUI Settings": "WebUI 設定",
-	"WebUI will make requests to": "WebUI 將會存取",
-	"What’s New in": "全新內容",
-	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "當歷史被關閉時,這個瀏覽器上的新聊天將不會出現在任何裝置的歷史記錄中",
+	"WebUI will make requests to": "WebUI 將會對以下網址發出請求",
+	"What’s New in": "新功能",
+	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "當歷史記錄關閉時,此瀏覽器上的新對話將不會出現在您任何裝置的歷史記錄中。",
 	"Whisper (Local)": "Whisper(本地)",
 	"Widescreen Mode": "寬螢幕模式",
 	"Workspace": "工作區",
-	"Write a prompt suggestion (e.g. Who are you?)": "寫一個提示詞建議(例如:您是誰?)",
-	"Write a summary in 50 words that summarizes [topic or keyword].": "寫一個 50 字的摘要來概括 [主題或關鍵詞]。",
+	"Write a prompt suggestion (e.g. Who are you?)": "撰寫提示詞建議(例如:你是誰?)",
+	"Write a summary in 50 words that summarizes [topic or keyword].": "用 50 字寫一篇總結 [主題或關鍵字] 的摘要。",
 	"Yesterday": "昨天",
 	"You": "您",
-	"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "您可以透過下方的「管理」按鈕新增記憶,個人化您的 LLM 互動,使其更有幫助並更符合您的需求。",
-	"You cannot clone a base model": "您不能複製基礎模型",
-	"You have no archived conversations.": "您沒有任何已封存的對話",
-	"You have shared this chat": "您已分享此聊天",
-	"You're a helpful assistant.": "您是一位善於協助他人的助手。",
-	"You're now logged in.": "已登入。",
-	"Your account status is currently pending activation.": "您的帳號狀態目前待啟用。",
-	"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "",
-	"Youtube": "Youtube",
-	"Youtube Loader Settings": "Youtube 載入器設定"
+	"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "您可以透過下方的「管理」按鈕新增記憶,將您與大型語言模型的互動個人化,讓它們更有幫助並更符合您的需求。",
+	"You cannot clone a base model": "您無法複製基礎模型",
+	"You have no archived conversations.": "您沒有已封存的對話",
+	"You have shared this chat": "您已分享此對話",
+	"You're a helpful assistant.": "您是一位樂於助人的助手。",
+	"You're now logged in.": "已登入。",
+	"Your account status is currently pending activation.": "您的帳號目前正在等待啟用。",
+	"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "您的所有貢獻將會直接交給外掛開發者;Open WebUI 不會收取任何百分比。然而,所選擇的贊助平臺可能有其自身的費用。",
+	"Youtube": "YouTube",
+	"Youtube Loader Settings": "YouTube 載入器設定"
 }

+ 4 - 0
src/lib/stores/index.ts

@@ -42,6 +42,9 @@ export const showArchivedChats = writable(false);
 export const showChangelog = writable(false);
 export const showCallOverlay = writable(false);
 
+export const scrollPaginationEnabled = writable(false);
+export const currentChatPage = writable(1);
+
 export type Model = OpenAIModel | OllamaModel;
 
 type BaseModel = {
@@ -149,6 +152,7 @@ type Config = {
 		enable_web_search?: boolean;
 		enable_image_generation: boolean;
 		enable_admin_export: boolean;
+		enable_admin_chat_access: boolean;
 		enable_community_sharing: boolean;
 	};
 	oauth: {

+ 5 - 0
src/lib/utils/index.ts

@@ -90,6 +90,11 @@ export const revertSanitizedResponseContent = (content: string) => {
 	return content.replaceAll('&lt;', '<').replaceAll('&gt;', '>');
 };
 
+export function unescapeHtml(html: string) {
+	const doc = new DOMParser().parseFromString(html, 'text/html');
+	return doc.documentElement.textContent;
+}
+
 export const capitalizeFirstLetter = (string) => {
 	return string.charAt(0).toUpperCase() + string.slice(1);
 };

+ 1 - 1
src/routes/(app)/admin/+page.svelte

@@ -307,7 +307,7 @@
 
 						<td class="px-3 py-2 text-right">
 							<div class="flex justify-end w-full">
-								{#if user.role !== 'admin'}
+								{#if $config.features.enable_admin_chat_access && user.role !== 'admin'}
 									<Tooltip content={$i18n.t('Chats')}>
 										<button
 											class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"