Bläddra i källkod

Merge pull request #9190 from open-webui/dev

0.5.8
Timothy Jaeryang Baek 2 månader sedan
förälder
incheckning
dc3b2f1f1e
100 ändrade filer med 2598 tillägg och 988 borttagningar
  1. 29 0
      CHANGELOG.md
  2. 70 12
      backend/open_webui/config.py
  3. 1 1
      backend/open_webui/constants.py
  4. 14 6
      backend/open_webui/env.py
  5. 23 6
      backend/open_webui/main.py
  6. 55 16
      backend/open_webui/retrieval/utils.py
  7. 5 1
      backend/open_webui/retrieval/vector/dbs/milvus.py
  8. 76 0
      backend/open_webui/retrieval/web/exa.py
  9. 4 0
      backend/open_webui/retrieval/web/main.py
  10. 15 4
      backend/open_webui/retrieval/web/utils.py
  11. 14 9
      backend/open_webui/routers/auths.py
  12. 8 2
      backend/open_webui/routers/chats.py
  13. 4 2
      backend/open_webui/routers/files.py
  14. 37 12
      backend/open_webui/routers/knowledge.py
  15. 6 4
      backend/open_webui/routers/memories.py
  16. 5 1
      backend/open_webui/routers/models.py
  17. 5 4
      backend/open_webui/routers/ollama.py
  18. 4 4
      backend/open_webui/routers/openai.py
  19. 5 1
      backend/open_webui/routers/prompts.py
  20. 42 9
      backend/open_webui/routers/retrieval.py
  21. 19 3
      backend/open_webui/routers/tasks.py
  22. 5 1
      backend/open_webui/routers/tools.py
  23. 1 0
      backend/open_webui/routers/users.py
  24. 5 2
      backend/open_webui/socket/main.py
  25. 697 203
      backend/open_webui/utils/middleware.py
  26. 66 5
      backend/open_webui/utils/misc.py
  27. 14 8
      backend/open_webui/utils/oauth.py
  28. 15 10
      backend/open_webui/utils/payload.py
  29. 9 4
      backend/open_webui/utils/plugin.py
  30. 61 2
      backend/open_webui/utils/response.py
  31. 6 0
      backend/open_webui/utils/task.py
  32. 13 0
      backend/open_webui/utils/tools.py
  33. 8 8
      backend/requirements.txt
  34. 18 6
      package-lock.json
  35. 3 2
      package.json
  36. 8 8
      pyproject.toml
  37. 4 1
      scripts/prepare-pyodide.js
  38. 5 2
      src/lib/apis/chats/index.ts
  39. 3 2
      src/lib/apis/index.ts
  40. 1 1
      src/lib/apis/models/index.ts
  41. 1 1
      src/lib/apis/openai/index.ts
  42. 2 2
      src/lib/components/admin/Settings/Audio.svelte
  43. 16 1
      src/lib/components/admin/Settings/Interface.svelte
  44. 13 1
      src/lib/components/admin/Settings/WebSearch.svelte
  45. 2 1
      src/lib/components/admin/Users/Groups.svelte
  46. 2 1
      src/lib/components/admin/Users/Groups/EditGroupModal.svelte
  47. 10 1
      src/lib/components/admin/Users/Groups/Permissions.svelte
  48. 3 1
      src/lib/components/admin/Users/UserList.svelte
  49. 3 1
      src/lib/components/admin/Users/UserList/EditUserModal.svelte
  50. 3 1
      src/lib/components/admin/Users/UserList/UserChatsModal.svelte
  51. 35 33
      src/lib/components/channel/MessageInput.svelte
  52. 4 6
      src/lib/components/channel/Messages/Message.svelte
  53. 103 76
      src/lib/components/chat/Chat.svelte
  54. 168 80
      src/lib/components/chat/MessageInput.svelte
  55. 65 63
      src/lib/components/chat/MessageInput/InputMenu.svelte
  56. 1 1
      src/lib/components/chat/Messages.svelte
  57. 98 92
      src/lib/components/chat/Messages/CodeBlock.svelte
  58. 8 2
      src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte
  59. 2 0
      src/lib/components/chat/Messages/Message.svelte
  60. 6 1
      src/lib/components/chat/Messages/MultiResponseMessages.svelte
  61. 36 1
      src/lib/components/chat/Messages/ResponseMessage.svelte
  62. 3 1
      src/lib/components/chat/Messages/UserMessage.svelte
  63. 8 9
      src/lib/components/chat/Placeholder.svelte
  64. 30 0
      src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte
  65. 1 1
      src/lib/components/chat/Settings/Audio.svelte
  66. 1 0
      src/lib/components/chat/Settings/General.svelte
  67. 28 0
      src/lib/components/chat/Settings/Interface.svelte
  68. 3 3
      src/lib/components/chat/Settings/Personalization/ManageModal.svelte
  69. 2 1
      src/lib/components/chat/SettingsModal.svelte
  70. 114 36
      src/lib/components/chat/Suggestions.svelte
  71. 6 0
      src/lib/components/common/Collapsible.svelte
  72. 1 1
      src/lib/components/common/FileItem.svelte
  73. 32 51
      src/lib/components/common/Textarea.svelte
  74. 19 0
      src/lib/components/icons/CommandLine.svelte
  75. 11 0
      src/lib/components/icons/CommandLineSolid.svelte
  76. 21 0
      src/lib/components/icons/Photo.svelte
  77. 2 2
      src/lib/components/layout/Help.svelte
  78. 4 1
      src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte
  79. 7 1
      src/lib/components/layout/Sidebar/ChatItem.svelte
  80. 26 1
      src/lib/components/playground/Chat.svelte
  81. 76 0
      src/lib/components/playground/Chat/Message.svelte
  82. 8 65
      src/lib/components/playground/Chat/Messages.svelte
  83. 1 1
      src/lib/components/workspace/Knowledge.svelte
  84. 2 2
      src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte
  85. 9 9
      src/lib/components/workspace/Knowledge/KnowledgeBase.svelte
  86. 1 1
      src/lib/components/workspace/Models.svelte
  87. 1 1
      src/lib/components/workspace/Models/ModelEditor.svelte
  88. 1 1
      src/lib/components/workspace/common/ValvesModal.svelte
  89. 18 6
      src/lib/i18n/locales/ar-BH/translation.json
  90. 18 6
      src/lib/i18n/locales/bg-BG/translation.json
  91. 18 6
      src/lib/i18n/locales/bn-BD/translation.json
  92. 18 6
      src/lib/i18n/locales/ca-ES/translation.json
  93. 18 6
      src/lib/i18n/locales/ceb-PH/translation.json
  94. 18 6
      src/lib/i18n/locales/cs-CZ/translation.json
  95. 18 6
      src/lib/i18n/locales/da-DK/translation.json
  96. 18 6
      src/lib/i18n/locales/de-DE/translation.json
  97. 18 6
      src/lib/i18n/locales/dg-DG/translation.json
  98. 18 6
      src/lib/i18n/locales/el-GR/translation.json
  99. 18 6
      src/lib/i18n/locales/en-GB/translation.json
  100. 18 6
      src/lib/i18n/locales/en-US/translation.json

+ 29 - 0
CHANGELOG.md

@@ -5,6 +5,35 @@ 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.5.8] - 2025-02-05
+
+### Added
+
+- **🖥️ Code Interpreter**: Models can now execute code in real time to refine their answers dynamically, running securely within a sandboxed browser environment using Pyodide. Perfect for calculations, data analysis, and AI-assisted coding tasks!
+- **💬 Redesigned Chat Input UI**: Enjoy a sleeker and more intuitive message input with improved feature selection, making it easier than ever to toggle tools, enable search, and interact with AI seamlessly.
+- **🛠️ Native Tool Calling Support (Experimental)**: Supported models can now call tools natively, reducing query latency and improving contextual responses. More enhancements coming soon!
+- **🔗 Exa Search Engine Integration**: A new search provider has been added, allowing users to retrieve up-to-date and relevant information without leaving the chat interface.
+- **🌍 Localized Dates & Times**: Date and time formats now match your system locale, ensuring a more natural, region-specific experience.
+- **📎 User Headers for External Embedding APIs**: API calls to external embedding services now include user-related headers.
+- **🌍 "Always On" Web Search Toggle**: A new option under Settings > Interface allows users to enable Web Search by default—transform Open WebUI into your go-to search engine, ensuring AI-powered results with every query.
+- **🚀 General Performance & Stability**: Significant improvements across the platform for a faster, more reliable experience.
+- **🖼️ UI/UX Enhancements**: Numerous design refinements improving readability, responsiveness, and accessibility.
+- **🌍 Improved Translations**: Chinese, Korean, French, Ukrainian and Serbian translations have been updated with refined terminologies for better clarity.
+
+### Fixed
+
+- **🔄 OAuth Name Field Fallback**: Resolves OAuth login failures by using the email field as a fallback when a name is missing.
+- **🔑 Google Drive Credentials Restriction**: Ensures only authenticated users can access Google Drive credentials for enhanced security.
+- **🌐 DuckDuckGo Search Rate Limit Handling**: Fixes issues where users would encounter 202 errors due to rate limits when using DuckDuckGo for web search.
+- **📁 File Upload Permission Indicator**: Users are now notified when they lack permission to upload files, improving clarity on system restrictions.
+- **🔧 Max Tokens Issue**: Fixes cases where 'max_tokens' were not applied correctly, ensuring proper model behavior.
+- **🔍 Validation for RAG Web Search URLs**: Filters out invalid or unsupported URLs when using web-based retrieval augmentation.
+- **🖋️ Title Generation Bug**: Fixes inconsistencies in title generation, ensuring proper chat organization.
+
+### Removed
+
+- **⚡ Deprecated Non-Web Worker Pyodide Execution**: Moves entirely to browser sandboxing for better performance and security.
+
 ## [0.5.7] - 2025-01-23
 
 ### Added

+ 70 - 12
backend/open_webui/config.py

@@ -927,6 +927,12 @@ USER_PERMISSIONS_FEATURES_IMAGE_GENERATION = (
     == "true"
 )
 
+USER_PERMISSIONS_FEATURES_CODE_INTERPRETER = (
+    os.environ.get("USER_PERMISSIONS_FEATURES_CODE_INTERPRETER", "True").lower()
+    == "true"
+)
+
+
 DEFAULT_USER_PERMISSIONS = {
     "workspace": {
         "models": USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS,
@@ -944,6 +950,7 @@ DEFAULT_USER_PERMISSIONS = {
     "features": {
         "web_search": USER_PERMISSIONS_FEATURES_WEB_SEARCH,
         "image_generation": USER_PERMISSIONS_FEATURES_IMAGE_GENERATION,
+        "code_interpreter": USER_PERMISSIONS_FEATURES_CODE_INTERPRETER,
     },
 }
 
@@ -1094,21 +1101,27 @@ TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
     os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""),
 )
 
-DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """Create a concise, 3-5 word title with an emoji as a title for the chat history, in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
-
-Examples of titles:
-📉 Stock Market Trends
-🍪 Perfect Chocolate Chip Recipe
-Evolution of Music Streaming
-Remote Work Productivity Tips
-Artificial Intelligence in Healthcare
-🎮 Video Game Development Insights
-
+DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """### Task:
+Generate a concise, 3-5 word title with an emoji summarizing the chat history.
+### Guidelines:
+- The title should clearly represent the main theme or subject of the conversation.
+- Use emojis that enhance understanding of the topic, but avoid quotation marks or special formatting.
+- Write the title in the chat's primary language; default to English if multilingual.
+- Prioritize accuracy over excessive creativity; keep it clear and simple.
+### Output:
+JSON format: { "title": "your concise title here" }
+### Examples:
+- { "title": "📉 Stock Market Trends" },
+- { "title": "🍪 Perfect Chocolate Chip Recipe" },
+- { "title": "Evolution of Music Streaming" },
+- { "title": "Remote Work Productivity Tips" },
+- { "title": "Artificial Intelligence in Healthcare" },
+- { "title": "🎮 Video Game Development Insights" }
+### Chat History:
 <chat_history>
 {{MESSAGES:END:2}}
 </chat_history>"""
 
-
 TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
     "TAGS_GENERATION_PROMPT_TEMPLATE",
     "task.tags.prompt_template",
@@ -1277,7 +1290,28 @@ TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
 )
 
 
-DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = """Available Tools: {{TOOLS}}\nReturn an empty string if no tools match the query. If a function tool matches, construct and return a JSON object in the format {\"name\": \"functionName\", \"parameters\": {\"requiredFunctionParamKey\": \"requiredFunctionParamValue\"}} using the appropriate tool and its parameters. Only return the object and limit the response to the JSON object without additional text."""
+DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = """Available Tools: {{TOOLS}}
+
+Your task is to choose and return the correct tool(s) from the list of available tools based on the query. Follow these guidelines:
+
+- Return only the JSON object, without any additional text or explanation.
+
+- If no tools match the query, return an empty array: 
+   {
+     "tool_calls": []
+   }
+
+- If one or more tools match the query, construct a JSON response containing a "tool_calls" array with objects that include:
+   - "name": The tool's name.
+   - "parameters": A dictionary of required parameters and their corresponding values.
+
+The format for the JSON response is strictly:
+{
+  "tool_calls": [
+    {"name": "toolName1", "parameters": {"key1": "value1"}},
+    {"name": "toolName2", "parameters": {"key2": "value2"}}
+  ]
+}"""
 
 
 DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE = """Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱).
@@ -1290,6 +1324,24 @@ Your task is to synthesize these responses into a single, high-quality response.
 
 Responses from models: {{responses}}"""
 
+
+DEFAULT_CODE_INTERPRETER_PROMPT = """
+#### Tools Available
+
+1. **Code Interpreter**: `<code_interpreter type="code" lang="python"></code_interpreter>`
+   - You have access to a Python shell that runs directly in the user's browser, enabling fast execution of code for analysis, calculations, or problem-solving.  Use it in this response.
+   - The Python code you write can incorporate a wide array of libraries, handle data manipulation or visualization, perform API calls for web-related tasks, or tackle virtually any computational challenge. Use this flexibility to **think outside the box, craft elegant solutions, and harness Python's full potential**.
+   - To use it, **you must enclose your code within `<code_interpreter type="code" lang="python">` XML tags** and stop right away. If you don't, the code won't execute. Do NOT use triple backticks.
+   - When coding, **always aim to print meaningful outputs** (e.g., results, tables, summaries, or visuals) to better interpret and verify the findings. Avoid relying on implicit outputs; prioritize explicit and clear print statements so the results are effectively communicated to the user.  
+   - After obtaining the printed output, **always provide a concise analysis, interpretation, or next steps to help the user understand the findings or refine the outcome further.**  
+   - If the results are unclear, unexpected, or require validation, refine the code and execute it again as needed. Always aim to deliver meaningful insights from the results, iterating if necessary.  
+   - If a link is provided for an image, audio, or any file, include it in the response exactly as given to ensure the user has access to the original resource.  
+   - All responses should be communicated in the chat's primary language, ensuring seamless understanding. If the chat is multilingual, default to English for clarity.
+   - **If a link to an image, audio, or any file is provided in markdown format, explicitly display it as part of the response to ensure the user can access it easily, do NOT change the link.**
+
+Ensure that the tools are effectively utilized to achieve the highest-quality analysis for the user."""
+
+
 ####################################
 # Vector Database
 ####################################
@@ -1319,6 +1371,7 @@ CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
 
 MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
 MILVUS_DB = os.environ.get("MILVUS_DB", "default")
+MILVUS_TOKEN = os.environ.get("MILVUS_TOKEN", None)
 
 # Qdrant
 QDRANT_URI = os.environ.get("QDRANT_URI", None)
@@ -1699,6 +1752,11 @@ BING_SEARCH_V7_SUBSCRIPTION_KEY = PersistentConfig(
     os.environ.get("BING_SEARCH_V7_SUBSCRIPTION_KEY", ""),
 )
 
+EXA_API_KEY = PersistentConfig(
+    "EXA_API_KEY",
+    "rag.web.search.exa_api_key",
+    os.getenv("EXA_API_KEY", ""),
+)
 
 RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
     "RAG_WEB_SEARCH_RESULT_COUNT",

+ 1 - 1
backend/open_webui/constants.py

@@ -57,7 +57,7 @@ class ERROR_MESSAGES(str, Enum):
     )
 
     FILE_NOT_SENT = "FILE_NOT_SENT"
-    FILE_NOT_SUPPORTED = "Oops! It seems like the file format you're trying to upload is not supported. Please upload a file with a supported format (e.g., JPG, PNG, PDF, TXT) and try again."
+    FILE_NOT_SUPPORTED = "Oops! It seems like the file format you're trying to upload is not supported. Please upload a file with a supported format and try again."
 
     NOT_FOUND = "We could not find what you're looking for :/"
     USER_NOT_FOUND = "We could not find what you're looking for :/"

+ 14 - 6
backend/open_webui/env.py

@@ -356,14 +356,22 @@ WEBUI_SECRET_KEY = os.environ.get(
     ),  # DEPRECATED: remove at next major version
 )
 
-WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get(
-    "WEBUI_SESSION_COOKIE_SAME_SITE",
-    os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax"),
+WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax")
+
+WEBUI_SESSION_COOKIE_SECURE = (
+    os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true"
+)
+
+WEBUI_AUTH_COOKIE_SAME_SITE = os.environ.get(
+    "WEBUI_AUTH_COOKIE_SAME_SITE", WEBUI_SESSION_COOKIE_SAME_SITE
 )
 
-WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
-    "WEBUI_SESSION_COOKIE_SECURE",
-    os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true",
+WEBUI_AUTH_COOKIE_SECURE = (
+    os.environ.get(
+        "WEBUI_AUTH_COOKIE_SECURE",
+        os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false"),
+    ).lower()
+    == "true"
 )
 
 if WEBUI_AUTH and WEBUI_SECRET_KEY == "":

+ 23 - 6
backend/open_webui/main.py

@@ -177,6 +177,7 @@ from open_webui.config import (
     BING_SEARCH_V7_ENDPOINT,
     BING_SEARCH_V7_SUBSCRIPTION_KEY,
     BRAVE_SEARCH_API_KEY,
+    EXA_API_KEY,
     KAGI_SEARCH_API_KEY,
     MOJEEK_SEARCH_API_KEY,
     GOOGLE_PSE_API_KEY,
@@ -523,6 +524,7 @@ app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE
 app.state.config.JINA_API_KEY = JINA_API_KEY
 app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
 app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
+app.state.config.EXA_API_KEY = EXA_API_KEY
 
 app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
 app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
@@ -859,6 +861,7 @@ async def chat_completion(
         if model_id not in request.app.state.MODELS:
             raise Exception("Model not found")
         model = request.app.state.MODELS[model_id]
+        model_info = Models.get_model_by_id(model_id)
 
         # Check if user has access to the model
         if not BYPASS_MODEL_ACCESS_CONTROL and user.role == "user":
@@ -875,12 +878,25 @@ async def chat_completion(
             "tool_ids": form_data.get("tool_ids", None),
             "files": form_data.get("files", None),
             "features": form_data.get("features", None),
+            "variables": form_data.get("variables", None),
+            "model": model_info,
+            **(
+                {"function_calling": "native"}
+                if form_data.get("params", {}).get("function_calling") == "native"
+                or (
+                    model_info
+                    and model_info.params.model_dump().get("function_calling")
+                    == "native"
+                )
+                else {}
+            ),
         }
         form_data["metadata"] = metadata
 
-        form_data, events = await process_chat_payload(
+        form_data, metadata, events = await process_chat_payload(
             request, form_data, metadata, user, model
         )
+
     except Exception as e:
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
@@ -889,6 +905,7 @@ async def chat_completion(
 
     try:
         response = await chat_completion_handler(request, form_data, user)
+
         return await process_chat_response(
             request, response, form_data, user, events, metadata, tasks
         )
@@ -1007,10 +1024,6 @@ async def get_app_config(request: Request):
                 else {}
             ),
         },
-        "google_drive": {
-            "client_id": GOOGLE_DRIVE_CLIENT_ID.value,
-            "api_key": GOOGLE_DRIVE_API_KEY.value,
-        },
         **(
             {
                 "default_models": app.state.config.DEFAULT_MODELS,
@@ -1030,6 +1043,10 @@ async def get_app_config(request: Request):
                     "max_count": app.state.config.FILE_MAX_COUNT,
                 },
                 "permissions": {**app.state.config.USER_PERMISSIONS},
+                "google_drive": {
+                    "client_id": GOOGLE_DRIVE_CLIENT_ID.value,
+                    "api_key": GOOGLE_DRIVE_API_KEY.value,
+                },
             }
             if user is not None
             else {}
@@ -1063,7 +1080,7 @@ async def get_app_version():
 
 
 @app.get("/api/version/updates")
-async def get_app_latest_release_version():
+async def get_app_latest_release_version(user=Depends(get_verified_user)):
     if OFFLINE_MODE:
         log.debug(
             f"Offline mode is enabled, returning current version as latest version"

+ 55 - 16
backend/open_webui/retrieval/utils.py

@@ -15,8 +15,13 @@ from langchain_core.documents import Document
 from open_webui.config import VECTOR_DB
 from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
 from open_webui.utils.misc import get_last_user_message
+from open_webui.models.users import UserModel
 
-from open_webui.env import SRC_LOG_LEVELS, OFFLINE_MODE
+from open_webui.env import (
+    SRC_LOG_LEVELS,
+    OFFLINE_MODE,
+    ENABLE_FORWARD_USER_INFO_HEADERS,
+)
 
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["RAG"])
@@ -61,9 +66,7 @@ class VectorSearchRetriever(BaseRetriever):
 
 
 def query_doc(
-    collection_name: str,
-    query_embedding: list[float],
-    k: int,
+    collection_name: str, query_embedding: list[float], k: int, user: UserModel = None
 ):
     try:
         result = VECTOR_DB_CLIENT.search(
@@ -259,26 +262,31 @@ def get_embedding_function(
     embedding_batch_size,
 ):
     if embedding_engine == "":
-        return lambda query: embedding_function.encode(query).tolist()
+        return lambda query, user=None: embedding_function.encode(query).tolist()
     elif embedding_engine in ["ollama", "openai"]:
-        func = lambda query: generate_embeddings(
+        func = lambda query, user=None: generate_embeddings(
             engine=embedding_engine,
             model=embedding_model,
             text=query,
             url=url,
             key=key,
+            user=user,
         )
 
-        def generate_multiple(query, func):
+        def generate_multiple(query, user, func):
             if isinstance(query, list):
                 embeddings = []
                 for i in range(0, len(query), embedding_batch_size):
-                    embeddings.extend(func(query[i : i + embedding_batch_size]))
+                    embeddings.extend(
+                        func(query[i : i + embedding_batch_size], user=user)
+                    )
                 return embeddings
             else:
-                return func(query)
+                return func(query, user)
 
-        return lambda query: generate_multiple(query, func)
+        return lambda query, user=None: generate_multiple(query, user, func)
+    else:
+        raise ValueError(f"Unknown embedding engine: {embedding_engine}")
 
 
 def get_sources_from_files(
@@ -423,7 +431,11 @@ def get_model_path(model: str, update_model: bool = False):
 
 
 def generate_openai_batch_embeddings(
-    model: str, texts: list[str], url: str = "https://api.openai.com/v1", key: str = ""
+    model: str,
+    texts: list[str],
+    url: str = "https://api.openai.com/v1",
+    key: str = "",
+    user: UserModel = None,
 ) -> Optional[list[list[float]]]:
     try:
         r = requests.post(
@@ -431,6 +443,16 @@ def generate_openai_batch_embeddings(
             headers={
                 "Content-Type": "application/json",
                 "Authorization": f"Bearer {key}",
+                **(
+                    {
+                        "X-OpenWebUI-User-Name": user.name,
+                        "X-OpenWebUI-User-Id": user.id,
+                        "X-OpenWebUI-User-Email": user.email,
+                        "X-OpenWebUI-User-Role": user.role,
+                    }
+                    if ENABLE_FORWARD_USER_INFO_HEADERS and user
+                    else {}
+                ),
             },
             json={"input": texts, "model": model},
         )
@@ -446,7 +468,7 @@ def generate_openai_batch_embeddings(
 
 
 def generate_ollama_batch_embeddings(
-    model: str, texts: list[str], url: str, key: str = ""
+    model: str, texts: list[str], url: str, key: str = "", user: UserModel = None
 ) -> Optional[list[list[float]]]:
     try:
         r = requests.post(
@@ -454,6 +476,16 @@ def generate_ollama_batch_embeddings(
             headers={
                 "Content-Type": "application/json",
                 "Authorization": f"Bearer {key}",
+                **(
+                    {
+                        "X-OpenWebUI-User-Name": user.name,
+                        "X-OpenWebUI-User-Id": user.id,
+                        "X-OpenWebUI-User-Email": user.email,
+                        "X-OpenWebUI-User-Role": user.role,
+                    }
+                    if ENABLE_FORWARD_USER_INFO_HEADERS
+                    else {}
+                ),
             },
             json={"input": texts, "model": model},
         )
@@ -472,22 +504,29 @@ def generate_ollama_batch_embeddings(
 def generate_embeddings(engine: str, model: str, text: Union[str, list[str]], **kwargs):
     url = kwargs.get("url", "")
     key = kwargs.get("key", "")
+    user = kwargs.get("user")
 
     if engine == "ollama":
         if isinstance(text, list):
             embeddings = generate_ollama_batch_embeddings(
-                **{"model": model, "texts": text, "url": url, "key": key}
+                **{"model": model, "texts": text, "url": url, "key": key, "user": user}
             )
         else:
             embeddings = generate_ollama_batch_embeddings(
-                **{"model": model, "texts": [text], "url": url, "key": key}
+                **{
+                    "model": model,
+                    "texts": [text],
+                    "url": url,
+                    "key": key,
+                    "user": user,
+                }
             )
         return embeddings[0] if isinstance(text, str) else embeddings
     elif engine == "openai":
         if isinstance(text, list):
-            embeddings = generate_openai_batch_embeddings(model, text, url, key)
+            embeddings = generate_openai_batch_embeddings(model, text, url, key, user)
         else:
-            embeddings = generate_openai_batch_embeddings(model, [text], url, key)
+            embeddings = generate_openai_batch_embeddings(model, [text], url, key, user)
 
         return embeddings[0] if isinstance(text, str) else embeddings
 

+ 5 - 1
backend/open_webui/retrieval/vector/dbs/milvus.py

@@ -8,13 +8,17 @@ from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
 from open_webui.config import (
     MILVUS_URI,
     MILVUS_DB,
+    MILVUS_TOKEN,
 )
 
 
 class MilvusClient:
     def __init__(self):
         self.collection_prefix = "open_webui"
-        self.client = Client(uri=MILVUS_URI, database=MILVUS_DB)
+        if MILVUS_TOKEN is None:
+            self.client = Client(uri=MILVUS_URI, database=MILVUS_DB)
+        else:
+            self.client = Client(uri=MILVUS_URI, database=MILVUS_DB, token=MILVUS_TOKEN)
 
     def _result_to_get_result(self, result) -> GetResult:
         ids = []

+ 76 - 0
backend/open_webui/retrieval/web/exa.py

@@ -0,0 +1,76 @@
+import logging
+from dataclasses import dataclass
+from typing import Optional
+
+import requests
+from open_webui.env import SRC_LOG_LEVELS
+from open_webui.retrieval.web.main import SearchResult
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["RAG"])
+
+EXA_API_BASE = "https://api.exa.ai"
+
+
+@dataclass
+class ExaResult:
+    url: str
+    title: str
+    text: str
+
+
+def search_exa(
+    api_key: str,
+    query: str,
+    count: int,
+    filter_list: Optional[list[str]] = None,
+) -> list[SearchResult]:
+    """Search using Exa Search API and return the results as a list of SearchResult objects.
+
+    Args:
+        api_key (str): A Exa Search API key
+        query (str): The query to search for
+        count (int): Number of results to return
+        filter_list (Optional[list[str]]): List of domains to filter results by
+    """
+    log.info(f"Searching with Exa for query: {query}")
+
+    headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
+
+    payload = {
+        "query": query,
+        "numResults": count or 5,
+        "includeDomains": filter_list,
+        "contents": {"text": True, "highlights": True},
+        "type": "auto",  # Use the auto search type (keyword or neural)
+    }
+
+    try:
+        response = requests.post(
+            f"{EXA_API_BASE}/search", headers=headers, json=payload
+        )
+        response.raise_for_status()
+        data = response.json()
+
+        results = []
+        for result in data["results"]:
+            results.append(
+                ExaResult(
+                    url=result["url"],
+                    title=result["title"],
+                    text=result["text"],
+                )
+            )
+
+        log.info(f"Found {len(results)} results")
+        return [
+            SearchResult(
+                link=result.url,
+                title=result.title,
+                snippet=result.text,
+            )
+            for result in results
+        ]
+    except Exception as e:
+        log.error(f"Error searching Exa: {e}")
+        return []

+ 4 - 0
backend/open_webui/retrieval/web/main.py

@@ -1,3 +1,5 @@
+import validators
+
 from typing import Optional
 from urllib.parse import urlparse
 
@@ -10,6 +12,8 @@ def get_filtered_results(results, filter_list):
     filtered_results = []
     for result in results:
         url = result.get("url") or result.get("link", "")
+        if not validators.url(url):
+            continue
         domain = urlparse(url).netloc
         if any(domain.endswith(filtered_domain) for filtered_domain in filter_list):
             filtered_results.append(result)

+ 15 - 4
backend/open_webui/retrieval/web/utils.py

@@ -43,6 +43,17 @@ def validate_url(url: Union[str, Sequence[str]]):
         return False
 
 
+def safe_validate_urls(url: Sequence[str]) -> Sequence[str]:
+    valid_urls = []
+    for u in url:
+        try:
+            if validate_url(u):
+                valid_urls.append(u)
+        except ValueError:
+            continue
+    return valid_urls
+
+
 def resolve_hostname(hostname):
     # Get address information
     addr_info = socket.getaddrinfo(hostname, None)
@@ -86,11 +97,11 @@ def get_web_loader(
     verify_ssl: bool = True,
     requests_per_second: int = 2,
 ):
-    # Check if the URL is valid
-    if not validate_url(urls):
-        raise ValueError(ERROR_MESSAGES.INVALID_URL)
+    # Check if the URLs are valid
+    safe_urls = safe_validate_urls([urls] if isinstance(urls, str) else urls)
+
     return SafeWebBaseLoader(
-        urls,
+        safe_urls,
         verify_ssl=verify_ssl,
         requests_per_second=requests_per_second,
         continue_on_failure=True,

+ 14 - 9
backend/open_webui/routers/auths.py

@@ -25,8 +25,8 @@ from open_webui.env import (
     WEBUI_AUTH,
     WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
     WEBUI_AUTH_TRUSTED_NAME_HEADER,
-    WEBUI_SESSION_COOKIE_SAME_SITE,
-    WEBUI_SESSION_COOKIE_SECURE,
+    WEBUI_AUTH_COOKIE_SAME_SITE,
+    WEBUI_AUTH_COOKIE_SECURE,
     SRC_LOG_LEVELS,
 )
 from fastapi import APIRouter, Depends, HTTPException, Request, status
@@ -95,8 +95,8 @@ async def get_session_user(
         value=token,
         expires=datetime_expires_at,
         httponly=True,  # Ensures the cookie is not accessible via JavaScript
-        samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
-        secure=WEBUI_SESSION_COOKIE_SECURE,
+        samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
+        secure=WEBUI_AUTH_COOKIE_SECURE,
     )
 
     user_permissions = get_permissions(
@@ -164,7 +164,7 @@ async def update_password(
 ############################
 # LDAP Authentication
 ############################
-@router.post("/ldap", response_model=SigninResponse)
+@router.post("/ldap", response_model=SessionUserResponse)
 async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
     ENABLE_LDAP = request.app.state.config.ENABLE_LDAP
     LDAP_SERVER_LABEL = request.app.state.config.LDAP_SERVER_LABEL
@@ -288,6 +288,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
                     httponly=True,  # Ensures the cookie is not accessible via JavaScript
                 )
 
+                user_permissions = get_permissions(
+                    user.id, request.app.state.config.USER_PERMISSIONS
+                )
+
                 return {
                     "token": token,
                     "token_type": "Bearer",
@@ -296,6 +300,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
                     "name": user.name,
                     "role": user.role,
                     "profile_image_url": user.profile_image_url,
+                    "permissions": user_permissions,
                 }
             else:
                 raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
@@ -378,8 +383,8 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
             value=token,
             expires=datetime_expires_at,
             httponly=True,  # Ensures the cookie is not accessible via JavaScript
-            samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
-            secure=WEBUI_SESSION_COOKIE_SECURE,
+            samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
+            secure=WEBUI_AUTH_COOKIE_SECURE,
         )
 
         user_permissions = get_permissions(
@@ -473,8 +478,8 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
                 value=token,
                 expires=datetime_expires_at,
                 httponly=True,  # Ensures the cookie is not accessible via JavaScript
-                samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
-                secure=WEBUI_SESSION_COOKIE_SECURE,
+                samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
+                secure=WEBUI_AUTH_COOKIE_SECURE,
             )
 
             if request.app.state.config.WEBHOOK_URL:

+ 8 - 2
backend/open_webui/routers/chats.py

@@ -444,15 +444,21 @@ async def pin_chat_by_id(id: str, user=Depends(get_verified_user)):
 ############################
 
 
+class CloneForm(BaseModel):
+    title: Optional[str] = None
+
+
 @router.post("/{id}/clone", response_model=Optional[ChatResponse])
-async def clone_chat_by_id(id: str, user=Depends(get_verified_user)):
+async def clone_chat_by_id(
+    form_data: CloneForm, id: str, user=Depends(get_verified_user)
+):
     chat = Chats.get_chat_by_id_and_user_id(id, user.id)
     if chat:
         updated_chat = {
             **chat.chat,
             "originalChatId": chat.id,
             "branchPointMessageId": chat.chat["history"]["currentId"],
-            "title": f"Clone of {chat.title}",
+            "title": form_data.title if form_data.title else f"Clone of {chat.title}",
         }
 
         chat = Chats.insert_new_chat(user.id, ChatForm(**{"chat": updated_chat}))

+ 4 - 2
backend/open_webui/routers/files.py

@@ -71,7 +71,7 @@ def upload_file(
         )
 
         try:
-            process_file(request, ProcessFileForm(file_id=id))
+            process_file(request, ProcessFileForm(file_id=id), user=user)
             file_item = Files.get_file_by_id(id=id)
         except Exception as e:
             log.exception(e)
@@ -193,7 +193,9 @@ async def update_file_data_content_by_id(
     if file and (file.user_id == user.id or user.role == "admin"):
         try:
             process_file(
-                request, ProcessFileForm(file_id=id, content=form_data.content)
+                request,
+                ProcessFileForm(file_id=id, content=form_data.content),
+                user=user,
             )
             file = Files.get_file_by_id(id=id)
         except Exception as e:

+ 37 - 12
backend/open_webui/routers/knowledge.py

@@ -264,7 +264,11 @@ def add_file_to_knowledge_by_id(
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -285,7 +289,9 @@ def add_file_to_knowledge_by_id(
     # Add content to the vector database
     try:
         process_file(
-            request, ProcessFileForm(file_id=form_data.file_id, collection_name=id)
+            request,
+            ProcessFileForm(file_id=form_data.file_id, collection_name=id),
+            user=user,
         )
     except Exception as e:
         log.debug(e)
@@ -342,7 +348,12 @@ def update_file_from_knowledge_by_id(
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+    ):
+
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -363,7 +374,9 @@ def update_file_from_knowledge_by_id(
     # Add content to the vector database
     try:
         process_file(
-            request, ProcessFileForm(file_id=form_data.file_id, collection_name=id)
+            request,
+            ProcessFileForm(file_id=form_data.file_id, collection_name=id),
+            user=user,
         )
     except Exception as e:
         raise HTTPException(
@@ -406,7 +419,11 @@ def remove_file_from_knowledge_by_id(
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -429,10 +446,6 @@ def remove_file_from_knowledge_by_id(
     if VECTOR_DB_CLIENT.has_collection(collection_name=file_collection):
         VECTOR_DB_CLIENT.delete_collection(collection_name=file_collection)
 
-    # Delete physical file
-    if file.path:
-        Storage.delete_file(file.path)
-
     # Delete file from database
     Files.delete_file_by_id(form_data.file_id)
 
@@ -484,7 +497,11 @@ async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)):
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -543,7 +560,11 @@ async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)):
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -582,7 +603,11 @@ def add_files_to_knowledge_batch(
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,

+ 6 - 4
backend/open_webui/routers/memories.py

@@ -57,7 +57,7 @@ async def add_memory(
             {
                 "id": memory.id,
                 "text": memory.content,
-                "vector": request.app.state.EMBEDDING_FUNCTION(memory.content),
+                "vector": request.app.state.EMBEDDING_FUNCTION(memory.content, user),
                 "metadata": {"created_at": memory.created_at},
             }
         ],
@@ -82,7 +82,7 @@ async def query_memory(
 ):
     results = VECTOR_DB_CLIENT.search(
         collection_name=f"user-memory-{user.id}",
-        vectors=[request.app.state.EMBEDDING_FUNCTION(form_data.content)],
+        vectors=[request.app.state.EMBEDDING_FUNCTION(form_data.content, user)],
         limit=form_data.k,
     )
 
@@ -105,7 +105,7 @@ async def reset_memory_from_vector_db(
             {
                 "id": memory.id,
                 "text": memory.content,
-                "vector": request.app.state.EMBEDDING_FUNCTION(memory.content),
+                "vector": request.app.state.EMBEDDING_FUNCTION(memory.content, user),
                 "metadata": {
                     "created_at": memory.created_at,
                     "updated_at": memory.updated_at,
@@ -160,7 +160,9 @@ async def update_memory_by_id(
                 {
                     "id": memory.id,
                     "text": memory.content,
-                    "vector": request.app.state.EMBEDDING_FUNCTION(memory.content),
+                    "vector": request.app.state.EMBEDDING_FUNCTION(
+                        memory.content, user
+                    ),
                     "metadata": {
                         "created_at": memory.created_at,
                         "updated_at": memory.updated_at,

+ 5 - 1
backend/open_webui/routers/models.py

@@ -183,7 +183,11 @@ async def delete_model_by_id(id: str, user=Depends(get_verified_user)):
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if model.user_id != user.id and user.role != "admin":
+    if (
+        user.role != "admin"
+        and model.user_id != user.id
+        and not has_access(user.id, "write", model.access_control)
+    ):
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail=ERROR_MESSAGES.UNAUTHORIZED,

+ 5 - 4
backend/open_webui/routers/ollama.py

@@ -395,7 +395,7 @@ async def get_ollama_tags(
             )
 
     if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
-        models["models"] = get_filtered_models(models, user)
+        models["models"] = await get_filtered_models(models, user)
 
     return models
 
@@ -939,6 +939,7 @@ async def generate_completion(
 class ChatMessage(BaseModel):
     role: str
     content: str
+    tool_calls: Optional[list[dict]] = None
     images: Optional[list[str]] = None
 
 
@@ -950,6 +951,7 @@ class GenerateChatCompletionForm(BaseModel):
     template: Optional[str] = None
     stream: Optional[bool] = True
     keep_alive: Optional[Union[int, str]] = None
+    tools: Optional[list[dict]] = None
 
 
 async def get_ollama_url(request: Request, model: str, url_idx: Optional[int] = None):
@@ -977,6 +979,7 @@ async def generate_chat_completion(
     if BYPASS_MODEL_ACCESS_CONTROL:
         bypass_filter = True
 
+    metadata = form_data.pop("metadata", None)
     try:
         form_data = GenerateChatCompletionForm(**form_data)
     except Exception as e:
@@ -987,8 +990,6 @@ async def generate_chat_completion(
         )
 
     payload = {**form_data.model_dump(exclude_none=True)}
-    if "metadata" in payload:
-        del payload["metadata"]
 
     model_id = payload["model"]
     model_info = Models.get_model_by_id(model_id)
@@ -1006,7 +1007,7 @@ async def generate_chat_completion(
             payload["options"] = apply_model_params_to_body_ollama(
                 params, payload["options"]
             )
-            payload = apply_model_system_prompt_to_body(params, payload, user)
+            payload = apply_model_system_prompt_to_body(params, payload, metadata)
 
         # Check if user has access to the model
         if not bypass_filter and user.role == "user":

+ 4 - 4
backend/open_webui/routers/openai.py

@@ -489,7 +489,7 @@ async def get_models(
                 raise HTTPException(status_code=500, detail=error_detail)
 
     if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
-        models["data"] = get_filtered_models(models, user)
+        models["data"] = await get_filtered_models(models, user)
 
     return models
 
@@ -551,9 +551,9 @@ async def generate_chat_completion(
         bypass_filter = True
 
     idx = 0
+
     payload = {**form_data}
-    if "metadata" in payload:
-        del payload["metadata"]
+    metadata = payload.pop("metadata", None)
 
     model_id = form_data.get("model")
     model_info = Models.get_model_by_id(model_id)
@@ -566,7 +566,7 @@ async def generate_chat_completion(
 
         params = model_info.params.model_dump()
         payload = apply_model_params_to_body_openai(params, payload)
-        payload = apply_model_system_prompt_to_body(params, payload, user)
+        payload = apply_model_system_prompt_to_body(params, payload, metadata)
 
         # Check if user has access to the model
         if not bypass_filter and user.role == "user":

+ 5 - 1
backend/open_webui/routers/prompts.py

@@ -147,7 +147,11 @@ async def delete_prompt_by_command(command: str, user=Depends(get_verified_user)
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if prompt.user_id != user.id and user.role != "admin":
+    if (
+        prompt.user_id != user.id
+        and not has_access(user.id, "write", prompt.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,

+ 42 - 9
backend/open_webui/routers/retrieval.py

@@ -55,6 +55,7 @@ from open_webui.retrieval.web.serply import search_serply
 from open_webui.retrieval.web.serpstack import search_serpstack
 from open_webui.retrieval.web.tavily import search_tavily
 from open_webui.retrieval.web.bing import search_bing
+from open_webui.retrieval.web.exa import search_exa
 
 
 from open_webui.retrieval.utils import (
@@ -388,6 +389,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
                 "jina_api_key": request.app.state.config.JINA_API_KEY,
                 "bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
                 "bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
+                "exa_api_key": request.app.state.config.EXA_API_KEY,
                 "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
                 "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
             },
@@ -436,6 +438,7 @@ class WebSearchConfig(BaseModel):
     jina_api_key: Optional[str] = None
     bing_search_v7_endpoint: Optional[str] = None
     bing_search_v7_subscription_key: Optional[str] = None
+    exa_api_key: Optional[str] = None
     result_count: Optional[int] = None
     concurrent_requests: Optional[int] = None
 
@@ -542,6 +545,8 @@ async def update_rag_config(
             form_data.web.search.bing_search_v7_subscription_key
         )
 
+        request.app.state.config.EXA_API_KEY = form_data.web.search.exa_api_key
+
         request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = (
             form_data.web.search.result_count
         )
@@ -591,6 +596,7 @@ async def update_rag_config(
                 "jina_api_key": request.app.state.config.JINA_API_KEY,
                 "bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
                 "bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
+                "exa_api_key": request.app.state.config.EXA_API_KEY,
                 "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
                 "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
             },
@@ -660,6 +666,7 @@ def save_docs_to_vector_db(
     overwrite: bool = False,
     split: bool = True,
     add: bool = False,
+    user=None,
 ) -> bool:
     def _get_docs_info(docs: list[Document]) -> str:
         docs_info = set()
@@ -775,7 +782,7 @@ def save_docs_to_vector_db(
         )
 
         embeddings = embedding_function(
-            list(map(lambda x: x.replace("\n", " "), texts))
+            list(map(lambda x: x.replace("\n", " "), texts)), user=user
         )
 
         items = [
@@ -933,6 +940,7 @@ def process_file(
                     "hash": hash,
                 },
                 add=(True if form_data.collection_name else False),
+                user=user,
             )
 
             if result:
@@ -990,7 +998,7 @@ def process_text(
     text_content = form_data.content
     log.debug(f"text_content: {text_content}")
 
-    result = save_docs_to_vector_db(request, docs, collection_name)
+    result = save_docs_to_vector_db(request, docs, collection_name, user=user)
     if result:
         return {
             "status": True,
@@ -1023,7 +1031,9 @@ def process_youtube_video(
         content = " ".join([doc.page_content for doc in docs])
         log.debug(f"text_content: {content}")
 
-        save_docs_to_vector_db(request, docs, collection_name, overwrite=True)
+        save_docs_to_vector_db(
+            request, docs, collection_name, overwrite=True, user=user
+        )
 
         return {
             "status": True,
@@ -1064,7 +1074,9 @@ def process_web(
         content = " ".join([doc.page_content for doc in docs])
 
         log.debug(f"text_content: {content}")
-        save_docs_to_vector_db(request, docs, collection_name, overwrite=True)
+        save_docs_to_vector_db(
+            request, docs, collection_name, overwrite=True, user=user
+        )
 
         return {
             "status": True,
@@ -1099,6 +1111,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
     - SERPER_API_KEY
     - SERPLY_API_KEY
     - TAVILY_API_KEY
+    - EXA_API_KEY
     - SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
     Args:
         query (str): The query to search for
@@ -1233,6 +1246,13 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
             request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
             request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
         )
+    elif engine == "exa":
+        return search_exa(
+            request.app.state.config.EXA_API_KEY,
+            query,
+            request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
+            request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+        )
     else:
         raise Exception("No search engine API key found in environment variables")
 
@@ -1272,7 +1292,9 @@ def process_web_search(
             requests_per_second=request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
         )
         docs = loader.load()
-        save_docs_to_vector_db(request, docs, collection_name, overwrite=True)
+        save_docs_to_vector_db(
+            request, docs, collection_name, overwrite=True, user=user
+        )
 
         return {
             "status": True,
@@ -1306,7 +1328,9 @@ def query_doc_handler(
             return query_doc_with_hybrid_search(
                 collection_name=form_data.collection_name,
                 query=form_data.query,
-                embedding_function=request.app.state.EMBEDDING_FUNCTION,
+                embedding_function=lambda query: request.app.state.EMBEDDING_FUNCTION(
+                    query, user=user
+                ),
                 k=form_data.k if form_data.k else request.app.state.config.TOP_K,
                 reranking_function=request.app.state.rf,
                 r=(
@@ -1314,12 +1338,16 @@ def query_doc_handler(
                     if form_data.r
                     else request.app.state.config.RELEVANCE_THRESHOLD
                 ),
+                user=user,
             )
         else:
             return query_doc(
                 collection_name=form_data.collection_name,
-                query_embedding=request.app.state.EMBEDDING_FUNCTION(form_data.query),
+                query_embedding=request.app.state.EMBEDDING_FUNCTION(
+                    form_data.query, user=user
+                ),
                 k=form_data.k if form_data.k else request.app.state.config.TOP_K,
+                user=user,
             )
     except Exception as e:
         log.exception(e)
@@ -1348,7 +1376,9 @@ def query_collection_handler(
             return query_collection_with_hybrid_search(
                 collection_names=form_data.collection_names,
                 queries=[form_data.query],
-                embedding_function=request.app.state.EMBEDDING_FUNCTION,
+                embedding_function=lambda query: request.app.state.EMBEDDING_FUNCTION(
+                    query, user=user
+                ),
                 k=form_data.k if form_data.k else request.app.state.config.TOP_K,
                 reranking_function=request.app.state.rf,
                 r=(
@@ -1361,7 +1391,9 @@ def query_collection_handler(
             return query_collection(
                 collection_names=form_data.collection_names,
                 queries=[form_data.query],
-                embedding_function=request.app.state.EMBEDDING_FUNCTION,
+                embedding_function=lambda query: request.app.state.EMBEDDING_FUNCTION(
+                    query, user=user
+                ),
                 k=form_data.k if form_data.k else request.app.state.config.TOP_K,
             )
 
@@ -1509,6 +1541,7 @@ def process_files_batch(
                 docs=all_docs,
                 collection_name=collection_name,
                 add=True,
+                user=user,
             )
 
             # Update all files with collection name

+ 19 - 3
backend/open_webui/routers/tasks.py

@@ -4,6 +4,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
 from pydantic import BaseModel
 from typing import Optional
 import logging
+import re
 
 from open_webui.utils.chat import generate_chat_completion
 from open_webui.utils.task import (
@@ -89,6 +90,10 @@ async def update_task_config(
         form_data.TITLE_GENERATION_PROMPT_TEMPLATE
     )
 
+    request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = (
+        form_data.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
+    )
+
     request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = (
         form_data.ENABLE_AUTOCOMPLETE_GENERATION
     )
@@ -161,9 +166,20 @@ async def generate_title(
     else:
         template = DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE
 
+    messages = form_data["messages"]
+
+    # Remove reasoning details from the messages
+    for message in messages:
+        message["content"] = re.sub(
+            r"<details\s+type=\"reasoning\"[^>]*>.*?<\/details>",
+            "",
+            message["content"],
+            flags=re.S,
+        ).strip()
+
     content = title_generation_template(
         template,
-        form_data["messages"],
+        messages,
         {
             "name": user.name,
             "location": user.info.get("location") if user.info else None,
@@ -175,10 +191,10 @@ async def generate_title(
         "messages": [{"role": "user", "content": content}],
         "stream": False,
         **(
-            {"max_tokens": 50}
+            {"max_tokens": 1000}
             if models[task_model_id]["owned_by"] == "ollama"
             else {
-                "max_completion_tokens": 50,
+                "max_completion_tokens": 1000,
             }
         ),
         "metadata": {

+ 5 - 1
backend/open_webui/routers/tools.py

@@ -227,7 +227,11 @@ async def delete_tools_by_id(
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
 
-    if tools.user_id != user.id and user.role != "admin":
+    if (
+        tools.user_id != user.id
+        and not has_access(user.id, "write", tools.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail=ERROR_MESSAGES.UNAUTHORIZED,

+ 1 - 0
backend/open_webui/routers/users.py

@@ -79,6 +79,7 @@ class ChatPermissions(BaseModel):
 class FeaturesPermissions(BaseModel):
     web_search: bool = True
     image_generation: bool = True
+    code_interpreter: bool = True
 
 
 class UserPermissions(BaseModel):

+ 5 - 2
backend/open_webui/socket/main.py

@@ -325,7 +325,7 @@ def get_event_emitter(request_info):
 
 
 def get_event_call(request_info):
-    async def __event_call__(event_data):
+    async def __event_caller__(event_data):
         response = await sio.call(
             "chat-events",
             {
@@ -337,7 +337,10 @@ def get_event_call(request_info):
         )
         return response
 
-    return __event_call__
+    return __event_caller__
+
+
+get_event_caller = get_event_call
 
 
 def get_user_id_from_session_pool(sid):

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 697 - 203
backend/open_webui/utils/middleware.py


+ 66 - 5
backend/open_webui/utils/misc.py

@@ -7,6 +7,18 @@ from pathlib import Path
 from typing import Callable, Optional
 
 
+import collections.abc
+
+
+def deep_update(d, u):
+    for k, v in u.items():
+        if isinstance(v, collections.abc.Mapping):
+            d[k] = deep_update(d.get(k, {}), v)
+        else:
+            d[k] = v
+    return d
+
+
 def get_message_list(messages, message_id):
     """
     Reconstructs a list of messages in order up to the specified message_id.
@@ -20,7 +32,7 @@ def get_message_list(messages, message_id):
     current_message = messages.get(message_id)
 
     if not current_message:
-        return f"Message ID {message_id} not found in the history."
+        return None
 
     # Reconstruct the chain by following the parentId links
     message_list = []
@@ -131,6 +143,44 @@ def add_or_update_system_message(content: str, messages: list[dict]):
     return messages
 
 
+def add_or_update_user_message(content: str, messages: list[dict]):
+    """
+    Adds a new user message at the end of the messages list
+    or updates the existing user message at the end.
+
+    :param msg: The message to be added or appended.
+    :param messages: The list of message dictionaries.
+    :return: The updated list of message dictionaries.
+    """
+
+    if messages and messages[-1].get("role") == "user":
+        messages[-1]["content"] = f"{messages[-1]['content']}\n{content}"
+    else:
+        # Insert at the end
+        messages.append({"role": "user", "content": content})
+
+    return messages
+
+
+def append_or_update_assistant_message(content: str, messages: list[dict]):
+    """
+    Adds a new assistant message at the end of the messages list
+    or updates the existing assistant message at the end.
+
+    :param msg: The message to be added or appended.
+    :param messages: The list of message dictionaries.
+    :return: The updated list of message dictionaries.
+    """
+
+    if messages and messages[-1].get("role") == "assistant":
+        messages[-1]["content"] = f"{messages[-1]['content']}\n{content}"
+    else:
+        # Insert at the end
+        messages.append({"role": "assistant", "content": content})
+
+    return messages
+
+
 def openai_chat_message_template(model: str):
     return {
         "id": f"{model}-{str(uuid.uuid4())}",
@@ -141,13 +191,24 @@ def openai_chat_message_template(model: str):
 
 
 def openai_chat_chunk_message_template(
-    model: str, message: Optional[str] = None, usage: Optional[dict] = None
+    model: str,
+    content: Optional[str] = None,
+    tool_calls: Optional[list[dict]] = None,
+    usage: Optional[dict] = None,
 ) -> dict:
     template = openai_chat_message_template(model)
     template["object"] = "chat.completion.chunk"
-    if message:
-        template["choices"][0]["delta"] = {"content": message}
-    else:
+
+    template["choices"][0]["index"] = 0
+    template["choices"][0]["delta"] = {}
+
+    if content:
+        template["choices"][0]["delta"]["content"] = content
+
+    if tool_calls:
+        template["choices"][0]["delta"]["tool_calls"] = tool_calls
+
+    if not content and not tool_calls:
         template["choices"][0]["finish_reason"] = "stop"
 
     if usage:

+ 14 - 8
backend/open_webui/utils/oauth.py

@@ -35,7 +35,7 @@ from open_webui.config import (
     AppConfig,
 )
 from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
-from open_webui.env import WEBUI_SESSION_COOKIE_SAME_SITE, WEBUI_SESSION_COOKIE_SECURE
+from open_webui.env import WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SECURE
 from open_webui.utils.misc import parse_duration
 from open_webui.utils.auth import get_password_hash, create_token
 from open_webui.utils.webhook import post_webhook
@@ -82,7 +82,8 @@ class OAuthManager:
             oauth_allowed_roles = auth_manager_config.OAUTH_ALLOWED_ROLES
             oauth_admin_roles = auth_manager_config.OAUTH_ADMIN_ROLES
             oauth_roles = None
-            role = "pending"  # Default/fallback role if no matching roles are found
+            # Default/fallback role if no matching roles are found
+            role = auth_manager_config.DEFAULT_USER_ROLE
 
             # Next block extracts the roles from the user data, accepting nested claims of any depth
             if oauth_claim and oauth_allowed_roles and oauth_admin_roles:
@@ -273,11 +274,16 @@ class OAuthManager:
                         log.error(
                             f"Error downloading profile image '{picture_url}': {e}"
                         )
-                        picture_url = ""
+                        picture_url = "/user.png"
                 if not picture_url:
                     picture_url = "/user.png"
+
                 username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
 
+                name = user_data.get(username_claim)
+                if not isinstance(user, str):
+                    name = email
+
                 role = self.get_user_role(None, user_data)
 
                 user = Auths.insert_new_auth(
@@ -285,7 +291,7 @@ class OAuthManager:
                     password=get_password_hash(
                         str(uuid.uuid4())
                     ),  # Random password, not used
-                    name=user_data.get(username_claim, "User"),
+                    name=name,
                     profile_image_url=picture_url,
                     role=role,
                     oauth_sub=provider_sub,
@@ -323,8 +329,8 @@ class OAuthManager:
             key="token",
             value=jwt_token,
             httponly=True,  # Ensures the cookie is not accessible via JavaScript
-            samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
-            secure=WEBUI_SESSION_COOKIE_SECURE,
+            samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
+            secure=WEBUI_AUTH_COOKIE_SECURE,
         )
 
         if ENABLE_OAUTH_SIGNUP.value:
@@ -333,8 +339,8 @@ class OAuthManager:
                 key="oauth_id_token",
                 value=oauth_id_token,
                 httponly=True,
-                samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
-                secure=WEBUI_SESSION_COOKIE_SECURE,
+                samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
+                secure=WEBUI_AUTH_COOKIE_SECURE,
             )
         # Redirect back to the frontend with the JWT token
         redirect_url = f"{request.base_url}auth#token={jwt_token}"

+ 15 - 10
backend/open_webui/utils/payload.py

@@ -1,4 +1,4 @@
-from open_webui.utils.task import prompt_template
+from open_webui.utils.task import prompt_variables_template
 from open_webui.utils.misc import (
     add_or_update_system_message,
 )
@@ -7,19 +7,18 @@ from typing import Callable, Optional
 
 
 # inplace function: form_data is modified
-def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict:
+def apply_model_system_prompt_to_body(
+    params: dict, form_data: dict, metadata: Optional[dict] = None
+) -> 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)
+    if metadata:
+        print("apply_model_system_prompt_to_body: metadata", metadata)
+        variables = metadata.get("variables", {})
+        system = prompt_variables_template(system, variables)
+
     form_data["messages"] = add_or_update_system_message(
         system, form_data.get("messages", [])
     )
@@ -155,6 +154,9 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
     )
     ollama_payload["stream"] = openai_payload.get("stream", False)
 
+    if "tools" in openai_payload:
+        ollama_payload["tools"] = openai_payload["tools"]
+
     if "format" in openai_payload:
         ollama_payload["format"] = openai_payload["format"]
 
@@ -188,4 +190,7 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
     if ollama_options:
         ollama_payload["options"] = ollama_options
 
+    if "metadata" in openai_payload:
+        ollama_payload["metadata"] = openai_payload["metadata"]
+
     return ollama_payload

+ 9 - 4
backend/open_webui/utils/plugin.py

@@ -167,9 +167,14 @@ def load_function_module_by_id(function_id, content=None):
 
 def install_frontmatter_requirements(requirements):
     if requirements:
-        req_list = [req.strip() for req in requirements.split(",")]
-        for req in req_list:
-            log.info(f"Installing requirement: {req}")
-            subprocess.check_call([sys.executable, "-m", "pip", "install", req])
+        try:
+            req_list = [req.strip() for req in requirements.split(",")]
+            for req in req_list:
+                log.info(f"Installing requirement: {req}")
+                subprocess.check_call([sys.executable, "-m", "pip", "install", req])
+        except Exception as e:
+            log.error(f"Error installing package: {req}")
+            raise e
+
     else:
         log.info("No requirements found in frontmatter.")

+ 61 - 2
backend/open_webui/utils/response.py

@@ -1,4 +1,5 @@
 import json
+from uuid import uuid4
 from open_webui.utils.misc import (
     openai_chat_chunk_message_template,
     openai_chat_completion_message_template,
@@ -9,7 +10,48 @@ def convert_response_ollama_to_openai(ollama_response: dict) -> dict:
     model = ollama_response.get("model", "ollama")
     message_content = ollama_response.get("message", {}).get("content", "")
 
-    response = openai_chat_completion_message_template(model, message_content)
+    data = ollama_response
+    usage = {
+        "response_token/s": (
+            round(
+                (
+                    (
+                        data.get("eval_count", 0)
+                        / ((data.get("eval_duration", 0) / 10_000_000))
+                    )
+                    * 100
+                ),
+                2,
+            )
+            if data.get("eval_duration", 0) > 0
+            else "N/A"
+        ),
+        "prompt_token/s": (
+            round(
+                (
+                    (
+                        data.get("prompt_eval_count", 0)
+                        / ((data.get("prompt_eval_duration", 0) / 10_000_000))
+                    )
+                    * 100
+                ),
+                2,
+            )
+            if data.get("prompt_eval_duration", 0) > 0
+            else "N/A"
+        ),
+        "total_duration": data.get("total_duration", 0),
+        "load_duration": data.get("load_duration", 0),
+        "prompt_eval_count": data.get("prompt_eval_count", 0),
+        "prompt_eval_duration": data.get("prompt_eval_duration", 0),
+        "eval_count": data.get("eval_count", 0),
+        "eval_duration": data.get("eval_duration", 0),
+        "approximate_total": (lambda s: f"{s // 3600}h{(s % 3600) // 60}m{s % 60}s")(
+            (data.get("total_duration", 0) or 0) // 1_000_000_000
+        ),
+    }
+
+    response = openai_chat_completion_message_template(model, message_content, usage)
     return response
 
 
@@ -19,6 +61,23 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response)
 
         model = data.get("model", "ollama")
         message_content = data.get("message", {}).get("content", "")
+        tool_calls = data.get("message", {}).get("tool_calls", None)
+        openai_tool_calls = None
+
+        if tool_calls:
+            openai_tool_calls = []
+            for tool_call in tool_calls:
+                openai_tool_call = {
+                    "index": tool_call.get("index", 0),
+                    "id": tool_call.get("id", f"call_{str(uuid4())}"),
+                    "type": "function",
+                    "function": {
+                        "name": tool_call.get("function", {}).get("name", ""),
+                        "arguments": f"{tool_call.get('function', {}).get('arguments', {})}",
+                    },
+                }
+                openai_tool_calls.append(openai_tool_call)
+
         done = data.get("done", False)
 
         usage = None
@@ -64,7 +123,7 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response)
             }
 
         data = openai_chat_chunk_message_template(
-            model, message_content if not done else None, usage
+            model, message_content if not done else None, openai_tool_calls, usage
         )
 
         line = f"data: {json.dumps(data)}\n\n"

+ 6 - 0
backend/open_webui/utils/task.py

@@ -32,6 +32,12 @@ def get_task_model_id(
     return task_model_id
 
 
+def prompt_variables_template(template: str, variables: dict[str, str]) -> str:
+    for variable, value in variables.items():
+        template = template.replace(variable, value)
+    return template
+
+
 def prompt_template(
     template: str, user_name: Optional[str] = None, user_location: Optional[str] = None
 ) -> str:

+ 13 - 0
backend/open_webui/utils/tools.py

@@ -61,6 +61,12 @@ def get_tools(
             )
 
         for spec in tools.specs:
+            # TODO: Fix hack for OpenAI API
+            # Some times breaks OpenAI but others don't. Leaving the comment
+            for val in spec.get("parameters", {}).get("properties", {}).values():
+                if val["type"] == "str":
+                    val["type"] = "string"
+
             # Remove internal parameters
             spec["parameters"]["properties"] = {
                 key: val
@@ -73,6 +79,13 @@ def get_tools(
             # convert to function that takes only model params and inserts custom params
             original_func = getattr(module, function_name)
             callable = apply_extra_params_to_tool_function(original_func, extra_params)
+
+            if callable.__doc__ and callable.__doc__.strip() != "":
+                s = re.split(":(param|return)", callable.__doc__, 1)
+                spec["description"] = s[0]
+            else:
+                spec["description"] = function_name
+
             # TODO: This needs to be a pydantic model
             tool_dict = {
                 "toolkit_id": tool_id,

+ 8 - 8
backend/requirements.txt

@@ -1,4 +1,4 @@
-fastapi==0.111.0
+fastapi==0.115.7
 uvicorn[standard]==0.30.6
 pydantic==2.9.2
 python-multipart==0.0.18
@@ -11,7 +11,7 @@ python-jose==3.3.0
 passlib[bcrypt]==1.7.4
 
 requests==2.32.3
-aiohttp==3.11.8
+aiohttp==3.11.11
 async-timeout
 aiocache
 aiofiles
@@ -57,10 +57,10 @@ einops==0.8.0
 ftfy==6.2.3
 pypdf==4.3.1
 fpdf2==2.8.2
-pymdown-extensions==10.11.2
+pymdown-extensions==10.14.2
 docx2txt==0.8
 python-pptx==1.0.0
-unstructured==0.15.9
+unstructured==0.16.11
 nltk==3.9.1
 Markdown==3.7
 pypandoc==1.13
@@ -71,16 +71,16 @@ xlrd==2.0.1
 validators==0.34.0
 psutil
 sentencepiece
-soundfile==0.12.1
+soundfile==0.13.1
 
-opencv-python-headless==4.10.0.84
+opencv-python-headless==4.11.0.86
 rapidocr-onnxruntime==1.3.24
 rank-bm25==0.2.2
 
 faster-whisper==1.0.3
 
 PyJWT[crypto]==2.10.1
-authlib==1.3.2
+authlib==1.4.1
 
 black==24.8.0
 langfuse==2.44.0
@@ -89,7 +89,7 @@ pytube==15.0.0
 
 extract_msg
 pydub
-duckduckgo-search~=7.2.1
+duckduckgo-search~=7.3.0
 
 ## Google Drive
 google-api-python-client

+ 18 - 6
package-lock.json

@@ -1,12 +1,12 @@
 {
 	"name": "open-webui",
-	"version": "0.5.7",
+	"version": "0.5.8",
 	"lockfileVersion": 3,
 	"requires": true,
 	"packages": {
 		"": {
 			"name": "open-webui",
-			"version": "0.5.7",
+			"version": "0.5.8",
 			"dependencies": {
 				"@codemirror/lang-javascript": "^6.2.2",
 				"@codemirror/lang-python": "^6.1.6",
@@ -56,7 +56,7 @@
 				"prosemirror-schema-list": "^1.4.1",
 				"prosemirror-state": "^1.4.3",
 				"prosemirror-view": "^1.34.3",
-				"pyodide": "^0.26.1",
+				"pyodide": "^0.27.2",
 				"socket.io-client": "^4.2.0",
 				"sortablejs": "^1.15.2",
 				"svelte-sonner": "^0.3.19",
@@ -69,6 +69,7 @@
 				"@sveltejs/adapter-static": "^3.0.2",
 				"@sveltejs/kit": "^2.5.20",
 				"@sveltejs/vite-plugin-svelte": "^3.1.1",
+				"@tailwindcss/container-queries": "^0.1.1",
 				"@tailwindcss/typography": "^0.5.13",
 				"@typescript-eslint/eslint-plugin": "^6.17.0",
 				"@typescript-eslint/parser": "^6.17.0",
@@ -2343,6 +2344,16 @@
 				"tslib": "^2.4.0"
 			}
 		},
+		"node_modules/@tailwindcss/container-queries": {
+			"version": "0.1.1",
+			"resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz",
+			"integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==",
+			"dev": true,
+			"license": "MIT",
+			"peerDependencies": {
+				"tailwindcss": ">=3.2.0"
+			}
+		},
 		"node_modules/@tailwindcss/typography": {
 			"version": "0.5.13",
 			"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz",
@@ -9366,9 +9377,10 @@
 			}
 		},
 		"node_modules/pyodide": {
-			"version": "0.26.1",
-			"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.26.1.tgz",
-			"integrity": "sha512-P+Gm88nwZqY7uBgjbQH8CqqU6Ei/rDn7pS1t02sNZsbyLJMyE2OVXjgNuqVT3KqYWnyGREUN0DbBUCJqk8R0ew==",
+			"version": "0.27.2",
+			"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.27.2.tgz",
+			"integrity": "sha512-sfA2kiUuQVRpWI4BYnU3sX5PaTTt/xrcVEmRzRcId8DzZXGGtPgCBC0gCqjUTUYSa8ofPaSjXmzESc86yvvCHg==",
+			"license": "Apache-2.0",
 			"dependencies": {
 				"ws": "^8.5.0"
 			},

+ 3 - 2
package.json

@@ -1,6 +1,6 @@
 {
 	"name": "open-webui",
-	"version": "0.5.7",
+	"version": "0.5.8",
 	"private": true,
 	"scripts": {
 		"dev": "npm run pyodide:fetch && vite dev --host",
@@ -25,6 +25,7 @@
 		"@sveltejs/adapter-static": "^3.0.2",
 		"@sveltejs/kit": "^2.5.20",
 		"@sveltejs/vite-plugin-svelte": "^3.1.1",
+		"@tailwindcss/container-queries": "^0.1.1",
 		"@tailwindcss/typography": "^0.5.13",
 		"@typescript-eslint/eslint-plugin": "^6.17.0",
 		"@typescript-eslint/parser": "^6.17.0",
@@ -98,7 +99,7 @@
 		"prosemirror-schema-list": "^1.4.1",
 		"prosemirror-state": "^1.4.3",
 		"prosemirror-view": "^1.34.3",
-		"pyodide": "^0.26.1",
+		"pyodide": "^0.27.2",
 		"socket.io-client": "^4.2.0",
 		"sortablejs": "^1.15.2",
 		"svelte-sonner": "^0.3.19",

+ 8 - 8
pyproject.toml

@@ -6,7 +6,7 @@ authors = [
 ]
 license = { file = "LICENSE" }
 dependencies = [
-    "fastapi==0.111.0",
+    "fastapi==0.115.7",
     "uvicorn[standard]==0.30.6",
     "pydantic==2.9.2",
     "python-multipart==0.0.18",
@@ -19,7 +19,7 @@ dependencies = [
     "passlib[bcrypt]==1.7.4",
 
     "requests==2.32.3",
-    "aiohttp==3.11.8",
+    "aiohttp==3.11.11",
     "async-timeout",
     "aiocache",
     "aiofiles",
@@ -62,10 +62,10 @@ dependencies = [
     "ftfy==6.2.3",
     "pypdf==4.3.1",
     "fpdf2==2.8.2",
-    "pymdown-extensions==10.11.2",
+    "pymdown-extensions==10.14.2",
     "docx2txt==0.8",
     "python-pptx==1.0.0",
-    "unstructured==0.15.9",
+    "unstructured==0.16.11",
     "nltk==3.9.1",
     "Markdown==3.7",
     "pypandoc==1.13",
@@ -76,16 +76,16 @@ dependencies = [
     "validators==0.34.0",
     "psutil",
     "sentencepiece",
-    "soundfile==0.12.1",
+    "soundfile==0.13.1",
 
-    "opencv-python-headless==4.10.0.84",
+    "opencv-python-headless==4.11.0.86",
     "rapidocr-onnxruntime==1.3.24",
     "rank-bm25==0.2.2",
 
     "faster-whisper==1.0.3",
 
     "PyJWT[crypto]==2.10.1",
-    "authlib==1.3.2",
+    "authlib==1.4.1",
 
     "black==24.8.0",
     "langfuse==2.44.0",
@@ -94,7 +94,7 @@ dependencies = [
 
     "extract_msg",
     "pydub",
-    "duckduckgo-search~=7.2.1",
+    "duckduckgo-search~=7.3.0",
 
     "google-api-python-client",
     "google-auth-httplib2",

+ 4 - 1
scripts/prepare-pyodide.js

@@ -9,7 +9,10 @@ const packages = [
 	'scikit-learn',
 	'scipy',
 	'regex',
-	'seaborn'
+	'sympy',
+	'tiktoken',
+	'seaborn',
+	'pytz'
 ];
 
 import { loadPyodide } from 'pyodide';

+ 5 - 2
src/lib/apis/chats/index.ts

@@ -580,7 +580,7 @@ export const toggleChatPinnedStatusById = async (token: string, id: string) => {
 	return res;
 };
 
-export const cloneChatById = async (token: string, id: string) => {
+export const cloneChatById = async (token: string, id: string, title?: string) => {
 	let error = null;
 
 	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/clone`, {
@@ -589,7 +589,10 @@ export const cloneChatById = async (token: string, id: string) => {
 			Accept: 'application/json',
 			'Content-Type': 'application/json',
 			...(token && { authorization: `Bearer ${token}` })
-		}
+		},
+		body: JSON.stringify({
+			...(title && { title: title })
+		})
 	})
 		.then(async (res) => {
 			if (!res.ok) throw await res.json();

+ 3 - 2
src/lib/apis/index.ts

@@ -880,13 +880,14 @@ export const getChangelog = async () => {
 	return res;
 };
 
-export const getVersionUpdates = async () => {
+export const getVersionUpdates = async (token: string) => {
 	let error = null;
 
 	const res = await fetch(`${WEBUI_BASE_URL}/api/version/updates`, {
 		method: 'GET',
 		headers: {
-			'Content-Type': 'application/json'
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
 		}
 	})
 		.then(async (res) => {

+ 1 - 1
src/lib/apis/models/index.ts

@@ -219,7 +219,7 @@ export const deleteModelById = async (token: string, id: string) => {
 			return json;
 		})
 		.catch((err) => {
-			error = err;
+			error = err.detail;
 
 			console.log(err);
 			return null;

+ 1 - 1
src/lib/apis/openai/index.ts

@@ -322,7 +322,7 @@ export const generateOpenAIChatCompletion = async (
 			return res.json();
 		})
 		.catch((err) => {
-			error = `${err?.detail ?? 'Network Problem'}`;
+			error = `${err?.detail ?? err}`;
 			return null;
 		});
 

+ 2 - 2
src/lib/components/admin/Settings/Audio.svelte

@@ -51,7 +51,7 @@
 			models = [];
 		} else {
 			const res = await _getModels(localStorage.token).catch((e) => {
-				toast.error(e);
+				toast.error(`${e}`);
 			});
 
 			if (res) {
@@ -74,7 +74,7 @@
 			}, 100);
 		} else {
 			const res = await _getVoices(localStorage.token).catch((e) => {
-				toast.error(e);
+				toast.error(`${e}`);
 			});
 
 			if (res) {

+ 16 - 1
src/lib/components/admin/Settings/Interface.svelte

@@ -31,7 +31,8 @@
 		ENABLE_TAGS_GENERATION: true,
 		ENABLE_SEARCH_QUERY_GENERATION: true,
 		ENABLE_RETRIEVAL_QUERY_GENERATION: true,
-		QUERY_GENERATION_PROMPT_TEMPLATE: ''
+		QUERY_GENERATION_PROMPT_TEMPLATE: '',
+		TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: ''
 	};
 
 	let promptSuggestions = [];
@@ -251,6 +252,20 @@
 				</div>
 			</div>
 
+			<div class="mt-3">
+				<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tools Function Calling Prompt')}</div>
+
+				<Tooltip
+					content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
+					placement="top-start"
+				>
+					<Textarea
+						bind:value={taskConfig.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE}
+						placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
+					/>
+				</Tooltip>
+			</div>
+
 			<hr class=" border-gray-50 dark:border-gray-850 my-3" />
 
 			<div class=" space-y-3 {banners.length > 0 ? ' mb-3' : ''}">

+ 13 - 1
src/lib/components/admin/Settings/WebSearch.svelte

@@ -25,7 +25,8 @@
 		'duckduckgo',
 		'tavily',
 		'jina',
-		'bing'
+		'bing',
+		'exa'
 	];
 
 	let youtubeLanguage = 'en';
@@ -261,6 +262,17 @@
 									bind:value={webConfig.search.jina_api_key}
 								/>
 							</div>
+						{:else if webConfig.search.engine === 'exa'}
+							<div>
+								<div class=" self-center text-xs font-medium mb-1">
+									{$i18n.t('Exa API Key')}
+								</div>
+
+								<SensitiveInput
+									placeholder={$i18n.t('Enter Exa API Key')}
+									bind:value={webConfig.search.exa_api_key}
+								/>
+							</div>
 						{:else if webConfig.search.engine === 'bing'}
 							<div>
 								<div class=" self-center text-xs font-medium mb-1">

+ 2 - 1
src/lib/components/admin/Users/Groups.svelte

@@ -61,7 +61,8 @@
 		},
 		features: {
 			web_search: true,
-			image_generation: true
+			image_generation: true,
+			code_interpreter: true
 		}
 	};
 

+ 2 - 1
src/lib/components/admin/Users/Groups/EditGroupModal.svelte

@@ -45,7 +45,8 @@
 		},
 		features: {
 			web_search: true,
-			image_generation: true
+			image_generation: true,
+			code_interpreter: true
 		}
 	};
 	export let userIds = [];

+ 10 - 1
src/lib/components/admin/Users/Groups/Permissions.svelte

@@ -22,7 +22,8 @@
 		},
 		features: {
 			web_search: true,
-			image_generation: true
+			image_generation: true,
+			code_interpreter: true
 		}
 	};
 
@@ -257,5 +258,13 @@
 
 			<Switch bind:state={permissions.features.image_generation} />
 		</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">
+				{$i18n.t('Code Interpreter')}
+			</div>
+
+			<Switch bind:state={permissions.features.code_interpreter} />
+		</div>
 	</div>
 </div>

+ 3 - 1
src/lib/components/admin/Users/UserList.svelte

@@ -6,7 +6,9 @@
 
 	import dayjs from 'dayjs';
 	import relativeTime from 'dayjs/plugin/relativeTime';
+	import localizedFormat from 'dayjs/plugin/localizedFormat';
 	dayjs.extend(relativeTime);
+	dayjs.extend(localizedFormat);
 
 	import { toast } from 'svelte-sonner';
 
@@ -364,7 +366,7 @@
 					</td>
 
 					<td class=" px-3 py-1">
-						{dayjs(user.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
+						{dayjs(user.created_at * 1000).format('LL')}
 					</td>
 
 					<td class=" px-3 py-1"> {user.oauth_sub ?? ''} </td>

+ 3 - 1
src/lib/components/admin/Users/UserList/EditUserModal.svelte

@@ -7,9 +7,11 @@
 	import { updateUserById } from '$lib/apis/users';
 
 	import Modal from '$lib/components/common/Modal.svelte';
+	import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 	const i18n = getContext('i18n');
 	const dispatch = createEventDispatcher();
+	dayjs.extend(localizedFormat);
 
 	export let show = false;
 	export let selectedUser;
@@ -87,7 +89,7 @@
 
 							<div class="text-xs text-gray-500">
 								{$i18n.t('Created at')}
-								{dayjs(selectedUser.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
+								{dayjs(selectedUser.created_at * 1000).format('LL')}
 							</div>
 						</div>
 					</div>

+ 3 - 1
src/lib/components/admin/Users/UserList/UserChatsModal.svelte

@@ -2,8 +2,10 @@
 	import { toast } from 'svelte-sonner';
 	import dayjs from 'dayjs';
 	import { getContext, createEventDispatcher } from 'svelte';
+	import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 	const dispatch = createEventDispatcher();
+	dayjs.extend(localizedFormat);
 
 	import { getChatListByUserId, deleteChatById, getArchivedChatList } from '$lib/apis/chats';
 
@@ -130,7 +132,7 @@
 
 											<td class=" px-3 py-1 hidden md:flex h-[2.5rem] justify-end">
 												<div class="my-auto shrink-0">
-													{dayjs(chat.updated_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
+													{dayjs(chat.updated_at * 1000).format('LLL')}
 												</div>
 											</td>
 

+ 35 - 33
src/lib/components/channel/MessageInput.svelte

@@ -200,7 +200,7 @@
 				files = files.filter((item) => item?.itemId !== tempItemId);
 			}
 		} catch (e) {
-			toast.error(e);
+			toast.error(`${e}`);
 			files = files.filter((item) => item?.itemId !== tempItemId);
 		}
 	};
@@ -398,7 +398,7 @@
 						dir={$settings?.chatDirection ?? 'LTR'}
 					>
 						{#if files.length > 0}
-							<div class="mx-1 mt-2.5 mb-1 flex flex-wrap gap-2">
+							<div class="mx-2 mt-2.5 -mb-1 flex flex-wrap gap-2">
 								{#each files as file, fileIdx}
 									{#if file.type === 'image'}
 										<div class=" relative group">
@@ -411,7 +411,7 @@
 											</div>
 											<div class=" absolute -top-1 -right-1">
 												<button
-													class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
+													class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
 													type="button"
 													on:click={() => {
 														files.splice(fileIdx, 1);
@@ -453,35 +453,9 @@
 							</div>
 						{/if}
 
-						<div class=" flex">
-							<div class="ml-1 self-end mb-1.5 flex space-x-1">
-								<InputMenu
-									{screenCaptureHandler}
-									uploadFilesHandler={() => {
-										filesInputElement.click();
-									}}
-								>
-									<button
-										class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-2 outline-none focus:outline-none"
-										type="button"
-										aria-label="More"
-									>
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											viewBox="0 0 20 20"
-											fill="currentColor"
-											class="size-5"
-										>
-											<path
-												d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
-											/>
-										</svg>
-									</button>
-								</InputMenu>
-							</div>
-
+						<div class="px-2.5">
 							<div
-								class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
+								class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-none w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
 							>
 								<RichTextInput
 									bind:value={content}
@@ -528,8 +502,36 @@
 									}}
 								/>
 							</div>
+						</div>
+
+						<div class=" flex justify-between mb-2.5 mt-1.5 mx-0.5">
+							<div class="ml-1 self-end flex space-x-1">
+								<InputMenu
+									{screenCaptureHandler}
+									uploadFilesHandler={() => {
+										filesInputElement.click();
+									}}
+								>
+									<button
+										class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-none focus:outline-none"
+										type="button"
+										aria-label="More"
+									>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 20 20"
+											fill="currentColor"
+											class="size-5"
+										>
+											<path
+												d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
+											/>
+										</svg>
+									</button>
+								</InputMenu>
+							</div>
 
-							<div class="self-end mb-1.5 flex space-x-1 mr-1">
+							<div class="self-end flex space-x-1 mr-1">
 								{#if content === ''}
 									<Tooltip content={$i18n.t('Record voice')}>
 										<button
@@ -591,7 +593,7 @@
 													xmlns="http://www.w3.org/2000/svg"
 													viewBox="0 0 16 16"
 													fill="currentColor"
-													class="size-6"
+													class="size-5"
 												>
 													<path
 														fill-rule="evenodd"

+ 4 - 6
src/lib/components/channel/Messages/Message.svelte

@@ -3,10 +3,12 @@
 	import relativeTime from 'dayjs/plugin/relativeTime';
 	import isToday from 'dayjs/plugin/isToday';
 	import isYesterday from 'dayjs/plugin/isYesterday';
+	import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 	dayjs.extend(relativeTime);
 	dayjs.extend(isToday);
 	dayjs.extend(isYesterday);
+	dayjs.extend(localizedFormat);
 
 	import { getContext, onMount } from 'svelte';
 	const i18n = getContext<Writable<i18nType>>('i18n');
@@ -153,9 +155,7 @@
 						<div
 							class="mt-1.5 flex flex-shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize"
 						>
-							<Tooltip
-								content={dayjs(message.created_at / 1000000).format('dddd, DD MMMM YYYY HH:mm')}
-							>
+							<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
 								{dayjs(message.created_at / 1000000).format('HH:mm')}
 							</Tooltip>
 						</div>
@@ -174,9 +174,7 @@
 							<div
 								class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
 							>
-								<Tooltip
-									content={dayjs(message.created_at / 1000000).format('dddd, DD MMMM YYYY HH:mm')}
-								>
+								<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
 									<span class="line-clamp-1">{formatDate(message.created_at / 1000000)}</span>
 								</Tooltip>
 							</div>

+ 103 - 76
src/lib/components/chat/Chat.svelte

@@ -41,11 +41,13 @@
 		convertMessagesToHistory,
 		copyToClipboard,
 		getMessageContentParts,
+		createMessagesList,
 		extractSentencesForAudio,
 		promptTemplate,
 		splitStream,
 		sleep,
-		removeDetailsWithReasoning
+		removeDetails,
+		getPromptVariables
 	} from '$lib/utils';
 
 	import { generateChatCompletion } from '$lib/apis/ollama';
@@ -82,10 +84,12 @@
 	import EventConfirmDialog from '../common/ConfirmDialog.svelte';
 	import Placeholder from './Placeholder.svelte';
 	import NotificationToast from '../NotificationToast.svelte';
+	import Spinner from '../common/Spinner.svelte';
 
 	export let chatIdProp = '';
 
-	let loaded = false;
+	let loading = false;
+
 	const eventTarget = new EventTarget();
 	let controlPane;
 	let controlPaneComponent;
@@ -114,7 +118,7 @@
 	let selectedToolIds = [];
 	let imageGenerationEnabled = false;
 	let webSearchEnabled = false;
-
+	let codeInterpreterEnabled = false;
 	let chat = null;
 	let tags = [];
 
@@ -133,6 +137,7 @@
 
 	$: if (chatIdProp) {
 		(async () => {
+			loading = true;
 			console.log(chatIdProp);
 
 			prompt = '';
@@ -141,11 +146,9 @@
 			webSearchEnabled = false;
 			imageGenerationEnabled = false;
 
-			loaded = false;
-
 			if (chatIdProp && (await loadChat())) {
 				await tick();
-				loaded = true;
+				loading = false;
 
 				if (localStorage.getItem(`chat-input-${chatIdProp}`)) {
 					try {
@@ -223,7 +226,7 @@
 		}
 
 		await tick();
-		saveChatHandler(_chatId);
+		saveChatHandler(_chatId, history);
 	};
 
 	const chatEventHandler = async (event, cb) => {
@@ -627,7 +630,7 @@
 		} catch (e) {
 			// Remove the failed doc from the files array
 			files = files.filter((f) => f.name !== url);
-			toast.error(e);
+			toast.error(`${e}`);
 		}
 	};
 
@@ -715,6 +718,7 @@
 		if ($page.url.searchParams.get('web-search') === 'true') {
 			webSearchEnabled = true;
 		}
+
 		if ($page.url.searchParams.get('image-generation') === 'true') {
 			imageGenerationEnabled = true;
 		}
@@ -823,20 +827,6 @@
 			messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
 		}
 	};
-
-	const createMessagesList = (responseMessageId) => {
-		if (responseMessageId === null) {
-			return [];
-		}
-
-		const message = history.messages[responseMessageId];
-		if (message?.parentId) {
-			return [...createMessagesList(message.parentId), message];
-		} else {
-			return [message];
-		}
-	};
-
 	const chatCompletedHandler = async (chatId, modelId, responseMessageId, messages) => {
 		const res = await chatCompleted(localStorage.token, {
 			model: modelId,
@@ -893,7 +883,7 @@
 	};
 
 	const chatActionHandler = async (chatId, actionId, modelId, responseMessageId, event = null) => {
-		const messages = createMessagesList(responseMessageId);
+		const messages = createMessagesList(history, responseMessageId);
 
 		const res = await chatAction(localStorage.token, actionId, {
 			model: modelId,
@@ -962,7 +952,7 @@
 			const modelId = selectedModels[0];
 			const model = $models.filter((m) => m.id === modelId).at(0);
 
-			const messages = createMessagesList(history.currentId);
+			const messages = createMessagesList(history, history.currentId);
 			const parentMessage = messages.length !== 0 ? messages.at(-1) : null;
 
 			const userMessageId = uuidv4();
@@ -1007,9 +997,9 @@
 			}
 
 			if (messages.length === 0) {
-				await initChatHandler();
+				await initChatHandler(history);
 			} else {
-				await saveChatHandler($chatId);
+				await saveChatHandler($chatId, history);
 			}
 		}
 	};
@@ -1071,9 +1061,9 @@
 		}
 
 		if (messages.length === 0) {
-			await initChatHandler();
+			await initChatHandler(history);
 		} else {
-			await saveChatHandler($chatId);
+			await saveChatHandler($chatId, history);
 		}
 	};
 
@@ -1207,7 +1197,12 @@
 			);
 
 			history.messages[message.id] = message;
-			await chatCompletedHandler(chatId, message.model, message.id, createMessagesList(message.id));
+			await chatCompletedHandler(
+				chatId,
+				message.model,
+				message.id,
+				createMessagesList(history, message.id)
+			);
 		}
 
 		console.log(data);
@@ -1223,7 +1218,7 @@
 	const submitPrompt = async (userPrompt, { _raw = false } = {}) => {
 		console.log('submitPrompt', userPrompt, $chatId);
 
-		const messages = createMessagesList(history.currentId);
+		const messages = createMessagesList(history, history.currentId);
 		const _selectedModels = selectedModels.map((modelId) =>
 			$models.map((m) => m.id).includes(modelId) ? modelId : ''
 		);
@@ -1271,7 +1266,6 @@
 		}
 
 		prompt = '';
-		await tick();
 
 		// Reset chat input textarea
 		const chatInputElement = document.getElementById('chat-input');
@@ -1313,34 +1307,25 @@
 			history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
 		}
 
-		// Wait until history/message have been updated
-		await tick();
-
 		// focus on chat input
 		const chatInput = document.getElementById('chat-input');
 		chatInput?.focus();
 
 		saveSessionSelectedModels();
 
-		await sendPrompt(userPrompt, userMessageId, { newChat: true });
+		await sendPrompt(history, userPrompt, userMessageId, { newChat: true });
 	};
 
 	const sendPrompt = async (
+		_history,
 		prompt: string,
 		parentId: string,
 		{ modelId = null, modelIdx = null, newChat = false } = {}
 	) => {
-		// Create new chat if newChat is true and first user message
-		if (
-			newChat &&
-			history.messages[history.currentId].parentId === null &&
-			history.messages[history.currentId].role === 'user'
-		) {
-			await initChatHandler();
-		} else {
-			await saveChatHandler($chatId);
-		}
+		let _chatId = JSON.parse(JSON.stringify($chatId));
+		_history = JSON.parse(JSON.stringify(_history));
 
+		const responseMessageIds: Record<PropertyKey, string> = {};
 		// If modelId is provided, use it, else use selected model
 		let selectedModelIds = modelId
 			? [modelId]
@@ -1349,7 +1334,6 @@
 				: selectedModels;
 
 		// Create response messages for each selected model
-		const responseMessageIds: Record<PropertyKey, string> = {};
 		for (const [_modelIdx, modelId] of selectedModelIds.entries()) {
 			const model = $models.filter((m) => m.id === modelId).at(0);
 
@@ -1384,19 +1368,26 @@
 				responseMessageIds[`${modelId}-${modelIdx ? modelIdx : _modelIdx}`] = responseMessageId;
 			}
 		}
+		history = history;
+
+		// Create new chat if newChat is true and first user message
+		if (newChat && _history.messages[_history.currentId].parentId === null) {
+			_chatId = await initChatHandler(_history);
+		}
+
 		await tick();
 
+		_history = JSON.parse(JSON.stringify(history));
 		// Save chat after all messages have been created
-		await saveChatHandler($chatId);
+		await saveChatHandler(_chatId, _history);
 
-		const _chatId = JSON.parse(JSON.stringify($chatId));
 		await Promise.all(
 			selectedModelIds.map(async (modelId, _modelIdx) => {
 				console.log('modelId', modelId);
 				const model = $models.filter((m) => m.id === modelId).at(0);
 
 				if (model) {
-					const messages = createMessagesList(parentId);
+					const messages = createMessagesList(_history, parentId);
 					// If there are image files, check if model is vision capable
 					const hasImages = messages.some((message) =>
 						message.files?.some((file) => file.type === 'image')
@@ -1412,7 +1403,7 @@
 
 					let responseMessageId =
 						responseMessageIds[`${modelId}-${modelIdx ? modelIdx : _modelIdx}`];
-					let responseMessage = history.messages[responseMessageId];
+					let responseMessage = _history.messages[responseMessageId];
 
 					let userContext = null;
 					if ($settings?.memory ?? false) {
@@ -1441,7 +1432,7 @@
 					const chatEventEmitter = await getChatEventEmitter(model.id, _chatId);
 
 					scrollToBottom();
-					await sendPromptSocket(model, responseMessageId, _chatId);
+					await sendPromptSocket(_history, model, responseMessageId, _chatId);
 
 					if (chatEventEmitter) clearInterval(chatEventEmitter);
 				} else {
@@ -1454,9 +1445,9 @@
 		chats.set(await getChatList(localStorage.token, $currentChatPage));
 	};
 
-	const sendPromptSocket = async (model, responseMessageId, _chatId) => {
-		const responseMessage = history.messages[responseMessageId];
-		const userMessage = history.messages[responseMessage.parentId];
+	const sendPromptSocket = async (_history, model, responseMessageId, _chatId) => {
+		const responseMessage = _history.messages[responseMessageId];
+		const userMessage = _history.messages[responseMessage.parentId];
 
 		let files = JSON.parse(JSON.stringify(chatFiles));
 		files.push(
@@ -1504,9 +1495,9 @@
 						}`
 					}
 				: undefined,
-			...createMessagesList(responseMessageId).map((message) => ({
+			...createMessagesList(_history, responseMessageId).map((message) => ({
 				...message,
-				content: removeDetailsWithReasoning(message.content)
+				content: removeDetails(message.content, ['reasoning', 'code_interpreter'])
 			}))
 		]
 			.filter((message) => message?.content?.trim())
@@ -1557,9 +1548,28 @@
 
 				files: (files?.length ?? 0) > 0 ? files : undefined,
 				tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
+
 				features: {
-					image_generation: imageGenerationEnabled,
-					web_search: webSearchEnabled
+					image_generation:
+						$config?.features?.enable_image_generation &&
+						($user.role === 'admin' || $user?.permissions?.features?.image_generation)
+							? imageGenerationEnabled
+							: false,
+					code_interpreter:
+						$user.role === 'admin' || $user?.permissions?.features?.code_interpreter
+							? codeInterpreterEnabled
+							: false,
+					web_search:
+						$config?.features?.enable_web_search &&
+						($user.role === 'admin' || $user?.permissions?.features?.web_search)
+							? webSearchEnabled || ($settings?.webSearch ?? false) === 'always'
+							: false
+				},
+				variables: {
+					...getPromptVariables(
+						$user.name,
+						$settings?.userLocation ? await getAndUpdateUserLocation(localStorage.token) : undefined
+					)
 				},
 
 				session_id: $socket?.id,
@@ -1590,12 +1600,15 @@
 			},
 			`${WEBUI_BASE_URL}/api`
 		).catch((error) => {
-			console.log(error);
+			toast.error(`${error}`);
+
 			responseMessage.error = {
 				content: error
 			};
 			responseMessage.done = true;
+
 			history.messages[responseMessageId] = responseMessage;
+			history.currentId = responseMessageId;
 			return null;
 		});
 
@@ -1693,7 +1706,7 @@
 		history.currentId = userMessageId;
 
 		await tick();
-		await sendPrompt(userPrompt, userMessageId);
+		await sendPrompt(history, userPrompt, userMessageId);
 	};
 
 	const regenerateResponse = async (message) => {
@@ -1705,11 +1718,11 @@
 
 			if ((userMessage?.models ?? [...selectedModels]).length == 1) {
 				// If user message has only one model selected, sendPrompt automatically selects it for regeneration
-				await sendPrompt(userPrompt, userMessage.id);
+				await sendPrompt(history, userPrompt, userMessage.id);
 			} else {
 				// If there are multiple models selected, use the model of the response message for regeneration
 				// e.g. many model chat
-				await sendPrompt(userPrompt, userMessage.id, {
+				await sendPrompt(history, userPrompt, userMessage.id, {
 					modelId: message.model,
 					modelIdx: message.modelIdx
 				});
@@ -1731,7 +1744,7 @@
 				.at(0);
 
 			if (model) {
-				await sendPromptSocket(model, responseMessage.id, _chatId);
+				await sendPromptSocket(history, model, responseMessage.id, _chatId);
 			}
 		}
 	};
@@ -1774,7 +1787,7 @@
 					}
 				}
 
-				await saveChatHandler(_chatId);
+				await saveChatHandler(_chatId, history);
 			} else {
 				console.error(res);
 			}
@@ -1783,42 +1796,48 @@
 		}
 	};
 
-	const initChatHandler = async () => {
+	const initChatHandler = async (history) => {
+		let _chatId = $chatId;
+
 		if (!$temporaryChatEnabled) {
 			chat = await createNewChat(localStorage.token, {
-				id: $chatId,
+				id: _chatId,
 				title: $i18n.t('New Chat'),
 				models: selectedModels,
 				system: $settings.system ?? undefined,
 				params: params,
 				history: history,
-				messages: createMessagesList(history.currentId),
+				messages: createMessagesList(history, history.currentId),
 				tags: [],
 				timestamp: Date.now()
 			});
 
-			currentChatPage.set(1);
+			_chatId = chat.id;
+			await chatId.set(_chatId);
+
 			await chats.set(await getChatList(localStorage.token, $currentChatPage));
-			await chatId.set(chat.id);
+			currentChatPage.set(1);
 
-			window.history.replaceState(history.state, '', `/c/${chat.id}`);
+			window.history.replaceState(history.state, '', `/c/${_chatId}`);
 		} else {
+			_chatId = 'local';
 			await chatId.set('local');
 		}
 		await tick();
+
+		return _chatId;
 	};
 
-	const saveChatHandler = async (_chatId) => {
+	const saveChatHandler = async (_chatId, history) => {
 		if ($chatId == _chatId) {
 			if (!$temporaryChatEnabled) {
 				chat = await updateChatById(localStorage.token, _chatId, {
 					models: selectedModels,
 					history: history,
-					messages: createMessagesList(history.currentId),
+					messages: createMessagesList(history, history.currentId),
 					params: params,
 					files: chatFiles
 				});
-
 				currentChatPage.set(1);
 				await chats.set(await getChatList(localStorage.token, $currentChatPage));
 			}
@@ -1861,7 +1880,7 @@
 		: ' '} w-full max-w-full flex flex-col"
 	id="chat-container"
 >
-	{#if !chatIdProp || (loaded && chatIdProp)}
+	{#if chatIdProp === '' || (!loading && chatIdProp)}
 		{#if $settings?.backgroundImageUrl ?? null}
 			<div
 				class="absolute {$showSidebar
@@ -1921,8 +1940,8 @@
 					</div>
 				{/if}
 
-				<div class="flex flex-col flex-auto z-10 w-full">
-					{#if $settings?.landingPageMode === 'chat' || createMessagesList(history.currentId).length > 0}
+				<div class="flex flex-col flex-auto z-10 w-full @container">
+					{#if $settings?.landingPageMode === 'chat' || createMessagesList(history, history.currentId).length > 0}
 						<div
 							class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden"
 							id="messages-container"
@@ -1962,6 +1981,7 @@
 								bind:autoScroll
 								bind:selectedToolIds
 								bind:imageGenerationEnabled
+								bind:codeInterpreterEnabled
 								bind:webSearchEnabled
 								bind:atSelectedModel
 								transparentBackground={$settings?.backgroundImageUrl ?? false}
@@ -2013,6 +2033,7 @@
 								bind:autoScroll
 								bind:selectedToolIds
 								bind:imageGenerationEnabled
+								bind:codeInterpreterEnabled
 								bind:webSearchEnabled
 								bind:atSelectedModel
 								transparentBackground={$settings?.backgroundImageUrl ?? false}
@@ -2065,5 +2086,11 @@
 				{eventTarget}
 			/>
 		</PaneGroup>
+	{:else if loading}
+		<div class=" flex items-center justify-center h-full w-full">
+			<div class="m-auto">
+				<Spinner />
+			</div>
+		</div>
 	{/if}
 </div>

+ 168 - 80
src/lib/components/chat/MessageInput.svelte

@@ -22,23 +22,27 @@
 	import { blobToFile, compressImage, createMessagesList, findWordIndices } from '$lib/utils';
 	import { transcribeAudio } from '$lib/apis/audio';
 	import { uploadFile } from '$lib/apis/files';
-	import { getTools } from '$lib/apis/tools';
+	import { generateAutoCompletion } from '$lib/apis';
+	import { deleteFileById } from '$lib/apis/files';
 
 	import { WEBUI_BASE_URL, WEBUI_API_BASE_URL, PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
 
-	import Tooltip from '../common/Tooltip.svelte';
 	import InputMenu from './MessageInput/InputMenu.svelte';
-	import Headphone from '../icons/Headphone.svelte';
 	import VoiceRecording from './MessageInput/VoiceRecording.svelte';
-	import FileItem from '../common/FileItem.svelte';
 	import FilesOverlay from './MessageInput/FilesOverlay.svelte';
 	import Commands from './MessageInput/Commands.svelte';
-	import XMark from '../icons/XMark.svelte';
+
 	import RichTextInput from '../common/RichTextInput.svelte';
-	import { generateAutoCompletion } from '$lib/apis';
-	import { error, text } from '@sveltejs/kit';
+	import Tooltip from '../common/Tooltip.svelte';
+	import FileItem from '../common/FileItem.svelte';
 	import Image from '../common/Image.svelte';
-	import { deleteFileById } from '$lib/apis/files';
+
+	import XMark from '../icons/XMark.svelte';
+	import Headphone from '../icons/Headphone.svelte';
+	import GlobeAlt from '../icons/GlobeAlt.svelte';
+	import PhotoSolid from '../icons/PhotoSolid.svelte';
+	import Photo from '../icons/Photo.svelte';
+	import CommandLine from '../icons/CommandLine.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -65,6 +69,7 @@
 
 	export let imageGenerationEnabled = false;
 	export let webSearchEnabled = false;
+	export let codeInterpreterEnabled = false;
 
 	$: onChange({
 		prompt,
@@ -211,7 +216,7 @@
 				files = files.filter((item) => item?.itemId !== tempItemId);
 			}
 		} catch (e) {
-			toast.error(e);
+			toast.error(`${e}`);
 			files = files.filter((item) => item?.itemId !== tempItemId);
 		}
 	};
@@ -385,7 +390,7 @@
 				</div>
 
 				<div class="w-full relative">
-					{#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled || imageGenerationEnabled}
+					{#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled || ($settings?.webSearch ?? false) === 'always' || imageGenerationEnabled || codeInterpreterEnabled}
 						<div
 							class="px-3 pb-0.5 pt-1.5 text-left w-full flex flex-col absolute bottom-0 left-0 right-0 bg-gradient-to-t from-white dark:from-gray-900 z-10"
 						>
@@ -421,23 +426,39 @@
 								</div>
 							{/if}
 
+							{#if webSearchEnabled || ($settings?.webSearch ?? false) === 'always'}
+								<div class="flex items-center justify-between w-full">
+									<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
+										<div class="pl-1">
+											<span class="relative flex size-2">
+												<span
+													class="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"
+												/>
+												<span class="relative inline-flex rounded-full size-2 bg-blue-500" />
+											</span>
+										</div>
+										<div class=" translate-y-[0.5px]">{$i18n.t('Search the internet')}</div>
+									</div>
+								</div>
+							{/if}
+
 							{#if imageGenerationEnabled}
 								<div class="flex items-center justify-between w-full">
 									<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
 										<div class="pl-1">
 											<span class="relative flex size-2">
 												<span
-													class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
+													class="animate-ping absolute inline-flex h-full w-full rounded-full bg-teal-400 opacity-75"
 												/>
-												<span class="relative inline-flex rounded-full size-2 bg-green-500" />
+												<span class="relative inline-flex rounded-full size-2 bg-teal-500" />
 											</span>
 										</div>
-										<div class=" ">{$i18n.t('Image generation')}</div>
+										<div class=" translate-y-[0.5px]">{$i18n.t('Generate an image')}</div>
 									</div>
 								</div>
 							{/if}
 
-							{#if webSearchEnabled}
+							{#if codeInterpreterEnabled}
 								<div class="flex items-center justify-between w-full">
 									<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
 										<div class="pl-1">
@@ -448,7 +469,7 @@
 												<span class="relative inline-flex rounded-full size-2 bg-green-500" />
 											</span>
 										</div>
-										<div class=" ">{$i18n.t('Search the web')}</div>
+										<div class=" translate-y-[0.5px]">{$i18n.t('Execute code for analysis')}</div>
 									</div>
 								</div>
 							{/if}
@@ -568,7 +589,7 @@
 								dir={$settings?.chatDirection ?? 'LTR'}
 							>
 								{#if files.length > 0}
-									<div class="mx-1 mt-2.5 mb-1 flex items-center flex-wrap gap-2">
+									<div class="mx-2 mt-2.5 -mb-1 flex items-center flex-wrap gap-2">
 										{#each files as file, fileIdx}
 											{#if file.type === 'image'}
 												<div class=" relative group">
@@ -606,7 +627,7 @@
 													</div>
 													<div class=" absolute -top-1 -right-1">
 														<button
-															class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
+															class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
 															type="button"
 															on:click={() => {
 																files.splice(fileIdx, 1);
@@ -656,65 +677,10 @@
 									</div>
 								{/if}
 
-								<div class=" flex">
-									<div class="ml-1 self-end mb-1.5 flex space-x-1">
-										<InputMenu
-											bind:imageGenerationEnabled
-											bind:webSearchEnabled
-											bind:selectedToolIds
-											{screenCaptureHandler}
-											uploadFilesHandler={() => {
-												filesInputElement.click();
-											}}
-											uploadGoogleDriveHandler={async () => {
-												try {
-													const fileData = await createPicker();
-													if (fileData) {
-														const file = new File([fileData.blob], fileData.name, {
-															type: fileData.blob.type
-														});
-														await uploadFileHandler(file);
-													} else {
-														console.log('No file was selected from Google Drive');
-													}
-												} catch (error) {
-													console.error('Google Drive Error:', error);
-													toast.error(
-														$i18n.t('Error accessing Google Drive: {{error}}', {
-															error: error.message
-														})
-													);
-												}
-											}}
-											onClose={async () => {
-												await tick();
-
-												const chatInput = document.getElementById('chat-input');
-												chatInput?.focus();
-											}}
-										>
-											<button
-												class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-2 outline-none focus:outline-none"
-												type="button"
-												aria-label="More"
-											>
-												<svg
-													xmlns="http://www.w3.org/2000/svg"
-													viewBox="0 0 20 20"
-													fill="currentColor"
-													class="size-5"
-												>
-													<path
-														d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
-													/>
-												</svg>
-											</button>
-										</InputMenu>
-									</div>
-
+								<div class="px-2.5">
 									{#if $settings?.richTextInput ?? true}
 										<div
-											class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
+											class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
 										>
 											<RichTextInput
 												bind:this={chatInputElement}
@@ -918,7 +884,7 @@
 										<textarea
 											id="chat-input"
 											bind:this={chatInputElement}
-											class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-none w-full py-3 px-1 rounded-xl resize-none h-[48px]"
+											class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-none w-full pt-3 px-1 resize-none"
 											placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
 											bind:value={prompt}
 											on:keypress={(e) => {
@@ -1107,8 +1073,125 @@
 											}}
 										/>
 									{/if}
+								</div>
+
+								<div class=" flex justify-between mt-1.5 mb-2.5 mx-0.5 max-w-full">
+									<div class="ml-1 self-end gap-0.5 flex items-center flex-1 max-w-[80%]">
+										<InputMenu
+											bind:selectedToolIds
+											{screenCaptureHandler}
+											{inputFilesHandler}
+											uploadFilesHandler={() => {
+												filesInputElement.click();
+											}}
+											uploadGoogleDriveHandler={async () => {
+												try {
+													const fileData = await createPicker();
+													if (fileData) {
+														const file = new File([fileData.blob], fileData.name, {
+															type: fileData.blob.type
+														});
+														await uploadFileHandler(file);
+													} else {
+														console.log('No file was selected from Google Drive');
+													}
+												} catch (error) {
+													console.error('Google Drive Error:', error);
+													toast.error(
+														$i18n.t('Error accessing Google Drive: {{error}}', {
+															error: error.message
+														})
+													);
+												}
+											}}
+											onClose={async () => {
+												await tick();
+
+												const chatInput = document.getElementById('chat-input');
+												chatInput?.focus();
+											}}
+										>
+											<button
+												class="bg-transparent hover:bg-gray-100 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-none focus:outline-none"
+												type="button"
+												aria-label="More"
+											>
+												<svg
+													xmlns="http://www.w3.org/2000/svg"
+													viewBox="0 0 20 20"
+													fill="currentColor"
+													class="size-5"
+												>
+													<path
+														d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
+													/>
+												</svg>
+											</button>
+										</InputMenu>
+
+										<div class="flex gap-0.5 items-center overflow-x-auto scrollbar-none flex-1">
+											{#if $_user}
+												{#if $config?.features?.enable_web_search && ($_user.role === 'admin' || $_user?.permissions?.features?.web_search)}
+													<Tooltip content={$i18n.t('Search the internet')} placement="top">
+														<button
+															on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
+															type="button"
+															class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-none max-w-full overflow-hidden {webSearchEnabled ||
+															($settings?.webSearch ?? false) === 'always'
+																? 'bg-blue-100 dark:bg-blue-500/20 text-blue-500 dark:text-blue-400'
+																: 'bg-transparent text-gray-600 dark:text-gray-400 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'}"
+														>
+															<GlobeAlt className="size-5" strokeWidth="1.75" />
+															<span
+																class="hidden @sm:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px] mr-0.5"
+																>{$i18n.t('Web Search')}</span
+															>
+														</button>
+													</Tooltip>
+												{/if}
+
+												{#if $config?.features?.enable_image_generation && ($_user.role === 'admin' || $_user?.permissions?.features?.image_generation)}
+													<Tooltip content={$i18n.t('Generate an image')} placement="top">
+														<button
+															on:click|preventDefault={() =>
+																(imageGenerationEnabled = !imageGenerationEnabled)}
+															type="button"
+															class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-none max-w-full overflow-hidden {imageGenerationEnabled
+																? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400'
+																: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
+														>
+															<Photo className="size-5" strokeWidth="1.75" />
+															<span
+																class="hidden @sm:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px] mr-0.5"
+																>{$i18n.t('Image')}</span
+															>
+														</button>
+													</Tooltip>
+												{/if}
+
+												{#if $_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter}
+													<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
+														<button
+															on:click|preventDefault={() =>
+																(codeInterpreterEnabled = !codeInterpreterEnabled)}
+															type="button"
+															class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-none max-w-full overflow-hidden {codeInterpreterEnabled
+																? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400'
+																: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
+														>
+															<CommandLine className="size-5" strokeWidth="1.75" />
+															<span
+																class="hidden @sm:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px] mr-0.5"
+																>{$i18n.t('Code Interpreter')}</span
+															>
+														</button>
+													</Tooltip>
+												{/if}
+											{/if}
+										</div>
+									</div>
 
-									<div class="self-end mb-1.5 flex space-x-1 mr-1">
+									<div class="self-end flex space-x-1 mr-1 flex-shrink-0">
 										{#if !history?.currentId || history.messages[history.currentId]?.done == true}
 											<Tooltip content={$i18n.t('Record voice')}>
 												<button
@@ -1163,7 +1246,10 @@
 												<div class=" flex items-center">
 													<Tooltip content={$i18n.t('Call')}>
 														<button
-															class=" bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full p-2 self-center"
+															class=" {webSearchEnabled ||
+															($settings?.webSearch ?? false) === 'always'
+																? 'bg-blue-500 text-white hover:bg-blue-400 '
+																: 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100'} transition rounded-full p-1.5 self-center"
 															type="button"
 															on:click={async () => {
 																if (selectedModels.length > 1) {
@@ -1216,7 +1302,9 @@
 														<button
 															id="send-message-button"
 															class="{prompt !== ''
-																? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
+																? webSearchEnabled || ($settings?.webSearch ?? false) === 'always'
+																	? 'bg-blue-500 text-white hover:bg-blue-400 '
+																	: 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
 																: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
 															type="submit"
 															disabled={prompt === ''}
@@ -1225,7 +1313,7 @@
 																xmlns="http://www.w3.org/2000/svg"
 																viewBox="0 0 16 16"
 																fill="currentColor"
-																class="size-6"
+																class="size-5"
 															>
 																<path
 																	fill-rule="evenodd"
@@ -1250,7 +1338,7 @@
 															xmlns="http://www.w3.org/2000/svg"
 															viewBox="0 0 24 24"
 															fill="currentColor"
-															class="size-6"
+															class="size-5"
 														>
 															<path
 																fill-rule="evenodd"

+ 65 - 63
src/lib/components/chat/MessageInput/InputMenu.svelte

@@ -15,39 +15,30 @@
 	import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
 	import CameraSolid from '$lib/components/icons/CameraSolid.svelte';
 	import PhotoSolid from '$lib/components/icons/PhotoSolid.svelte';
+	import CommandLineSolid from '$lib/components/icons/CommandLineSolid.svelte';
 
 	const i18n = getContext('i18n');
 
 	export let screenCaptureHandler: Function;
 	export let uploadFilesHandler: Function;
+	export let inputFilesHandler: Function;
+
 	export let uploadGoogleDriveHandler: Function;
 
 	export let selectedToolIds: string[] = [];
 
-	export let webSearchEnabled: boolean;
-	export let imageGenerationEnabled: boolean;
-
 	export let onClose: Function;
 
 	let tools = {};
 	let show = false;
 
-	let showImageGeneration = false;
-
-	$: showImageGeneration =
-		$config?.features?.enable_image_generation &&
-		($user.role === 'admin' || $user?.permissions?.features?.image_generation);
-
-	let showWebSearch = false;
-
-	$: showWebSearch =
-		$config?.features?.enable_web_search &&
-		($user.role === 'admin' || $user?.permissions?.features?.web_search);
-
 	$: if (show) {
 		init();
 	}
 
+	let fileUploadEnabled = true;
+	$: fileUploadEnabled = $user.role === 'admin' || $user?.permissions?.chat?.file_upload;
+
 	const init = async () => {
 		if ($_tools === null) {
 			await _tools.set(await getTools(localStorage.token));
@@ -62,8 +53,31 @@
 			return a;
 		}, {});
 	};
+
+	const detectMobile = () => {
+		const userAgent = navigator.userAgent || navigator.vendor || window.opera;
+		return /android|iphone|ipad|ipod|windows phone/i.test(userAgent);
+	};
+
+	function handleFileChange(event) {
+		const inputFiles = Array.from(event.target?.files);
+		if (inputFiles && inputFiles.length > 0) {
+			console.log(inputFiles);
+			inputFilesHandler(inputFiles);
+		}
+	}
 </script>
 
+<!-- Hidden file input used to open the camera on mobile -->
+<input
+	id="camera-input"
+	type="file"
+	accept="image/*"
+	capture="environment"
+	on:change={handleFileChange}
+	style="display: none;"
+/>
+
 <Dropdown
 	bind:show
 	on:change={(e) => {
@@ -129,63 +143,51 @@
 				<hr class="border-black/5 dark:border-white/5 my-1" />
 			{/if}
 
-			{#if showImageGeneration}
-				<button
-					class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
-					on:click={() => {
-						imageGenerationEnabled = !imageGenerationEnabled;
-					}}
-				>
-					<div class="flex-1 flex items-center gap-2">
-						<PhotoSolid />
-						<div class=" line-clamp-1">{$i18n.t('Image')}</div>
-					</div>
-
-					<Switch state={imageGenerationEnabled} />
-				</button>
-			{/if}
-
-			{#if showWebSearch}
-				<button
-					class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
-					on:click={() => {
-						webSearchEnabled = !webSearchEnabled;
-					}}
-				>
-					<div class="flex-1 flex items-center gap-2">
-						<GlobeAltSolid />
-						<div class=" line-clamp-1">{$i18n.t('Web Search')}</div>
-					</div>
-
-					<Switch state={webSearchEnabled} />
-				</button>
-			{/if}
-
-			{#if showImageGeneration || showWebSearch}
-				<hr class="border-black/5 dark:border-white/5 my-1" />
-			{/if}
-
-			{#if !$mobile}
+			<Tooltip
+				content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
+				className="w-full"
+			>
 				<DropdownMenu.Item
-					class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-xl"
+					class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-xl {!fileUploadEnabled
+						? 'opacity-50'
+						: ''}"
 					on:click={() => {
-						screenCaptureHandler();
+						if (fileUploadEnabled) {
+							if (!detectMobile()) {
+								screenCaptureHandler();
+							} else {
+								const cameraInputElement = document.getElementById('camera-input');
+
+								if (cameraInputElement) {
+									cameraInputElement.click();
+								}
+							}
+						}
 					}}
 				>
 					<CameraSolid />
 					<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
 				</DropdownMenu.Item>
-			{/if}
+			</Tooltip>
 
-			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
-				on:click={() => {
-					uploadFilesHandler();
-				}}
+			<Tooltip
+				content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
+				className="w-full"
 			>
-				<DocumentArrowUpSolid />
-				<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
-			</DropdownMenu.Item>
+				<DropdownMenu.Item
+					class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
+						? 'opacity-50'
+						: ''}"
+					on:click={() => {
+						if (fileUploadEnabled) {
+							uploadFilesHandler();
+						}
+					}}
+				>
+					<DocumentArrowUpSolid />
+					<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
+				</DropdownMenu.Item>
+			</Tooltip>
 
 			{#if $config?.features?.enable_google_drive_integration}
 				<DropdownMenu.Item

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

@@ -233,7 +233,7 @@
 				history.currentId = userMessageId;
 
 				await tick();
-				await sendPrompt(userPrompt, userMessageId);
+				await sendPrompt(history, userPrompt, userMessageId);
 			} else {
 				// Edit user message
 				history.messages[messageId].content = content;

+ 98 - 92
src/lib/components/chat/Messages/CodeBlock.svelte

@@ -25,6 +25,7 @@
 	export let token;
 	export let lang = '';
 	export let code = '';
+	export let attributes = {};
 
 	export let className = 'my-2';
 	export let editorClassName = '';
@@ -49,6 +50,7 @@
 	let stdout = null;
 	let stderr = null;
 	let result = null;
+	let files = null;
 
 	let copied = false;
 	let saved = false;
@@ -109,86 +111,7 @@
 	};
 
 	const executePython = async (code) => {
-		if (!code.includes('input') && !code.includes('matplotlib')) {
-			executePythonAsWorker(code);
-		} else {
-			result = null;
-			stdout = null;
-			stderr = null;
-
-			executing = true;
-
-			document.pyodideMplTarget = document.getElementById(`plt-canvas-${id}`);
-
-			let pyodide = await loadPyodide({
-				indexURL: '/pyodide/',
-				stdout: (text) => {
-					console.log('Python output:', text);
-
-					if (stdout) {
-						stdout += `${text}\n`;
-					} else {
-						stdout = `${text}\n`;
-					}
-				},
-				stderr: (text) => {
-					console.log('An error occurred:', text);
-					if (stderr) {
-						stderr += `${text}\n`;
-					} else {
-						stderr = `${text}\n`;
-					}
-				},
-				packages: ['micropip']
-			});
-
-			try {
-				const micropip = pyodide.pyimport('micropip');
-
-				// await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
-
-				let packages = [
-					code.includes('requests') ? 'requests' : null,
-					code.includes('bs4') ? 'beautifulsoup4' : null,
-					code.includes('numpy') ? 'numpy' : null,
-					code.includes('pandas') ? 'pandas' : null,
-					code.includes('matplotlib') ? 'matplotlib' : null,
-					code.includes('sklearn') ? 'scikit-learn' : null,
-					code.includes('scipy') ? 'scipy' : null,
-					code.includes('re') ? 'regex' : null,
-					code.includes('seaborn') ? 'seaborn' : null
-				].filter(Boolean);
-
-				console.log(packages);
-				await micropip.install(packages);
-
-				result = await pyodide.runPythonAsync(`from js import prompt
-def input(p):
-    return prompt(p)
-__builtins__.input = input`);
-
-				result = await pyodide.runPython(code);
-
-				if (!result) {
-					result = '[NO OUTPUT]';
-				}
-
-				console.log(result);
-				console.log(stdout);
-				console.log(stderr);
-
-				const pltCanvasElement = document.getElementById(`plt-canvas-${id}`);
-
-				if (pltCanvasElement?.innerHTML !== '') {
-					pltCanvasElement.classList.add('pt-4');
-				}
-			} catch (error) {
-				console.error('Error:', error);
-				stderr = error;
-			}
-
-			executing = false;
-		}
+		executePythonAsWorker(code);
 	};
 
 	const executePythonAsWorker = async (code) => {
@@ -206,7 +129,11 @@ __builtins__.input = input`);
 			code.includes('sklearn') ? 'scikit-learn' : null,
 			code.includes('scipy') ? 'scipy' : null,
 			code.includes('re') ? 'regex' : null,
-			code.includes('seaborn') ? 'seaborn' : null
+			code.includes('seaborn') ? 'seaborn' : null,
+			code.includes('sympy') ? 'sympy' : null,
+			code.includes('tiktoken') ? 'tiktoken' : null,
+			code.includes('matplotlib') ? 'matplotlib' : null,
+			code.includes('pytz') ? 'pytz' : null
 		].filter(Boolean);
 
 		console.log(packages);
@@ -233,7 +160,31 @@ __builtins__.input = input`);
 
 			console.log(id, data);
 
-			data['stdout'] && (stdout = data['stdout']);
+			if (data['stdout']) {
+				stdout = data['stdout'];
+				const stdoutLines = stdout.split('\n');
+
+				for (const [idx, line] of stdoutLines.entries()) {
+					if (line.startsWith('data:image/png;base64')) {
+						if (files) {
+							files.push({
+								type: 'image/png',
+								data: line
+							});
+						} else {
+							files = [
+								{
+									type: 'image/png',
+									data: line
+								}
+							];
+						}
+
+						stdout = stdout.replace(`${line}\n`, ``);
+					}
+				}
+			}
+
 			data['stderr'] && (stderr = data['stderr']);
 			data['result'] && (result = data['result']);
 
@@ -279,6 +230,36 @@ __builtins__.input = input`);
 
 	$: dispatch('code', { lang, code });
 
+	$: if (attributes) {
+		onAttributesUpdate();
+	}
+
+	const onAttributesUpdate = () => {
+		if (attributes?.output) {
+			// Create a helper function to unescape HTML entities
+			const unescapeHtml = (html) => {
+				const textArea = document.createElement('textarea');
+				textArea.innerHTML = html;
+				return textArea.value;
+			};
+
+			try {
+				// Unescape the HTML-encoded string
+				const unescapedOutput = unescapeHtml(attributes.output);
+
+				// Parse the unescaped string into JSON
+				const output = JSON.parse(unescapedOutput);
+
+				// Assign the parsed values to variables
+				stdout = output.stdout;
+				stderr = output.stderr;
+				result = output.result;
+			} catch (error) {
+				console.error('Error:', error);
+			}
+		}
+	};
+
 	onMount(async () => {
 		console.log('codeblock', lang, code);
 
@@ -376,18 +357,43 @@ __builtins__.input = input`);
 
 			<div
 				id="plt-canvas-{id}"
-				class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
+				class="bg-gray-50 dark:bg-[#202123] dark:text-white max-w-full overflow-x-auto scrollbar-hidden"
 			/>
 
-			{#if executing}
-				<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
-					<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
-					<div class="text-sm">Running...</div>
-				</div>
-			{:else if stdout || stderr || result}
-				<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
-					<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
-					<div class="text-sm">{stdout || stderr || result}</div>
+			{#if executing || stdout || stderr || result}
+				<div
+					class="bg-gray-50 dark:bg-[#202123] dark:text-white !rounded-b-lg py-4 px-4 flex flex-col gap-2"
+				>
+					{#if executing}
+						<div class=" ">
+							<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
+							<div class="text-sm">Running...</div>
+						</div>
+					{:else}
+						{#if stdout || stderr}
+							<div class=" ">
+								<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
+								<div class="text-sm">{stdout || stderr}</div>
+							</div>
+						{/if}
+						{#if result || files}
+							<div class=" ">
+								<div class=" text-gray-500 text-xs mb-1">RESULT</div>
+								{#if result}
+									<div class="text-sm">{`${JSON.stringify(result)}`}</div>
+								{/if}
+								{#if files}
+									<div class="flex flex-col gap-2">
+										{#each files as file}
+											{#if file.type.startsWith('image')}
+												<img src={file.data} alt="Output" />
+											{/if}
+										{/each}
+									</div>
+								{/if}
+							</div>
+						{/if}
+					{/if}
 				</div>
 			{/if}
 		{/if}

+ 8 - 2
src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte

@@ -23,6 +23,7 @@
 	export let id: string;
 	export let tokens: Token[];
 	export let top = true;
+	export let attributes = {};
 
 	export let save = false;
 	export let onSourceClick: Function = () => {};
@@ -83,6 +84,7 @@
 				{token}
 				lang={token?.lang ?? ''}
 				code={token?.text ?? ''}
+				{attributes}
 				{save}
 				on:code={(e) => {
 					dispatch('code', e.detail);
@@ -195,9 +197,13 @@
 			</ul>
 		{/if}
 	{:else if token.type === 'details'}
-		<Collapsible title={token.summary} attributes={token?.attributes} className="w-fit space-y-1">
+		<Collapsible title={token.summary} attributes={token?.attributes} className="w-full space-y-1">
 			<div class=" mb-1.5" slot="content">
-				<svelte:self id={`${id}-${tokenIdx}-d`} tokens={marked.lexer(token.text)} />
+				<svelte:self
+					id={`${id}-${tokenIdx}-d`}
+					tokens={marked.lexer(token.text)}
+					attributes={token?.attributes}
+				/>
 			</div>
 		</Collapsible>
 	{:else if token.type === 'html'}

+ 2 - 0
src/lib/components/chat/Messages/Message.svelte

@@ -78,6 +78,7 @@
 				{rateMessage}
 				{actionMessage}
 				{submitMessage}
+				{deleteMessage}
 				{continueResponse}
 				{regenerateResponse}
 				{addMessages}
@@ -95,6 +96,7 @@
 				{rateMessage}
 				{actionMessage}
 				{submitMessage}
+				{deleteMessage}
 				{continueResponse}
 				{regenerateResponse}
 				{mergeResponses}

+ 6 - 1
src/lib/components/chat/Messages/MultiResponseMessages.svelte

@@ -16,7 +16,9 @@
 	import Markdown from './Markdown.svelte';
 	import Name from './Name.svelte';
 	import Skeleton from './Skeleton.svelte';
+	import localizedFormat from 'dayjs/plugin/localizedFormat';
 	const i18n = getContext('i18n');
+	dayjs.extend(localizedFormat);
 
 	export let chatId;
 	export let history;
@@ -32,6 +34,8 @@
 	export let actionMessage: Function;
 
 	export let submitMessage: Function;
+	export let deleteMessage: Function;
+
 	export let continueResponse: Function;
 	export let regenerateResponse: Function;
 	export let mergeResponses: Function;
@@ -226,6 +230,7 @@
 									{editMessage}
 									{saveMessage}
 									{rateMessage}
+									{deleteMessage}
 									{actionMessage}
 									{submitMessage}
 									{continueResponse}
@@ -264,7 +269,7 @@
 										<span
 											class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
 										>
-											{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
+											{dayjs(message.timestamp * 1000).format('LT')}
 										</span>
 									{/if}
 								</Name>

+ 36 - 1
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -114,6 +114,7 @@
 	export let saveMessage: Function;
 	export let rateMessage: Function;
 	export let actionMessage: Function;
+	export let deleteMessage: Function;
 
 	export let submitMessage: Function;
 	export let continueResponse: Function;
@@ -461,6 +462,10 @@
 		feedbackLoading = false;
 	};
 
+	const deleteMessageHandler = async () => {
+		deleteMessage(message.id);
+	};
+
 	$: if (!edit) {
 		(async () => {
 			await tick();
@@ -500,7 +505,7 @@
 					<div
 						class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
 					>
-						<Tooltip content={dayjs(message.timestamp * 1000).format('dddd, DD MMMM YYYY HH:mm')}>
+						<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
 							<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
 						</Tooltip>
 					</div>
@@ -1176,6 +1181,36 @@
 										</button>
 									</Tooltip>
 
+									{#if siblings.length > 1}
+										<Tooltip content={$i18n.t('Delete')} placement="bottom">
+											<button
+												type="button"
+												id="continue-response-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={() => {
+													deleteMessageHandler();
+												}}
+											>
+												<svg
+													xmlns="http://www.w3.org/2000/svg"
+													fill="none"
+													viewBox="0 0 24 24"
+													stroke-width="2"
+													stroke="currentColor"
+													class="w-4 h-4"
+												>
+													<path
+														stroke-linecap="round"
+														stroke-linejoin="round"
+														d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
+													/>
+												</svg>
+											</button>
+										</Tooltip>
+									{/if}
+
 									{#if isLastMessage}
 										{#each model?.actions ?? [] as action}
 											<Tooltip content={action.name} placement="bottom">

+ 3 - 1
src/lib/components/chat/Messages/UserMessage.svelte

@@ -13,8 +13,10 @@
 	import FileItem from '$lib/components/common/FileItem.svelte';
 	import Markdown from './Markdown.svelte';
 	import Image from '$lib/components/common/Image.svelte';
+	import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 	const i18n = getContext('i18n');
+	dayjs.extend(localizedFormat);
 
 	export let user;
 
@@ -112,7 +114,7 @@
 						<div
 							class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
 						>
-							<Tooltip content={dayjs(message.timestamp * 1000).format('dddd, DD MMMM YYYY HH:mm')}>
+							<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
 								<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
 							</Tooltip>
 						</div>

+ 8 - 9
src/lib/components/chat/Placeholder.svelte

@@ -35,6 +35,7 @@
 
 	export let selectedToolIds = [];
 	export let imageGenerationEnabled = false;
+	export let codeInterpreterEnabled = false;
 	export let webSearchEnabled = false;
 
 	let models = [];
@@ -87,7 +88,7 @@
 	onMount(() => {});
 </script>
 
-<div class="m-auto w-full max-w-6xl px-2 xl:px-20 translate-y-6 py-24 text-center">
+<div class="m-auto w-full max-w-6xl px-2 @2xl:px-20 translate-y-6 py-24 text-center">
 	{#if $temporaryChatEnabled}
 		<Tooltip
 			content="This chat won't appear in history and your messages will not be saved."
@@ -104,7 +105,7 @@
 		class="w-full text-3xl text-gray-800 dark:text-gray-100 font-medium text-center flex items-center gap-4 font-primary"
 	>
 		<div class="w-full flex flex-col justify-center items-center">
-			<div class="flex flex-row justify-center gap-3 sm:gap-3.5 w-fit px-5">
+			<div class="flex flex-row justify-center gap-3 @sm:gap-3.5 w-fit px-5">
 				<div class="flex flex-shrink-0 justify-center">
 					<div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 100 }}>
 						{#each models as model, modelIdx}
@@ -125,7 +126,7 @@
 											($i18n.language === 'dg-DG'
 												? `/doge.png`
 												: `${WEBUI_BASE_URL}/static/favicon.png`)}
-										class=" size-9 sm:size-10 rounded-full border-[1px] border-gray-200 dark:border-none"
+										class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-200 dark:border-none"
 										alt="logo"
 										draggable="false"
 									/>
@@ -135,7 +136,7 @@
 					</div>
 				</div>
 
-				<div class=" text-3xl sm:text-4xl line-clamp-1" in:fade={{ duration: 100 }}>
+				<div class=" text-3xl @sm:text-4xl line-clamp-1" in:fade={{ duration: 100 }}>
 					{#if models[selectedModelIdx]?.name}
 						{models[selectedModelIdx]?.name}
 					{:else}
@@ -183,11 +184,7 @@
 				</div>
 			</div>
 
-			<div
-				class="text-base font-normal xl:translate-x-6 md:max-w-3xl w-full py-3 {atSelectedModel
-					? 'mt-2'
-					: ''}"
-			>
+			<div class="text-base font-normal @md:max-w-3xl w-full py-3 {atSelectedModel ? 'mt-2' : ''}">
 				<MessageInput
 					{history}
 					{selectedModels}
@@ -196,6 +193,7 @@
 					bind:autoScroll
 					bind:selectedToolIds
 					bind:imageGenerationEnabled
+					bind:codeInterpreterEnabled
 					bind:webSearchEnabled
 					bind:atSelectedModel
 					{transparentBackground}
@@ -218,6 +216,7 @@
 				suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
 					$config?.default_prompt_suggestions ??
 					[]}
+				inputValue={prompt}
 				on:select={(e) => {
 					selectSuggestionPrompt(e.detail);
 				}}

+ 30 - 0
src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte

@@ -12,6 +12,7 @@
 	export let params = {
 		// Advanced
 		stream_response: null, // Set stream responses for this model individually
+		function_calling: null,
 		seed: null,
 		stop: null,
 		temperature: null,
@@ -81,6 +82,35 @@
 		</Tooltip>
 	</div>
 
+	<div>
+		<Tooltip
+			content={$i18n.t(
+				'Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.'
+			)}
+			placement="top-start"
+			className="inline-tooltip"
+		>
+			<div class=" py-0.5 flex w-full justify-between">
+				<div class=" self-center text-xs font-medium">
+					{$i18n.t('Function Calling')}
+				</div>
+				<button
+					class="p-1 px-3 text-xs flex rounded transition"
+					on:click={() => {
+						params.function_calling = (params?.function_calling ?? null) === null ? 'native' : null;
+					}}
+					type="button"
+				>
+					{#if params.function_calling === 'native'}
+						<span class="ml-2 self-center">{$i18n.t('Native')}</span>
+					{:else}
+						<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+					{/if}
+				</button>
+			</div>
+		</Tooltip>
+	</div>
+
 	<div class=" py-0.5 w-full justify-between">
 		<Tooltip
 			content={$i18n.t(

+ 1 - 1
src/lib/components/chat/Settings/Audio.svelte

@@ -39,7 +39,7 @@
 			}, 100);
 		} else {
 			const res = await _getVoices(localStorage.token).catch((e) => {
-				toast.error(e);
+				toast.error(`${e}`);
 			});
 
 			if (res) {

+ 1 - 0
src/lib/components/chat/Settings/General.svelte

@@ -46,6 +46,7 @@
 	let params = {
 		// Advanced
 		stream_response: null,
+		function_calling: null,
 		seed: null,
 		temperature: null,
 		frequency_penalty: null,

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

@@ -52,6 +52,8 @@
 	let voiceInterruption = false;
 	let hapticFeedback = false;
 
+	let webSearch = null;
+
 	const toggleSplitLargeChunks = async () => {
 		splitLargeChunks = !splitLargeChunks;
 		saveSettings({ splitLargeChunks: splitLargeChunks });
@@ -198,6 +200,11 @@
 		});
 	};
 
+	const toggleWebSearch = async () => {
+		webSearch = webSearch === null ? 'always' : null;
+		saveSettings({ webSearch: webSearch });
+	};
+
 	onMount(async () => {
 		titleAutoGenerate = $settings?.title?.auto ?? true;
 		autoTags = $settings.autoTags ?? true;
@@ -235,6 +242,7 @@
 		}
 
 		backgroundImageUrl = $settings.backgroundImageUrl ?? null;
+		webSearch = $settings.webSearch ?? null;
 	});
 </script>
 
@@ -666,6 +674,26 @@
 				</div>
 			</div>
 
+			<div>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs">{$i18n.t('Web Search in Chat')}</div>
+
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						on:click={() => {
+							toggleWebSearch();
+						}}
+						type="button"
+					>
+						{#if webSearch === 'always'}
+							<span class="ml-2 self-center">{$i18n.t('Always')}</span>
+						{:else}
+							<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+						{/if}
+					</button>
+				</div>
+			</div>
+
 			<div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
 
 			<div>

+ 3 - 3
src/lib/components/chat/Settings/Personalization/ManageModal.svelte

@@ -11,8 +11,10 @@
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import { error } from '@sveltejs/kit';
 	import EditMemoryModal from './EditMemoryModal.svelte';
+	import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 	const i18n = getContext('i18n');
+	dayjs.extend(localizedFormat);
 
 	export let show = false;
 
@@ -84,9 +86,7 @@
 											</td>
 											<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
 												<div class="my-auto whitespace-nowrap">
-													{dayjs(memory.updated_at * 1000).format(
-														$i18n.t('MMMM DD, YYYY hh:mm:ss A')
-													)}
+													{dayjs(memory.updated_at * 1000).format('LLL')}
 												</div>
 											</td>
 											<td class="px-3 py-1">

+ 2 - 1
src/lib/components/chat/SettingsModal.svelte

@@ -118,7 +118,8 @@
 				'displayemoji',
 				'save',
 				'interfaceoptions',
-				'interfacecustomization'
+				'interfacecustomization',
+				'alwaysonwebsearch'
 			]
 		},
 		{

+ 114 - 36
src/lib/components/chat/Suggestions.svelte

@@ -1,53 +1,131 @@
 <script lang="ts">
+	import Fuse from 'fuse.js';
 	import Bolt from '$lib/components/icons/Bolt.svelte';
 	import { onMount, getContext, createEventDispatcher } from 'svelte';
+	import { WEBUI_NAME } from '$lib/stores';
+	import { WEBUI_VERSION } from '$lib/constants';
 
 	const i18n = getContext('i18n');
 	const dispatch = createEventDispatcher();
 
 	export let suggestionPrompts = [];
 	export let className = '';
+	export let inputValue = '';
 
-	let prompts = [];
+	let sortedPrompts = [];
 
-	$: prompts = (suggestionPrompts ?? [])
-		.reduce((acc, current) => [...acc, ...[current]], [])
-		.sort(() => Math.random() - 0.5);
+	const fuseOptions = {
+		keys: ['content', 'title'],
+		threshold: 0.5
+	};
+
+	let fuse;
+	let filteredPrompts = [];
+
+	// Initialize Fuse
+	$: fuse = new Fuse(sortedPrompts, fuseOptions);
+
+	// Update the filteredPrompts if inputValue changes
+	// Only increase version if something wirklich geändert hat
+	$: getFilteredPrompts(inputValue);
+
+	// Helper function to check if arrays are the same
+	// (based on unique IDs oder content)
+	function arraysEqual(a, b) {
+		if (a.length !== b.length) return false;
+		for (let i = 0; i < a.length; i++) {
+			if ((a[i].id ?? a[i].content) !== (b[i].id ?? b[i].content)) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	const getFilteredPrompts = (inputValue) => {
+		const newFilteredPrompts = inputValue.trim()
+			? fuse.search(inputValue.trim()).map((result) => result.item)
+			: sortedPrompts;
+
+		// Compare with the oldFilteredPrompts
+		// If there's a difference, update array + version
+		if (!arraysEqual(filteredPrompts, newFilteredPrompts)) {
+			filteredPrompts = newFilteredPrompts;
+		}
+	};
+
+	$: if (suggestionPrompts) {
+		sortedPrompts = [...(suggestionPrompts ?? [])].sort(() => Math.random() - 0.5);
+		getFilteredPrompts(inputValue);
+	}
 </script>
 
-{#if prompts.length > 0}
-	<div class="mb-1 flex gap-1 text-sm font-medium items-center text-gray-400 dark:text-gray-600">
+<div class="mb-1 flex gap-1 text-xs font-medium items-center text-gray-400 dark:text-gray-600">
+	{#if filteredPrompts.length > 0}
 		<Bolt />
 		{$i18n.t('Suggested')}
-	</div>
-{/if}
-
-<div class=" h-40 max-h-full overflow-auto scrollbar-none {className}">
-	{#each prompts as prompt, promptIdx}
-		<button
-			class="flex flex-col flex-1 shrink-0 w-full justify-between px-3 py-2 rounded-xl bg-transparent hover:bg-black/5 dark:hover:bg-white/5 transition group"
-			on:click={() => {
-				dispatch('select', prompt.content);
-			}}
+	{:else}
+		<!-- Keine Vorschläge -->
+
+		<div
+			class="flex w-full text-center items-center justify-center self-start text-gray-400 dark:text-gray-600"
 		>
-			<div class="flex flex-col text-left">
-				{#if prompt.title && prompt.title[0] !== ''}
-					<div
-						class="  font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
-					>
-						{prompt.title[0]}
-					</div>
-					<div class="text-xs text-gray-500 font-normal line-clamp-1">{prompt.title[1]}</div>
-				{:else}
-					<div
-						class="  font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
-					>
-						{prompt.content}
-					</div>
-
-					<div class="text-xs text-gray-500 font-normal line-clamp-1">Prompt</div>
-				{/if}
-			</div>
-		</button>
-	{/each}
+			{$WEBUI_NAME} ‧ v{WEBUI_VERSION}
+		</div>
+	{/if}
+</div>
+
+<div class="h-40 overflow-auto scrollbar-none {className} items-start">
+	{#if filteredPrompts.length > 0}
+		{#each filteredPrompts as prompt, idx (prompt.id || prompt.content)}
+			<button
+				class="waterfall flex flex-col flex-1 shrink-0 w-full justify-between
+				       px-3 py-2 rounded-xl bg-transparent hover:bg-black/5
+				       dark:hover:bg-white/5 transition group"
+				style="animation-delay: {idx * 60}ms"
+				on:click={() => dispatch('select', prompt.content)}
+			>
+				<div class="flex flex-col text-left">
+					{#if prompt.title && prompt.title[0] !== ''}
+						<div
+							class="font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
+						>
+							{prompt.title[0]}
+						</div>
+						<div class="text-xs text-gray-500 font-normal line-clamp-1">
+							{prompt.title[1]}
+						</div>
+					{:else}
+						<div
+							class="font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
+						>
+							{prompt.content}
+						</div>
+						<div class="text-xs text-gray-500 font-normal line-clamp-1">{$i18n.t('Prompt')}</div>
+					{/if}
+				</div>
+			</button>
+		{/each}
+	{/if}
 </div>
+
+<style>
+	/* Waterfall animation for the suggestions */
+	@keyframes fadeInUp {
+		0% {
+			opacity: 0;
+			transform: translateY(20px);
+		}
+		100% {
+			opacity: 1;
+			transform: translateY(0);
+		}
+	}
+
+	.waterfall {
+		opacity: 0;
+		animation-name: fadeInUp;
+		animation-duration: 200ms;
+		animation-fill-mode: forwards;
+		animation-timing-function: ease;
+	}
+</style>

+ 6 - 0
src/lib/components/common/Collapsible.svelte

@@ -80,6 +80,12 @@
 						{:else}
 							{$i18n.t('Thinking...')}
 						{/if}
+					{:else if attributes?.type === 'code_interpreter'}
+						{#if attributes?.done === 'true'}
+							{$i18n.t('Analyzed')}
+						{:else}
+							{$i18n.t('Analyzing...')}
+						{/if}
 					{:else}
 						{title}
 					{/if}

+ 1 - 1
src/lib/components/common/FileItem.svelte

@@ -119,7 +119,7 @@
 	{#if dismissible}
 		<div class=" absolute -top-1 -right-1">
 			<button
-				class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
+				class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
 				type="button"
 				on:click|stopPropagation={() => {
 					dispatch('dismiss');

+ 32 - 51
src/lib/components/common/Textarea.svelte

@@ -3,68 +3,49 @@
 
 	export let value = '';
 	export let placeholder = '';
+	export let rows = 1;
+	export let required = false;
 	export let className =
-		'w-full rounded-lg px-3 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none h-full';
-
-	export let onKeydown: Function = () => {};
+		'w-full rounded-lg px-3 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none  h-full';
 
 	let textareaElement;
 
-	$: if (textareaElement) {
-		if (textareaElement.innerText !== value && value !== '') {
-			textareaElement.innerText = value ?? '';
-		}
-	}
-
 	// Adjust height on mount and after setting the element.
 	onMount(async () => {
 		await tick();
+		resize();
+
+		requestAnimationFrame(() => {
+			// setInterveal to cehck until textareaElement is set
+			const interval = setInterval(() => {
+				if (textareaElement) {
+					clearInterval(interval);
+					resize();
+				}
+			}, 100);
+		});
 	});
 
-	// Handle paste event to ensure only plaintext is pasted
-	function handlePaste(event: ClipboardEvent) {
-		event.preventDefault(); // Prevent the default paste action
-		const clipboardData = event.clipboardData?.getData('text/plain'); // Get plaintext from clipboard
-
-		// Insert plaintext into the textarea
-		document.execCommand('insertText', false, clipboardData);
-	}
+	const resize = () => {
+		if (textareaElement) {
+			textareaElement.style.height = '';
+			textareaElement.style.height = `${textareaElement.scrollHeight}px`;
+		}
+	};
 </script>
 
-<div
-	contenteditable="true"
+<textarea
 	bind:this={textareaElement}
-	class="{className} whitespace-pre-wrap relative {value
-		? !value.trim()
-			? 'placeholder'
-			: ''
-		: 'placeholder'}"
-	style="field-sizing: content; -moz-user-select: text !important;"
-	on:input={() => {
-		const text = textareaElement.innerText;
-		if (text === '\n') {
-			value = '';
-			return;
-		}
-
-		value = text;
+	bind:value
+	{placeholder}
+	class={className}
+	style="field-sizing: content;"
+	{rows}
+	{required}
+	on:input={(e) => {
+		resize();
+	}}
+	on:focus={() => {
+		resize();
 	}}
-	on:paste={handlePaste}
-	on:keydown={onKeydown}
-	data-placeholder={placeholder}
 />
-
-<style>
-	.placeholder::before {
-		/* absolute */
-		position: absolute;
-		content: attr(data-placeholder);
-		color: #adb5bd;
-		overflow: hidden;
-		display: -webkit-box;
-		-webkit-box-orient: vertical;
-		-webkit-line-clamp: 1;
-		pointer-events: none;
-		touch-action: none;
-	}
-</style>

+ 19 - 0
src/lib/components/icons/CommandLine.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+	export let className = 'w-4 h-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+>
+	<path
+		stroke-linecap="round"
+		stroke-linejoin="round"
+		d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"
+	/>
+</svg>

+ 11 - 0
src/lib/components/icons/CommandLineSolid.svelte

@@ -0,0 +1,11 @@
+<script lang="ts">
+	export let className = 'size-4';
+</script>
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}>
+	<path
+		fill-rule="evenodd"
+		d="M2.25 6a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H5.25a3 3 0 0 1-3-3V6Zm3.97.97a.75.75 0 0 1 1.06 0l2.25 2.25a.75.75 0 0 1 0 1.06l-2.25 2.25a.75.75 0 0 1-1.06-1.06l1.72-1.72-1.72-1.72a.75.75 0 0 1 0-1.06Zm4.28 4.28a.75.75 0 0 0 0 1.5h3a.75.75 0 0 0 0-1.5h-3Z"
+		clip-rule="evenodd"
+	/>
+</svg>

+ 21 - 0
src/lib/components/icons/Photo.svelte

@@ -0,0 +1,21 @@
+<script lang="ts">
+	export let className = 'size-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	class={className}
+	stroke-width={strokeWidth}
+	aria-hidden="true"
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+>
+	<path
+		stroke="currentColor"
+		stroke-linecap="round"
+		stroke-linejoin="round"
+		stroke-width="2"
+		d="m3 16 5-7 6 6.5m6.5 2.5L16 13l-4.286 6M14 10h.01M4 19h16a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1Z"
+	/>
+</svg>

+ 2 - 2
src/lib/components/layout/Help.svelte

@@ -10,7 +10,7 @@
 	let showShortcuts = false;
 </script>
 
-<div class=" hidden lg:flex fixed bottom-0 right-0 px-2 py-2 z-20">
+<div class=" hidden lg:flex fixed bottom-0 right-0 px-1 py-1 z-20">
 	<button
 		id="show-shortcuts-button"
 		class="hidden"
@@ -29,7 +29,7 @@
 	>
 		<Tooltip content={$i18n.t('Help')} placement="left">
 			<button
-				class="text-gray-600 dark:text-gray-300 bg-gray-300/20 size-5 flex items-center justify-center text-[0.7rem] rounded-full"
+				class="text-gray-600 dark:text-gray-300 bg-gray-300/20 size-4 flex items-center justify-center text-[0.7rem] rounded-full"
 			>
 				?
 			</button>

+ 4 - 1
src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte

@@ -4,6 +4,9 @@
 	import { toast } from 'svelte-sonner';
 	import dayjs from 'dayjs';
 	import { getContext, createEventDispatcher } from 'svelte';
+	import localizedFormat from 'dayjs/plugin/localizedFormat';
+
+	dayjs.extend(localizedFormat);
 
 	const dispatch = createEventDispatcher();
 
@@ -159,7 +162,7 @@
 
 												<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
 													<div class="my-auto">
-														{dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
+														{dayjs(chat.created_at * 1000).format('LLL')}
 													</div>
 												</td>
 

+ 7 - 1
src/lib/components/layout/Sidebar/ChatItem.svelte

@@ -87,7 +87,13 @@
 	};
 
 	const cloneChatHandler = async (id) => {
-		const res = await cloneChatById(localStorage.token, id).catch((error) => {
+		const res = await cloneChatById(
+			localStorage.token,
+			id,
+			$i18n.t('Clone of {{TITLE}}', {
+				TITLE: title
+			})
+		).catch((error) => {
 			toast.error(`${error}`);
 			return null;
 		});

+ 26 - 1
src/lib/components/playground/Chat.svelte

@@ -33,6 +33,7 @@
 	let loading = false;
 	let stopResponseFlag = false;
 
+	let systemTextareaElement: HTMLTextAreaElement;
 	let messagesContainerElement: HTMLDivElement;
 
 	let showSystem = false;
@@ -58,8 +59,29 @@
 		console.log('stopResponse');
 	};
 
+	const resizeSystemTextarea = async () => {
+		await tick();
+		if (systemTextareaElement) {
+			systemTextareaElement.style.height = '';
+			systemTextareaElement.style.height = Math.min(systemTextareaElement.scrollHeight, 555) + 'px';
+		}
+	};
+
+	$: if (showSystem) {
+		resizeSystemTextarea();
+	}
+
 	const chatCompletionHandler = async () => {
+		if (selectedModelId === '') {
+			toast.error($i18n.t('Please select a model.'));
+			return;
+		}
+
 		const model = $models.find((model) => model.id === selectedModelId);
+		if (!model) {
+			selectedModelId = '';
+			return;
+		}
 
 		const [res, controller] = await chatCompletion(
 			localStorage.token,
@@ -258,10 +280,13 @@
 					<div slot="content">
 						<div class="pt-1 px-1.5">
 							<textarea
-								id="system-textarea"
+								bind:this={systemTextareaElement}
 								class="w-full h-full bg-transparent resize-none outline-none text-sm"
 								bind:value={system}
 								placeholder={$i18n.t("You're a helpful assistant.")}
+								on:input={() => {
+									resizeSystemTextarea();
+								}}
 								rows="4"
 							/>
 						</div>

+ 76 - 0
src/lib/components/playground/Chat/Message.svelte

@@ -0,0 +1,76 @@
+<script lang="ts">
+	import { onMount, getContext } from 'svelte';
+
+	const i18n = getContext('i18n');
+
+	export let message;
+	export let idx;
+
+	export let onDelete;
+
+	let textAreaElement: HTMLTextAreaElement;
+
+	onMount(() => {
+		textAreaElement.style.height = '';
+		textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
+	});
+</script>
+
+<div class="flex gap-2 group">
+	<div class="flex items-start pt-1">
+		<div
+			class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left rounded-lg transition"
+		>
+			{$i18n.t(message.role)}
+		</div>
+	</div>
+
+	<div class="flex-1">
+		<!-- $i18n.t('a user') -->
+		<!-- $i18n.t('an assistant') -->
+		<textarea
+			id="{message.role}-{idx}-textarea"
+			bind:this={textAreaElement}
+			class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden"
+			placeholder={$i18n.t(`Enter {{role}} message here`, {
+				role: message.role === 'user' ? $i18n.t('a user') : $i18n.t('an assistant')
+			})}
+			rows="1"
+			on:input={(e) => {
+				textAreaElement.style.height = '';
+				textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
+			}}
+			on:focus={(e) => {
+				textAreaElement.style.height = '';
+				textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
+
+				// e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
+			}}
+			bind:value={message.content}
+		/>
+	</div>
+
+	<div class=" pt-1">
+		<button
+			class=" group-hover:text-gray-500 dark:text-gray-900 dark:hover:text-gray-300 transition"
+			on:click={() => {
+				onDelete();
+			}}
+		>
+			<svg
+				xmlns="http://www.w3.org/2000/svg"
+				fill="none"
+				viewBox="0 0 24 24"
+				stroke-width="2"
+				stroke="currentColor"
+				class="w-5 h-5"
+			>
+				<path
+					stroke-linecap="round"
+					stroke-linejoin="round"
+					d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
+				/>
+			</svg>
+		</button>
+	</div>
+</div>

+ 8 - 65
src/lib/components/playground/Chat/Messages.svelte

@@ -1,77 +1,20 @@
 <script lang="ts">
 	import { onMount, getContext } from 'svelte';
+	import Message from './Message.svelte';
 
 	const i18n = getContext('i18n');
 
 	export let messages = [];
-	let textAreaElement: HTMLTextAreaElement;
-	onMount(() => {
-		messages.forEach((message, idx) => {
-			textAreaElement.style.height = '';
-			textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
-		});
-	});
 </script>
 
 <div class="py-3 space-y-3">
 	{#each messages as message, idx}
-		<div class="flex gap-2 group">
-			<div class="flex items-start pt-1">
-				<div
-					class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left rounded-lg transition"
-				>
-					{$i18n.t(message.role)}
-				</div>
-			</div>
-
-			<div class="flex-1">
-				<!-- $i18n.t('a user') -->
-				<!-- $i18n.t('an assistant') -->
-				<textarea
-					id="{message.role}-{idx}-textarea"
-					bind:this={textAreaElement}
-					class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden"
-					placeholder={$i18n.t(`Enter {{role}} message here`, {
-						role: message.role === 'user' ? $i18n.t('a user') : $i18n.t('an assistant')
-					})}
-					rows="1"
-					on:input={(e) => {
-						textAreaElement.style.height = '';
-						textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
-					}}
-					on:focus={(e) => {
-						textAreaElement.style.height = '';
-						textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
-
-						// e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
-					}}
-					bind:value={message.content}
-				/>
-			</div>
-
-			<div class=" pt-1">
-				<button
-					class=" group-hover:text-gray-500 dark:text-gray-900 dark:hover:text-gray-300 transition"
-					on:click={() => {
-						messages = messages.filter((message, messageIdx) => messageIdx !== idx);
-					}}
-				>
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						fill="none"
-						viewBox="0 0 24 24"
-						stroke-width="2"
-						stroke="currentColor"
-						class="w-5 h-5"
-					>
-						<path
-							stroke-linecap="round"
-							stroke-linejoin="round"
-							d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
-						/>
-					</svg>
-				</button>
-			</div>
-		</div>
+		<Message
+			{message}
+			{idx}
+			onDelete={() => {
+				messages = messages.filter((message, messageIdx) => messageIdx !== idx);
+			}}
+		/>
 	{/each}
 </div>

+ 1 - 1
src/lib/components/workspace/Knowledge.svelte

@@ -54,7 +54,7 @@
 
 	const deleteHandler = async (item) => {
 		const res = await deleteKnowledgeById(localStorage.token, item.id).catch((e) => {
-			toast.error(e);
+			toast.error(`${e}`);
 		});
 
 		if (res) {

+ 2 - 2
src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte

@@ -31,7 +31,7 @@
 			description,
 			accessControl
 		).catch((e) => {
-			toast.error(e);
+			toast.error(`${e}`);
 		});
 
 		if (res) {
@@ -112,7 +112,7 @@
 
 		<div class="mt-2">
 			<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
-				<AccessControl bind:accessControl />
+				<AccessControl bind:accessControl accessRoles={['read', 'write']} />
 			</div>
 		</div>
 

+ 9 - 9
src/lib/components/workspace/Knowledge/KnowledgeBase.svelte

@@ -149,7 +149,7 @@
 
 		try {
 			const uploadedFile = await uploadFile(localStorage.token, file).catch((e) => {
-				toast.error(e);
+				toast.error(`${e}`);
 				return null;
 			});
 
@@ -169,7 +169,7 @@
 				toast.error($i18n.t('Failed to upload file.'));
 			}
 		} catch (e) {
-			toast.error(e);
+			toast.error(`${e}`);
 		}
 	};
 
@@ -339,7 +339,7 @@
 	const syncDirectoryHandler = async () => {
 		if ((knowledge?.files ?? []).length > 0) {
 			const res = await resetKnowledgeById(localStorage.token, id).catch((e) => {
-				toast.error(e);
+				toast.error(`${e}`);
 			});
 
 			if (res) {
@@ -357,7 +357,7 @@
 	const addFileHandler = async (fileId) => {
 		const updatedKnowledge = await addFileToKnowledgeById(localStorage.token, id, fileId).catch(
 			(e) => {
-				toast.error(e);
+				toast.error(`${e}`);
 				return null;
 			}
 		);
@@ -386,7 +386,7 @@
 			}
 		} catch (e) {
 			console.error('Error in deleteFileHandler:', e);
-			toast.error(e);
+			toast.error(`${e}`);
 		}
 	};
 
@@ -395,7 +395,7 @@
 		const content = selectedFile.data.content;
 
 		const res = updateFileDataContentById(localStorage.token, fileId, content).catch((e) => {
-			toast.error(e);
+			toast.error(`${e}`);
 		});
 
 		const updatedKnowledge = await updateFileFromKnowledgeById(
@@ -403,7 +403,7 @@
 			id,
 			fileId
 		).catch((e) => {
-			toast.error(e);
+			toast.error(`${e}`);
 		});
 
 		if (res && updatedKnowledge) {
@@ -430,7 +430,7 @@
 				description: knowledge.description,
 				access_control: knowledge.access_control
 			}).catch((e) => {
-				toast.error(e);
+				toast.error(`${e}`);
 			});
 
 			if (res) {
@@ -522,7 +522,7 @@
 		id = $page.params.id;
 
 		const res = await getKnowledgeById(localStorage.token, id).catch((e) => {
-			toast.error(e);
+			toast.error(`${e}`);
 			return null;
 		});
 

+ 1 - 1
src/lib/components/workspace/Models.svelte

@@ -60,7 +60,7 @@
 
 	const deleteModelHandler = async (model) => {
 		const res = await deleteModelById(localStorage.token, model.id).catch((e) => {
-			toast.error(e);
+			toast.error(`${e}`);
 			return null;
 		});
 

+ 1 - 1
src/lib/components/workspace/Models/ModelEditor.svelte

@@ -531,7 +531,7 @@
 
 					<div class="my-2">
 						<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
-							<AccessControl bind:accessControl />
+							<AccessControl bind:accessControl accessRoles={['read', 'write']} />
 						</div>
 					</div>
 

+ 1 - 1
src/lib/components/workspace/common/ValvesModal.svelte

@@ -136,7 +136,7 @@
 
 					<div class="flex justify-end pt-3 text-sm font-medium">
 						<button
-							class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center {saving
+							class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full {saving
 								? ' cursor-not-allowed'
 								: ''}"
 							type="submit"

+ 18 - 6
src/lib/i18n/locales/ar-BH/translation.json

@@ -63,8 +63,11 @@
 	"Allowed Endpoints": "",
 	"Already have an account?": "هل تملك حساب ؟",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "",
+	"Always": "",
 	"Amazing": "",
 	"an assistant": "مساعد",
+	"Analyzed": "",
+	"Analyzing...": "",
 	"and": "و",
 	"and {{COUNT}} more": "",
 	"and create a new shared link.": "و أنشئ رابط مشترك جديد.",
@@ -169,9 +172,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "",
 	"Clone": "استنساخ",
 	"Clone Chat": "",
+	"Clone of {{TITLE}}": "",
 	"Close": "أغلق",
 	"Code execution": "",
 	"Code formatted successfully": "",
+	"Code Interpreter": "",
 	"Collection": "مجموعة",
 	"Color": "",
 	"ComfyUI": "ComfyUI",
@@ -233,6 +238,7 @@
 	"Default": "الإفتراضي",
 	"Default (Open AI)": "",
 	"Default (SentenceTransformers)": "(SentenceTransformers) الإفتراضي",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
 	"Default Model": "النموذج الافتراضي",
 	"Default model updated": "الإفتراضي تحديث الموديل",
 	"Default Models": "",
@@ -343,6 +349,7 @@
 	"Enter Chunk Overlap": "أدخل الChunk Overlap",
 	"Enter Chunk Size": "أدخل Chunk الحجم",
 	"Enter description": "",
+	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "أدخل عنوان URL ل Github Raw",
 	"Enter Google PSE API Key": "أدخل مفتاح واجهة برمجة تطبيقات PSE من Google",
 	"Enter Google PSE Engine Id": "أدخل معرف محرك PSE من Google",
@@ -391,12 +398,14 @@
 	"Error accessing Google Drive: {{error}}": "",
 	"Error uploading file: {{error}}": "",
 	"Evaluations": "",
+	"Exa API Key": "",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "",
 	"Example: ALL": "",
 	"Example: mail": "",
 	"Example: ou=users,dc=foo,dc=example": "",
 	"Example: sAMAccountName or uid or userPrincipalName": "",
 	"Exclude": "",
+	"Execute code for analysis": "",
 	"Experimental": "تجريبي",
 	"Explore the cosmos": "",
 	"Export": "تصدير",
@@ -449,6 +458,7 @@
 	"Format your variables using brackets like this:": "",
 	"Frequency Penalty": "عقوبة التردد",
 	"Function": "",
+	"Function Calling": "",
 	"Function created successfully": "",
 	"Function deleted successfully": "",
 	"Function Description": "",
@@ -463,6 +473,7 @@
 	"Functions imported successfully": "",
 	"General": "عام",
 	"General Settings": "الاعدادات العامة",
+	"Generate an image": "",
 	"Generate Image": "",
 	"Generating search query": "إنشاء استعلام بحث",
 	"Get started": "",
@@ -478,7 +489,6 @@
 	"Group Name": "",
 	"Group updated successfully": "",
 	"Groups": "",
-	"h:mm a": "الساعة:الدقائق صباحا/مساء",
 	"Haptic Feedback": "",
 	"has no conversations.": "ليس لديه محادثات.",
 	"Hello, {{name}}": " {{name}} مرحبا",
@@ -496,7 +506,6 @@
 	"Ignite curiosity": "",
 	"Image": "",
 	"Image Compression": "",
-	"Image generation": "",
 	"Image Generation": "",
 	"Image Generation (Experimental)": "توليد الصور (تجريبي)",
 	"Image Generation Engine": "محرك توليد الصور",
@@ -595,9 +604,6 @@
 	"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": "",
 	"Model": "",
 	"Model '{{modelName}}' has been successfully downloaded.": "تم تحميل النموذج '{{modelName}}' بنجاح",
 	"Model '{{modelTag}}' is already in queue for downloading.": "النموذج '{{modelTag}}' موجود بالفعل في قائمة الانتظار للتحميل",
@@ -624,6 +630,7 @@
 	"More": "المزيد",
 	"Name": "الأسم",
 	"Name your knowledge base": "",
+	"Native": "",
 	"New Chat": "دردشة جديدة",
 	"New Folder": "",
 	"New Password": "كلمة المرور الجديدة",
@@ -718,6 +725,7 @@
 	"Please enter a prompt": "",
 	"Please fill in all fields.": "",
 	"Please select a model first.": "",
+	"Please select a model.": "",
 	"Please select a reason": "",
 	"Port": "",
 	"Positive attitude": "موقف ايجابي",
@@ -726,6 +734,7 @@
 	"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": "محتوى عاجل",
 	"Prompt created successfully": "",
@@ -801,7 +810,7 @@
 	"Search options": "",
 	"Search Prompts": "أبحث حث",
 	"Search Result Count": "عدد نتائج البحث",
-	"Search the web": "",
+	"Search the internet": "",
 	"Search Tools": "",
 	"SearchApi API Key": "",
 	"SearchApi Engine": "",
@@ -971,6 +980,7 @@
 	"Tools": "",
 	"Tools Access": "",
 	"Tools are a function calling system with arbitrary code execution": "",
+	"Tools Function Calling Prompt": "",
 	"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",
@@ -1041,6 +1051,7 @@
 	"Web Loader Settings": "Web تحميل اعدادات",
 	"Web Search": "بحث الويب",
 	"Web Search Engine": "محرك بحث الويب",
+	"Web Search in Chat": "",
 	"Web Search Query Generation": "",
 	"Webhook URL": "Webhook الرابط",
 	"WebUI Settings": "WebUI اعدادات",
@@ -1070,6 +1081,7 @@
 	"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 upload an empty file.": "",
 	"You do not have permission to access this feature.": "",
+	"You do not have permission to upload files": "",
 	"You do not have permission to upload files.": "",
 	"You have no archived conversations.": "لا تملك محادثات محفوظه",
 	"You have shared this chat": "تم مشاركة هذه المحادثة",

+ 18 - 6
src/lib/i18n/locales/bg-BG/translation.json

@@ -63,8 +63,11 @@
 	"Allowed Endpoints": "",
 	"Already have an account?": "Вече имате акаунт? ",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "",
+	"Always": "",
 	"Amazing": "",
 	"an assistant": "асистент",
+	"Analyzed": "",
+	"Analyzing...": "",
 	"and": "и",
 	"and {{COUNT}} more": "",
 	"and create a new shared link.": "и създай нов общ линк.",
@@ -169,9 +172,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "",
 	"Clone": "Клонинг",
 	"Clone Chat": "",
+	"Clone of {{TITLE}}": "",
 	"Close": "Затвори",
 	"Code execution": "",
 	"Code formatted successfully": "",
+	"Code Interpreter": "",
 	"Collection": "Колекция",
 	"Color": "",
 	"ComfyUI": "ComfyUI",
@@ -233,6 +238,7 @@
 	"Default": "По подразбиране",
 	"Default (Open AI)": "",
 	"Default (SentenceTransformers)": "По подразбиране (SentenceTransformers)",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
 	"Default Model": "Модел по подразбиране",
 	"Default model updated": "Моделът по подразбиране е обновен",
 	"Default Models": "",
@@ -343,6 +349,7 @@
 	"Enter Chunk Overlap": "Въведете Chunk Overlap",
 	"Enter Chunk Size": "Въведете Chunk Size",
 	"Enter description": "",
+	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Въведете URL адреса на Github Raw",
 	"Enter Google PSE API Key": "Въведете Google PSE API ключ",
 	"Enter Google PSE Engine Id": "Въведете идентификатор на двигателя на Google PSE",
@@ -391,12 +398,14 @@
 	"Error accessing Google Drive: {{error}}": "",
 	"Error uploading file: {{error}}": "",
 	"Evaluations": "",
+	"Exa API Key": "",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "",
 	"Example: ALL": "",
 	"Example: mail": "",
 	"Example: ou=users,dc=foo,dc=example": "",
 	"Example: sAMAccountName or uid or userPrincipalName": "",
 	"Exclude": "",
+	"Execute code for analysis": "",
 	"Experimental": "Експериментално",
 	"Explore the cosmos": "",
 	"Export": "Износ",
@@ -449,6 +458,7 @@
 	"Format your variables using brackets like this:": "",
 	"Frequency Penalty": "Наказание за честота",
 	"Function": "",
+	"Function Calling": "",
 	"Function created successfully": "",
 	"Function deleted successfully": "",
 	"Function Description": "",
@@ -463,6 +473,7 @@
 	"Functions imported successfully": "",
 	"General": "Основни",
 	"General Settings": "Основни Настройки",
+	"Generate an image": "",
 	"Generate Image": "",
 	"Generating search query": "Генериране на заявка за търсене",
 	"Get started": "",
@@ -478,7 +489,6 @@
 	"Group Name": "",
 	"Group updated successfully": "",
 	"Groups": "",
-	"h:mm a": "h:mm a",
 	"Haptic Feedback": "",
 	"has no conversations.": "няма разговори.",
 	"Hello, {{name}}": "Здравей, {{name}}",
@@ -496,7 +506,6 @@
 	"Ignite curiosity": "",
 	"Image": "",
 	"Image Compression": "",
-	"Image generation": "",
 	"Image Generation": "",
 	"Image Generation (Experimental)": "Генерация на изображения (Експериментално)",
 	"Image Generation Engine": "Двигател за генериране на изображения",
@@ -595,9 +604,6 @@
 	"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": "",
 	"Model": "",
 	"Model '{{modelName}}' has been successfully downloaded.": "Моделът '{{modelName}}' беше успешно свален.",
 	"Model '{{modelTag}}' is already in queue for downloading.": "Моделът '{{modelTag}}' е вече в очакване за сваляне.",
@@ -624,6 +630,7 @@
 	"More": "Повече",
 	"Name": "Име",
 	"Name your knowledge base": "",
+	"Native": "",
 	"New Chat": "Нов чат",
 	"New Folder": "",
 	"New Password": "Нова парола",
@@ -718,6 +725,7 @@
 	"Please enter a prompt": "",
 	"Please fill in all fields.": "",
 	"Please select a model first.": "",
+	"Please select a model.": "",
 	"Please select a reason": "",
 	"Port": "",
 	"Positive attitude": "Позитивна ативност",
@@ -726,6 +734,7 @@
 	"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": "Съдържание на промпта",
 	"Prompt created successfully": "",
@@ -801,7 +810,7 @@
 	"Search options": "",
 	"Search Prompts": "Търси Промптове",
 	"Search Result Count": "Брой резултати от търсенето",
-	"Search the web": "",
+	"Search the internet": "",
 	"Search Tools": "",
 	"SearchApi API Key": "",
 	"SearchApi Engine": "",
@@ -971,6 +980,7 @@
 	"Tools": "",
 	"Tools Access": "",
 	"Tools are a function calling system with arbitrary code execution": "",
+	"Tools Function Calling Prompt": "",
 	"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",
@@ -1041,6 +1051,7 @@
 	"Web Loader Settings": "Настройки за зареждане на уеб",
 	"Web Search": "Търсене в уеб",
 	"Web Search Engine": "Уеб търсачка",
+	"Web Search in Chat": "",
 	"Web Search Query Generation": "",
 	"Webhook URL": "Уебхук URL",
 	"WebUI Settings": "WebUI Настройки",
@@ -1070,6 +1081,7 @@
 	"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 upload an empty file.": "",
 	"You do not have permission to access this feature.": "",
+	"You do not have permission to upload files": "",
 	"You do not have permission to upload files.": "",
 	"You have no archived conversations.": "Нямате архивирани разговори.",
 	"You have shared this chat": "Вие сте споделели този чат",

+ 18 - 6
src/lib/i18n/locales/bn-BD/translation.json

@@ -63,8 +63,11 @@
 	"Allowed Endpoints": "",
 	"Already have an account?": "আগে থেকেই একাউন্ট আছে?",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "",
+	"Always": "",
 	"Amazing": "",
 	"an assistant": "একটা এসিস্ট্যান্ট",
+	"Analyzed": "",
+	"Analyzing...": "",
 	"and": "এবং",
 	"and {{COUNT}} more": "",
 	"and create a new shared link.": "এবং একটি নতুন শেয়ারে লিংক তৈরি করুন.",
@@ -169,9 +172,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "",
 	"Clone": "ক্লোন",
 	"Clone Chat": "",
+	"Clone of {{TITLE}}": "",
 	"Close": "বন্ধ",
 	"Code execution": "",
 	"Code formatted successfully": "",
+	"Code Interpreter": "",
 	"Collection": "সংগ্রহ",
 	"Color": "",
 	"ComfyUI": "ComfyUI",
@@ -233,6 +238,7 @@
 	"Default": "ডিফল্ট",
 	"Default (Open AI)": "",
 	"Default (SentenceTransformers)": "ডিফল্ট (SentenceTransformers)",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
 	"Default Model": "ডিফল্ট মডেল",
 	"Default model updated": "ডিফল্ট মডেল আপডেট হয়েছে",
 	"Default Models": "",
@@ -343,6 +349,7 @@
 	"Enter Chunk Overlap": "চাঙ্ক ওভারল্যাপ লিখুন",
 	"Enter Chunk Size": "চাংক সাইজ লিখুন",
 	"Enter description": "",
+	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "গিটহাব কাঁচা URL লিখুন",
 	"Enter Google PSE API Key": "গুগল পিএসই এপিআই কী লিখুন",
 	"Enter Google PSE Engine Id": "গুগল পিএসই ইঞ্জিন আইডি লিখুন",
@@ -391,12 +398,14 @@
 	"Error accessing Google Drive: {{error}}": "",
 	"Error uploading file: {{error}}": "",
 	"Evaluations": "",
+	"Exa API Key": "",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "",
 	"Example: ALL": "",
 	"Example: mail": "",
 	"Example: ou=users,dc=foo,dc=example": "",
 	"Example: sAMAccountName or uid or userPrincipalName": "",
 	"Exclude": "",
+	"Execute code for analysis": "",
 	"Experimental": "পরিক্ষামূলক",
 	"Explore the cosmos": "",
 	"Export": "রপ্তানি",
@@ -449,6 +458,7 @@
 	"Format your variables using brackets like this:": "",
 	"Frequency Penalty": "ফ্রিকোয়েন্সি পেনাল্টি",
 	"Function": "",
+	"Function Calling": "",
 	"Function created successfully": "",
 	"Function deleted successfully": "",
 	"Function Description": "",
@@ -463,6 +473,7 @@
 	"Functions imported successfully": "",
 	"General": "সাধারণ",
 	"General Settings": "সাধারণ সেটিংসমূহ",
+	"Generate an image": "",
 	"Generate Image": "",
 	"Generating search query": "অনুসন্ধান ক্যোয়ারী তৈরি করা হচ্ছে",
 	"Get started": "",
@@ -478,7 +489,6 @@
 	"Group Name": "",
 	"Group updated successfully": "",
 	"Groups": "",
-	"h:mm a": "h:mm a",
 	"Haptic Feedback": "",
 	"has no conversations.": "কোন কনভার্সেশন আছে না।",
 	"Hello, {{name}}": "হ্যালো, {{name}}",
@@ -496,7 +506,6 @@
 	"Ignite curiosity": "",
 	"Image": "",
 	"Image Compression": "",
-	"Image generation": "",
 	"Image Generation": "",
 	"Image Generation (Experimental)": "ইমেজ জেনারেশন (পরিক্ষামূলক)",
 	"Image Generation Engine": "ইমেজ জেনারেশন ইঞ্জিন",
@@ -595,9 +604,6 @@
 	"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": "",
 	"Model": "",
 	"Model '{{modelName}}' has been successfully downloaded.": "'{{modelName}}' মডেল সফলভাবে ডাউনলোড হয়েছে।",
 	"Model '{{modelTag}}' is already in queue for downloading.": "{{modelTag}} ডাউনলোডের জন্য আগে থেকেই অপেক্ষমান আছে।",
@@ -624,6 +630,7 @@
 	"More": "আরো",
 	"Name": "নাম",
 	"Name your knowledge base": "",
+	"Native": "",
 	"New Chat": "নতুন চ্যাট",
 	"New Folder": "",
 	"New Password": "নতুন পাসওয়ার্ড",
@@ -718,6 +725,7 @@
 	"Please enter a prompt": "",
 	"Please fill in all fields.": "",
 	"Please select a model first.": "",
+	"Please select a model.": "",
 	"Please select a reason": "",
 	"Port": "",
 	"Positive attitude": "পজিটিভ আক্রমণ",
@@ -726,6 +734,7 @@
 	"Previous 30 days": "পূর্ব ৩০ দিন",
 	"Previous 7 days": "পূর্ব ৭ দিন",
 	"Profile Image": "প্রোফাইল ইমেজ",
+	"Prompt": "",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "প্রম্প্ট (উদাহরণস্বরূপ, আমি রোমান ইমপার্টের সম্পর্কে একটি উপস্থিতি জানতে বল)",
 	"Prompt Content": "প্রম্পট কন্টেন্ট",
 	"Prompt created successfully": "",
@@ -801,7 +810,7 @@
 	"Search options": "",
 	"Search Prompts": "প্রম্পটসমূহ অনুসন্ধান করুন",
 	"Search Result Count": "অনুসন্ধানের ফলাফল গণনা",
-	"Search the web": "",
+	"Search the internet": "",
 	"Search Tools": "",
 	"SearchApi API Key": "",
 	"SearchApi Engine": "",
@@ -971,6 +980,7 @@
 	"Tools": "",
 	"Tools Access": "",
 	"Tools are a function calling system with arbitrary code execution": "",
+	"Tools Function Calling Prompt": "",
 	"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",
@@ -1041,6 +1051,7 @@
 	"Web Loader Settings": "ওয়েব লোডার সেটিংস",
 	"Web Search": "ওয়েব অনুসন্ধান",
 	"Web Search Engine": "ওয়েব সার্চ ইঞ্জিন",
+	"Web Search in Chat": "",
 	"Web Search Query Generation": "",
 	"Webhook URL": "ওয়েবহুক URL",
 	"WebUI Settings": "WebUI সেটিংসমূহ",
@@ -1070,6 +1081,7 @@
 	"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 upload an empty file.": "",
 	"You do not have permission to access this feature.": "",
+	"You do not have permission to upload files": "",
 	"You do not have permission to upload files.": "",
 	"You have no archived conversations.": "আপনার কোনও আর্কাইভ করা কথোপকথন নেই।",
 	"You have shared this chat": "আপনি এই চ্যাটটি শেয়ার করেছেন",

+ 18 - 6
src/lib/i18n/locales/ca-ES/translation.json

@@ -63,8 +63,11 @@
 	"Allowed Endpoints": "Punts d'accés permesos",
 	"Already have an account?": "Ja tens un compte?",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "Alternativa al top_p, i pretén garantir un equilibri de qualitat i varietat. El paràmetre p representa la probabilitat mínima que es consideri un token, en relació amb la probabilitat del token més probable. Per exemple, amb p=0,05 i el token més probable amb una probabilitat de 0,9, es filtren els logits amb un valor inferior a 0,045. (Per defecte: 0.0)",
+	"Always": "",
 	"Amazing": "Al·lucinant",
 	"an assistant": "un assistent",
+	"Analyzed": "",
+	"Analyzing...": "",
 	"and": "i",
 	"and {{COUNT}} more": "i {{COUNT}} més",
 	"and create a new shared link.": "i crear un nou enllaç compartit.",
@@ -169,9 +172,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Permís d'escriptura al porta-retalls denegat. Comprova els ajustos de navegador per donar l'accés necessari.",
 	"Clone": "Clonar",
 	"Clone Chat": "Clonar el xat",
+	"Clone of {{TITLE}}": "",
 	"Close": "Tancar",
 	"Code execution": "Execució de codi",
 	"Code formatted successfully": "Codi formatat correctament",
+	"Code Interpreter": "",
 	"Collection": "Col·lecció",
 	"Color": "Color",
 	"ComfyUI": "ComfyUI",
@@ -233,6 +238,7 @@
 	"Default": "Per defecte",
 	"Default (Open AI)": "Per defecte (Open AI)",
 	"Default (SentenceTransformers)": "Per defecte (SentenceTransformers)",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
 	"Default Model": "Model per defecte",
 	"Default model updated": "Model per defecte actualitzat",
 	"Default Models": "Models per defecte",
@@ -343,6 +349,7 @@
 	"Enter Chunk Overlap": "Introdueix la mida de solapament de blocs",
 	"Enter Chunk Size": "Introdueix la mida del bloc",
 	"Enter description": "Introdueix la descripció",
+	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Introdueix l'URL en brut de Github",
 	"Enter Google PSE API Key": "Introdueix la clau API de Google PSE",
 	"Enter Google PSE Engine Id": "Introdueix l'identificador del motor PSE de Google",
@@ -391,12 +398,14 @@
 	"Error accessing Google Drive: {{error}}": "Error en accedir a Google Drive: {{error}}",
 	"Error uploading file: {{error}}": "Error en pujar l'arxiu: {{error}}",
 	"Evaluations": "Avaluacions",
+	"Exa API Key": "",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "Exemple: (&(objectClass=inetOrgPerson)(uid=%s))",
 	"Example: ALL": "Exemple: TOTS",
 	"Example: mail": "Exemple: mail",
 	"Example: ou=users,dc=foo,dc=example": "Exemple: ou=users,dc=foo,dc=example",
 	"Example: sAMAccountName or uid or userPrincipalName": "Exemple: sAMAccountName o uid o userPrincipalName",
 	"Exclude": "Excloure",
+	"Execute code for analysis": "",
 	"Experimental": "Experimental",
 	"Explore the cosmos": "Explorar el cosmos",
 	"Export": "Exportar",
@@ -449,6 +458,7 @@
 	"Format your variables using brackets like this:": "Formata les teves variables utilitzant claudàtors així:",
 	"Frequency Penalty": "Penalització per freqüència",
 	"Function": "Funció",
+	"Function Calling": "",
 	"Function created successfully": "La funció s'ha creat correctament",
 	"Function deleted successfully": "La funció s'ha eliminat correctament",
 	"Function Description": "Descripció de la funció",
@@ -463,6 +473,7 @@
 	"Functions imported successfully": "Les funcions s'han importat correctament",
 	"General": "General",
 	"General Settings": "Preferències generals",
+	"Generate an image": "",
 	"Generate Image": "Generar imatge",
 	"Generating search query": "Generant consulta",
 	"Get started": "Començar",
@@ -478,7 +489,6 @@
 	"Group Name": "Nom del grup",
 	"Group updated successfully": "Grup actualitzat correctament",
 	"Groups": "Grups",
-	"h:mm a": "h:mm a",
 	"Haptic Feedback": "Retorn hàptic",
 	"has no conversations.": "no té converses.",
 	"Hello, {{name}}": "Hola, {{name}}",
@@ -496,7 +506,6 @@
 	"Ignite curiosity": "Despertar la curiositat",
 	"Image": "Imatge",
 	"Image Compression": "Compressió d'imatges",
-	"Image generation": "Generació d'imatges",
 	"Image Generation": "Generació d'imatges",
 	"Image Generation (Experimental)": "Generació d'imatges (Experimental)",
 	"Image Generation Engine": "Motor de generació d'imatges",
@@ -595,9 +604,6 @@
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Eta de Mirostat",
 	"Mirostat Tau": "Tau de Mirostat",
-	"MMMM DD, YYYY": "DD de MMMM, YYYY",
-	"MMMM DD, YYYY HH:mm": "DD de MMMM, YYYY HH:mm",
-	"MMMM DD, YYYY hh:mm:ss A": "DD de MMMM, YYYY HH:mm:ss, A",
 	"Model": "Model",
 	"Model '{{modelName}}' has been successfully downloaded.": "El model '{{modelName}}' s'ha descarregat correctament.",
 	"Model '{{modelTag}}' is already in queue for downloading.": "El model '{{modelTag}}' ja està en cua per ser descarregat.",
@@ -624,6 +630,7 @@
 	"More": "Més",
 	"Name": "Nom",
 	"Name your knowledge base": "Anomena la teva base de coneixement",
+	"Native": "",
 	"New Chat": "Nou xat",
 	"New Folder": "Nova carpeta",
 	"New Password": "Nova contrasenya",
@@ -718,6 +725,7 @@
 	"Please enter a prompt": "Si us plau, entra una indicació",
 	"Please fill in all fields.": "Emplena tots els camps, si us plau.",
 	"Please select a model first.": "Si us plau, selecciona un model primer",
+	"Please select a model.": "",
 	"Please select a reason": "Si us plau, selecciona una raó",
 	"Port": "Port",
 	"Positive attitude": "Actitud positiva",
@@ -726,6 +734,7 @@
 	"Previous 30 days": "30 dies anteriors",
 	"Previous 7 days": "7 dies anteriors",
 	"Profile Image": "Imatge de perfil",
+	"Prompt": "",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Indicació (p.ex. Digues-me quelcom divertit sobre l'Imperi Romà)",
 	"Prompt Content": "Contingut de la indicació",
 	"Prompt created successfully": "Indicació creada correctament",
@@ -801,7 +810,7 @@
 	"Search options": "Opcions de cerca",
 	"Search Prompts": "Cercar indicacions",
 	"Search Result Count": "Recompte de resultats de cerca",
-	"Search the web": "Cercar la web",
+	"Search the internet": "",
 	"Search Tools": "Cercar eines",
 	"SearchApi API Key": "Clau API de SearchApi",
 	"SearchApi Engine": "Motor de SearchApi",
@@ -971,6 +980,7 @@
 	"Tools": "Eines",
 	"Tools Access": "Accés a les eines",
 	"Tools are a function calling system with arbitrary code execution": "Les eines són un sistema de crida a funcions amb execució de codi arbitrari",
+	"Tools Function Calling Prompt": "",
 	"Tools have a function calling system that allows arbitrary code execution": "Les eines disposen d'un sistema de crida a funcions que permet execució de codi arbitrari",
 	"Tools have a function calling system that allows arbitrary code execution.": "Les eines disposen d'un sistema de crida a funcions que permet execució de codi arbitrari.",
 	"Top K": "Top K",
@@ -1041,6 +1051,7 @@
 	"Web Loader Settings": "Preferències del carregador web",
 	"Web Search": "Cerca la web",
 	"Web Search Engine": "Motor de cerca de la web",
+	"Web Search in Chat": "",
 	"Web Search Query Generation": "Generació de consultes per a la cerca de la web",
 	"Webhook URL": "URL del webhook",
 	"WebUI Settings": "Preferències de WebUI",
@@ -1070,6 +1081,7 @@
 	"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Pots personalitzar les teves interaccions amb els models de llenguatge afegint memòries mitjançant el botó 'Gestiona' que hi ha a continuació, fent-les més útils i adaptades a tu.",
 	"You cannot upload an empty file.": "No es pot pujar un ariux buit.",
 	"You do not have permission to access this feature.": "No tens permís per accedir a aquesta funcionalitat",
+	"You do not have permission to upload files": "",
 	"You do not have permission to upload files.": "No tens permisos per pujar arxius.",
 	"You have no archived conversations.": "No tens converses arxivades.",
 	"You have shared this chat": "Has compartit aquest xat",

+ 18 - 6
src/lib/i18n/locales/ceb-PH/translation.json

@@ -63,8 +63,11 @@
 	"Allowed Endpoints": "",
 	"Already have an account?": "Naa na kay account ?",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "",
+	"Always": "",
 	"Amazing": "",
 	"an assistant": "usa ka katabang",
+	"Analyzed": "",
+	"Analyzing...": "",
 	"and": "Ug",
 	"and {{COUNT}} more": "",
 	"and create a new shared link.": "",
@@ -169,9 +172,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "",
 	"Clone": "",
 	"Clone Chat": "",
+	"Clone of {{TITLE}}": "",
 	"Close": "Suod nga",
 	"Code execution": "",
 	"Code formatted successfully": "",
+	"Code Interpreter": "",
 	"Collection": "Koleksyon",
 	"Color": "",
 	"ComfyUI": "",
@@ -233,6 +238,7 @@
 	"Default": "Pinaagi sa default",
 	"Default (Open AI)": "",
 	"Default (SentenceTransformers)": "",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
 	"Default Model": "",
 	"Default model updated": "Gi-update nga default template",
 	"Default Models": "",
@@ -343,6 +349,7 @@
 	"Enter Chunk Overlap": "Pagsulod sa block overlap",
 	"Enter Chunk Size": "Isulod ang block size",
 	"Enter description": "",
+	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "",
 	"Enter Google PSE API Key": "",
 	"Enter Google PSE Engine Id": "",
@@ -391,12 +398,14 @@
 	"Error accessing Google Drive: {{error}}": "",
 	"Error uploading file: {{error}}": "",
 	"Evaluations": "",
+	"Exa API Key": "",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "",
 	"Example: ALL": "",
 	"Example: mail": "",
 	"Example: ou=users,dc=foo,dc=example": "",
 	"Example: sAMAccountName or uid or userPrincipalName": "",
 	"Exclude": "",
+	"Execute code for analysis": "",
 	"Experimental": "Eksperimento",
 	"Explore the cosmos": "",
 	"Export": "",
@@ -449,6 +458,7 @@
 	"Format your variables using brackets like this:": "",
 	"Frequency Penalty": "",
 	"Function": "",
+	"Function Calling": "",
 	"Function created successfully": "",
 	"Function deleted successfully": "",
 	"Function Description": "",
@@ -463,6 +473,7 @@
 	"Functions imported successfully": "",
 	"General": "Heneral",
 	"General Settings": "kinatibuk-ang mga setting",
+	"Generate an image": "",
 	"Generate Image": "",
 	"Generating search query": "",
 	"Get started": "",
@@ -478,7 +489,6 @@
 	"Group Name": "",
 	"Group updated successfully": "",
 	"Groups": "",
-	"h:mm a": "",
 	"Haptic Feedback": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "Maayong buntag, {{name}}",
@@ -496,7 +506,6 @@
 	"Ignite curiosity": "",
 	"Image": "",
 	"Image Compression": "",
-	"Image generation": "",
 	"Image Generation": "",
 	"Image Generation (Experimental)": "Pagmugna og hulagway (Eksperimento)",
 	"Image Generation Engine": "Makina sa paghimo og imahe",
@@ -595,9 +604,6 @@
 	"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:ss A": "",
 	"Model": "",
 	"Model '{{modelName}}' has been successfully downloaded.": "Ang modelo'{{modelName}}' malampuson nga na-download.",
 	"Model '{{modelTag}}' is already in queue for downloading.": "Ang modelo'{{modelTag}}' naa na sa pila para ma-download.",
@@ -624,6 +630,7 @@
 	"More": "",
 	"Name": "Ngalan",
 	"Name your knowledge base": "",
+	"Native": "",
 	"New Chat": "Bag-ong diskusyon",
 	"New Folder": "",
 	"New Password": "Bag-ong Password",
@@ -718,6 +725,7 @@
 	"Please enter a prompt": "",
 	"Please fill in all fields.": "",
 	"Please select a model first.": "",
+	"Please select a model.": "",
 	"Please select a reason": "",
 	"Port": "",
 	"Positive attitude": "",
@@ -726,6 +734,7 @@
 	"Previous 30 days": "",
 	"Previous 7 days": "",
 	"Profile Image": "",
+	"Prompt": "",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "",
 	"Prompt Content": "Ang sulod sa prompt",
 	"Prompt created successfully": "",
@@ -801,7 +810,7 @@
 	"Search options": "",
 	"Search Prompts": "Pangitaa ang mga prompt",
 	"Search Result Count": "",
-	"Search the web": "",
+	"Search the internet": "",
 	"Search Tools": "",
 	"SearchApi API Key": "",
 	"SearchApi Engine": "",
@@ -971,6 +980,7 @@
 	"Tools": "",
 	"Tools Access": "",
 	"Tools are a function calling system with arbitrary code execution": "",
+	"Tools Function Calling Prompt": "",
 	"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",
@@ -1041,6 +1051,7 @@
 	"Web Loader Settings": "",
 	"Web Search": "",
 	"Web Search Engine": "",
+	"Web Search in Chat": "",
 	"Web Search Query Generation": "",
 	"Webhook URL": "",
 	"WebUI Settings": "Mga Setting sa WebUI",
@@ -1070,6 +1081,7 @@
 	"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 upload an empty file.": "",
 	"You do not have permission to access this feature.": "",
+	"You do not have permission to upload files": "",
 	"You do not have permission to upload files.": "",
 	"You have no archived conversations.": "",
 	"You have shared this chat": "",

+ 18 - 6
src/lib/i18n/locales/cs-CZ/translation.json

@@ -63,8 +63,11 @@
 	"Allowed Endpoints": "",
 	"Already have an account?": "Už máte účet?",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "",
+	"Always": "",
 	"Amazing": "",
 	"an assistant": "asistent",
+	"Analyzed": "",
+	"Analyzing...": "",
 	"and": "a",
 	"and {{COUNT}} more": "a {{COUNT}} další/ch",
 	"and create a new shared link.": "a vytvořit nový sdílený odkaz.",
@@ -169,9 +172,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Přístup k zápisu do schránky byl zamítnut. Prosím, zkontrolujte nastavení svého prohlížeče a udělte potřebný přístup.",
 	"Clone": "Klonovat",
 	"Clone Chat": "",
+	"Clone of {{TITLE}}": "",
 	"Close": "Zavřít",
 	"Code execution": "Provádění kódu",
 	"Code formatted successfully": "Kód byl úspěšně naformátován.",
+	"Code Interpreter": "",
 	"Collection": "",
 	"Color": "Barva",
 	"ComfyUI": "ComfyUI.",
@@ -233,6 +238,7 @@
 	"Default": "Výchozí hodnoty nebo nastavení.",
 	"Default (Open AI)": "Výchozí (Open AI)",
 	"Default (SentenceTransformers)": "Výchozí (SentenceTransformers)",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
 	"Default Model": "Výchozí model",
 	"Default model updated": "Výchozí model aktualizován.",
 	"Default Models": "Výchozí modely",
@@ -343,6 +349,7 @@
 	"Enter Chunk Overlap": "Zadejte překryv části",
 	"Enter Chunk Size": "Zadejte velikost bloku",
 	"Enter description": "Zadejte popis",
+	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Zadejte URL adresu Github Raw",
 	"Enter Google PSE API Key": "Zadejte klíč rozhraní API Google PSE",
 	"Enter Google PSE Engine Id": "Zadejte ID vyhledávacího mechanismu Google PSE",
@@ -391,12 +398,14 @@
 	"Error accessing Google Drive: {{error}}": "",
 	"Error uploading file: {{error}}": "",
 	"Evaluations": "Hodnocení",
+	"Exa API Key": "",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "",
 	"Example: ALL": "",
 	"Example: mail": "",
 	"Example: ou=users,dc=foo,dc=example": "",
 	"Example: sAMAccountName or uid or userPrincipalName": "",
 	"Exclude": "Vyloučit",
+	"Execute code for analysis": "",
 	"Experimental": "Experimentální",
 	"Explore the cosmos": "",
 	"Export": "Exportovat",
@@ -449,6 +458,7 @@
 	"Format your variables using brackets like this:": "Formátujte své proměnné pomocí závorek takto:",
 	"Frequency Penalty": "Penalizace frekvence",
 	"Function": "Funkce",
+	"Function Calling": "",
 	"Function created successfully": "Funkce byla úspěšně vytvořena.",
 	"Function deleted successfully": "Funkce byla úspěšně odstraněna",
 	"Function Description": "",
@@ -463,6 +473,7 @@
 	"Functions imported successfully": "Funkce byly úspěšně importovány",
 	"General": "Obecný",
 	"General Settings": "Obecná nastavení",
+	"Generate an image": "",
 	"Generate Image": "Vygenerovat obrázek",
 	"Generating search query": "Generování vyhledávacího dotazu",
 	"Get started": "",
@@ -478,7 +489,6 @@
 	"Group Name": "",
 	"Group updated successfully": "",
 	"Groups": "",
-	"h:mm a": "hh:mm dop./odp.",
 	"Haptic Feedback": "Haptická zpětná vazba",
 	"has no conversations.": "nemá žádné konverzace.",
 	"Hello, {{name}}": "Ahoj, {{name}}",
@@ -496,7 +506,6 @@
 	"Ignite curiosity": "",
 	"Image": "",
 	"Image Compression": "",
-	"Image generation": "",
 	"Image Generation": "",
 	"Image Generation (Experimental)": "Generování obrázků (experimentální)",
 	"Image Generation Engine": "Engine pro generování obrázků",
@@ -595,9 +604,6 @@
 	"Mirostat": "Mirostat",
 	"Mirostat Eta": "Mirostat Eta",
 	"Mirostat Tau": "Mirostat Tau",
-	"MMMM DD, YYYY": "MMMM DD, RRRR",
-	"MMMM DD, YYYY HH:mm": "MMMM DD, RRRR HH:mm",
-	"MMMM DD, YYYY hh:mm:ss A": "MMMM DD, YYYY hh:mm:ss A",
 	"Model": "Model",
 	"Model '{{modelName}}' has been successfully downloaded.": "Model „{{modelName}}“ byl úspěšně stažen.",
 	"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' je již zařazen do fronty pro stahování.",
@@ -624,6 +630,7 @@
 	"More": "Více",
 	"Name": "Jméno",
 	"Name your knowledge base": "",
+	"Native": "",
 	"New Chat": "Nový chat",
 	"New Folder": "",
 	"New Password": "Nové heslo",
@@ -718,6 +725,7 @@
 	"Please enter a prompt": "Prosím, zadejte zadání.",
 	"Please fill in all fields.": "Prosím, vyplňte všechna pole.",
 	"Please select a model first.": "",
+	"Please select a model.": "",
 	"Please select a reason": "Prosím vyberte důvod",
 	"Port": "",
 	"Positive attitude": "Pozitivní přístup",
@@ -726,6 +734,7 @@
 	"Previous 30 days": "Předchozích 30 dnů",
 	"Previous 7 days": "Předchozích 7 dní",
 	"Profile Image": "Profilový obrázek",
+	"Prompt": "",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (např. Řekni mi zábavný fakt o Římské říši)",
 	"Prompt Content": "Obsah promptu",
 	"Prompt created successfully": "",
@@ -801,7 +810,7 @@
 	"Search options": "",
 	"Search Prompts": "Vyhledávací dotazy",
 	"Search Result Count": "Počet výsledků hledání",
-	"Search the web": "",
+	"Search the internet": "",
 	"Search Tools": "Nástroje pro vyhledávání",
 	"SearchApi API Key": "Klíč API pro SearchApi",
 	"SearchApi Engine": "Vyhledávací engine API",
@@ -971,6 +980,7 @@
 	"Tools": "Nástroje",
 	"Tools Access": "",
 	"Tools are a function calling system with arbitrary code execution": "Nástroje jsou systémem pro volání funkcí s vykonáváním libovolného kódu.",
+	"Tools Function Calling Prompt": "",
 	"Tools have a function calling system that allows arbitrary code execution": "Nástroje mají systém volání funkcí, který umožňuje libovolné spouštění kódu.",
 	"Tools have a function calling system that allows arbitrary code execution.": "Nástroje mají systém volání funkcí, který umožňuje spuštění libovolného kódu.",
 	"Top K": "Top K",
@@ -1041,6 +1051,7 @@
 	"Web Loader Settings": "Nastavení Web Loaderu",
 	"Web Search": "Vyhledávání na webu",
 	"Web Search Engine": "Webový vyhledávač",
+	"Web Search in Chat": "",
 	"Web Search Query Generation": "",
 	"Webhook URL": "Webhook URL",
 	"WebUI Settings": "Nastavení WebUI",
@@ -1070,6 +1081,7 @@
 	"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Můžete personalizovat své interakce s LLM pomocí přidávání vzpomínek prostřednictvím tlačítka 'Spravovat' níže, což je učiní pro vás užitečnějšími a lépe přizpůsobenými.",
 	"You cannot upload an empty file.": "Nemůžete nahrát prázdný soubor.",
 	"You do not have permission to access this feature.": "",
+	"You do not have permission to upload files": "",
 	"You do not have permission to upload files.": "",
 	"You have no archived conversations.": "Nemáte žádné archivované konverzace.",
 	"You have shared this chat": "Sdíleli jste tento chat.",

+ 18 - 6
src/lib/i18n/locales/da-DK/translation.json

@@ -63,8 +63,11 @@
 	"Allowed Endpoints": "",
 	"Already have an account?": "Har du allerede en profil?",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "",
+	"Always": "",
 	"Amazing": "",
 	"an assistant": "en assistent",
+	"Analyzed": "",
+	"Analyzing...": "",
 	"and": "og",
 	"and {{COUNT}} more": "",
 	"and create a new shared link.": "og lav et nyt link til deling",
@@ -169,9 +172,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Skriveadgang til udklipsholderen ikke tilladt. Tjek venligst indstillingerne i din browser for at give adgang.",
 	"Clone": "Klon",
 	"Clone Chat": "",
+	"Clone of {{TITLE}}": "",
 	"Close": "Luk",
 	"Code execution": "",
 	"Code formatted successfully": "Kode formateret korrekt",
+	"Code Interpreter": "",
 	"Collection": "Samling",
 	"Color": "",
 	"ComfyUI": "ComfyUI",
@@ -233,6 +238,7 @@
 	"Default": "Standard",
 	"Default (Open AI)": "Standard (Open AI)",
 	"Default (SentenceTransformers)": "Standard (SentenceTransformers)",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
 	"Default Model": "Standard model",
 	"Default model updated": "Standard model opdateret",
 	"Default Models": "",
@@ -343,6 +349,7 @@
 	"Enter Chunk Overlap": "Indtast overlapning af tekststykker",
 	"Enter Chunk Size": "Indtast størrelse af tekststykker",
 	"Enter description": "",
+	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Indtast Github Raw URL",
 	"Enter Google PSE API Key": "Indtast Google PSE API-nøgle",
 	"Enter Google PSE Engine Id": "Indtast Google PSE Engine ID",
@@ -391,12 +398,14 @@
 	"Error accessing Google Drive: {{error}}": "",
 	"Error uploading file: {{error}}": "",
 	"Evaluations": "",
+	"Exa API Key": "",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "",
 	"Example: ALL": "",
 	"Example: mail": "",
 	"Example: ou=users,dc=foo,dc=example": "",
 	"Example: sAMAccountName or uid or userPrincipalName": "",
 	"Exclude": "",
+	"Execute code for analysis": "",
 	"Experimental": "Eksperimentel",
 	"Explore the cosmos": "",
 	"Export": "Eksportér",
@@ -449,6 +458,7 @@
 	"Format your variables using brackets like this:": "",
 	"Frequency Penalty": "Hyppighedsstraf",
 	"Function": "",
+	"Function Calling": "",
 	"Function created successfully": "Funktion oprettet.",
 	"Function deleted successfully": "Funktion slettet.",
 	"Function Description": "",
@@ -463,6 +473,7 @@
 	"Functions imported successfully": "Funktioner importeret.",
 	"General": "Generelt",
 	"General Settings": "Generelle indstillinger",
+	"Generate an image": "",
 	"Generate Image": "Generer billede",
 	"Generating search query": "Genererer søgeforespørgsel",
 	"Get started": "",
@@ -478,7 +489,6 @@
 	"Group Name": "",
 	"Group updated successfully": "",
 	"Groups": "",
-	"h:mm a": "h:mm a",
 	"Haptic Feedback": "Haptisk feedback",
 	"has no conversations.": "har ingen samtaler.",
 	"Hello, {{name}}": "Hej {{name}}",
@@ -496,7 +506,6 @@
 	"Ignite curiosity": "",
 	"Image": "",
 	"Image Compression": "",
-	"Image generation": "",
 	"Image Generation": "",
 	"Image Generation (Experimental)": "Billedgenerering (eksperimentel)",
 	"Image Generation Engine": "Billedgenereringsengine",
@@ -595,9 +604,6 @@
 	"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": "",
 	"Model '{{modelName}}' has been successfully downloaded.": "Model '{{modelName}}' er blevet downloadet.",
 	"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' er allerede i kø til download.",
@@ -624,6 +630,7 @@
 	"More": "Mere",
 	"Name": "Navn",
 	"Name your knowledge base": "",
+	"Native": "",
 	"New Chat": "Ny chat",
 	"New Folder": "",
 	"New Password": "Ny adgangskode",
@@ -718,6 +725,7 @@
 	"Please enter a prompt": "",
 	"Please fill in all fields.": "Udfyld alle felter.",
 	"Please select a model first.": "",
+	"Please select a model.": "",
 	"Please select a reason": "Vælg en årsag",
 	"Port": "",
 	"Positive attitude": "Positiv holdning",
@@ -726,6 +734,7 @@
 	"Previous 30 days": "Seneste 30 dage",
 	"Previous 7 days": "Seneste 7 dage",
 	"Profile Image": "Profilbillede",
+	"Prompt": "",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (f.eks. Fortæl mig en sjov kendsgerning om Romerriget)",
 	"Prompt Content": "Promptindhold",
 	"Prompt created successfully": "",
@@ -801,7 +810,7 @@
 	"Search options": "",
 	"Search Prompts": "Søg i prompts",
 	"Search Result Count": "Antal søgeresultater",
-	"Search the web": "",
+	"Search the internet": "",
 	"Search Tools": "Søg i værktøjer",
 	"SearchApi API Key": "SearchApi API-nøgle",
 	"SearchApi Engine": "SearchApi-engine",
@@ -971,6 +980,7 @@
 	"Tools": "Værktøjer",
 	"Tools Access": "",
 	"Tools are a function calling system with arbitrary code execution": "Værktøjer er et funktionkaldssystem med vilkårlig kodeudførelse",
+	"Tools Function Calling Prompt": "",
 	"Tools have a function calling system that allows arbitrary code execution": "Værktøjer har et funktionkaldssystem, der tillader vilkårlig kodeudførelse",
 	"Tools have a function calling system that allows arbitrary code execution.": "Værktøjer har et funktionkaldssystem, der tillader vilkårlig kodeudførelse.",
 	"Top K": "Top K",
@@ -1041,6 +1051,7 @@
 	"Web Loader Settings": "Web Loader-indstillinger",
 	"Web Search": "Websøgning",
 	"Web Search Engine": "Websøgemaskine",
+	"Web Search in Chat": "",
 	"Web Search Query Generation": "",
 	"Webhook URL": "Webhook-URL",
 	"WebUI Settings": "WebUI-indstillinger",
@@ -1070,6 +1081,7 @@
 	"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Du kan personliggøre dine interaktioner med LLM'er ved at tilføje minder via knappen 'Administrer' nedenfor, hvilket gør dem mere nyttige og skræddersyet til dig.",
 	"You cannot upload an empty file.": "",
 	"You do not have permission to access this feature.": "",
+	"You do not have permission to upload files": "",
 	"You do not have permission to upload files.": "",
 	"You have no archived conversations.": "Du har ingen arkiverede samtaler.",
 	"You have shared this chat": "Du har delt denne chat",

+ 18 - 6
src/lib/i18n/locales/de-DE/translation.json

@@ -63,8 +63,11 @@
 	"Allowed Endpoints": "",
 	"Already have an account?": "Haben Sie bereits einen Account?",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "Alternative zu top_p und zielt darauf ab, ein Gleichgewicht zwischen Qualität und Vielfalt zu gewährleisten. Der Parameter p repräsentiert die Mindestwahrscheinlichkeit für ein Token, um berücksichtigt zu werden, relativ zur Wahrscheinlichkeit des wahrscheinlichsten Tokens. Zum Beispiel, bei p=0.05 und das wahrscheinlichste Token hat eine Wahrscheinlichkeit von 0.9, werden Logits mit einem Wert von weniger als 0.045 herausgefiltert. (Standard: 0.0)",
+	"Always": "",
 	"Amazing": "Fantastisch",
 	"an assistant": "ein Assistent",
+	"Analyzed": "",
+	"Analyzing...": "",
 	"and": "und",
 	"and {{COUNT}} more": "und {{COUNT}} mehr",
 	"and create a new shared link.": "und erstellen Sie einen neuen freigegebenen Link.",
@@ -169,9 +172,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Schreibberechtigung für die Zwischenablage verweigert. Bitte überprüfen Sie Ihre Browsereinstellungen, um den erforderlichen Zugriff zu erlauben.",
 	"Clone": "Klonen",
 	"Clone Chat": "",
+	"Clone of {{TITLE}}": "",
 	"Close": "Schließen",
 	"Code execution": "Codeausführung",
 	"Code formatted successfully": "Code erfolgreich formatiert",
+	"Code Interpreter": "",
 	"Collection": "Kollektion",
 	"Color": "Farbe",
 	"ComfyUI": "ComfyUI",
@@ -233,6 +238,7 @@
 	"Default": "Standard",
 	"Default (Open AI)": "Standard (Open AI)",
 	"Default (SentenceTransformers)": "Standard (SentenceTransformers)",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
 	"Default Model": "Standardmodell",
 	"Default model updated": "Standardmodell aktualisiert",
 	"Default Models": "",
@@ -343,6 +349,7 @@
 	"Enter Chunk Overlap": "Geben Sie die Blocküberlappung ein",
 	"Enter Chunk Size": "Geben Sie die Blockgröße ein",
 	"Enter description": "Geben Sie eine Beschreibung ein",
+	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Geben Sie die Github Raw-URL ein",
 	"Enter Google PSE API Key": "Geben Sie den Google PSE-API-Schlüssel ein",
 	"Enter Google PSE Engine Id": "Geben Sie die Google PSE-Engine-ID ein",
@@ -391,12 +398,14 @@
 	"Error accessing Google Drive: {{error}}": "",
 	"Error uploading file: {{error}}": "",
 	"Evaluations": "Evaluationen",
+	"Exa API Key": "",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "Beispiel: (&(objectClass=inetOrgPerson)(uid=%s))",
 	"Example: ALL": "Beispiel: ALL",
 	"Example: mail": "",
 	"Example: ou=users,dc=foo,dc=example": "Beispiel: ou=users,dc=foo,dc=example",
 	"Example: sAMAccountName or uid or userPrincipalName": "Beispiel: sAMAccountName or uid or userPrincipalName",
 	"Exclude": "Ausschließen",
+	"Execute code for analysis": "",
 	"Experimental": "Experimentell",
 	"Explore the cosmos": "Erforschen Sie das Universum",
 	"Export": "Exportieren",
@@ -449,6 +458,7 @@
 	"Format your variables using brackets like this:": "Formatieren Sie Ihre Variablen mit Klammern, wie hier:",
 	"Frequency Penalty": "Frequenzstrafe",
 	"Function": "Funktion",
+	"Function Calling": "",
 	"Function created successfully": "Funktion erfolgreich erstellt",
 	"Function deleted successfully": "Funktion erfolgreich gelöscht",
 	"Function Description": "Funktionsbeschreibung",
@@ -463,6 +473,7 @@
 	"Functions imported successfully": "Funktionen erfolgreich importiert",
 	"General": "Allgemein",
 	"General Settings": "Allgemeine Einstellungen",
+	"Generate an image": "",
 	"Generate Image": "Bild erzeugen",
 	"Generating search query": "Suchanfrage wird erstellt",
 	"Get started": "Loslegen",
@@ -478,7 +489,6 @@
 	"Group Name": "Gruppenname",
 	"Group updated successfully": "Gruppe erfolgreich aktualisiert",
 	"Groups": "Gruppen",
-	"h:mm a": "h:mm a",
 	"Haptic Feedback": "Haptisches Feedback",
 	"has no conversations.": "hat keine Unterhaltungen.",
 	"Hello, {{name}}": "Hallo, {{name}}",
@@ -496,7 +506,6 @@
 	"Ignite curiosity": "Neugier entfachen",
 	"Image": "",
 	"Image Compression": "",
-	"Image generation": "",
 	"Image Generation": "",
 	"Image Generation (Experimental)": "Bildgenerierung (experimentell)",
 	"Image Generation Engine": "Bildgenerierungs-Engine",
@@ -595,9 +604,6 @@
 	"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 A",
 	"Model": "Modell",
 	"Model '{{modelName}}' has been successfully downloaded.": "Modell '{{modelName}}' wurde erfolgreich heruntergeladen.",
 	"Model '{{modelTag}}' is already in queue for downloading.": "Modell '{{modelTag}}' befindet sich bereits in der Warteschlange zum Herunterladen.",
@@ -624,6 +630,7 @@
 	"More": "Mehr",
 	"Name": "Name",
 	"Name your knowledge base": "Benennen Sie Ihren Wissensspeicher",
+	"Native": "",
 	"New Chat": "Neue Unterhaltung",
 	"New Folder": "",
 	"New Password": "Neues Passwort",
@@ -718,6 +725,7 @@
 	"Please enter a prompt": "Bitte geben Sie einen Prompt ein",
 	"Please fill in all fields.": "Bitte füllen Sie alle Felder aus.",
 	"Please select a model first.": "",
+	"Please select a model.": "",
 	"Please select a reason": "Bitte wählen Sie einen Grund aus",
 	"Port": "Port",
 	"Positive attitude": "Positive Einstellung",
@@ -726,6 +734,7 @@
 	"Previous 30 days": "Vorherige 30 Tage",
 	"Previous 7 days": "Vorherige 7 Tage",
 	"Profile Image": "Profilbild",
+	"Prompt": "",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (z. B. \"Erzähle mir eine interessante Tatsache über das Römische Reich\")",
 	"Prompt Content": "Prompt-Inhalt",
 	"Prompt created successfully": "Prompt erfolgreich erstellt",
@@ -801,7 +810,7 @@
 	"Search options": "Suchoptionen",
 	"Search Prompts": "Prompts durchsuchen...",
 	"Search Result Count": "Anzahl der Suchergebnisse",
-	"Search the web": "Im Web suchen",
+	"Search the internet": "",
 	"Search Tools": "Werkzeuge durchsuchen...",
 	"SearchApi API Key": "SearchApi-API-Schlüssel",
 	"SearchApi Engine": "SearchApi-Engine",
@@ -971,6 +980,7 @@
 	"Tools": "Werkzeuge",
 	"Tools Access": "Werkzeugzugriff",
 	"Tools are a function calling system with arbitrary code execution": "Wekzeuge sind ein Funktionssystem mit beliebiger Codeausführung",
+	"Tools Function Calling Prompt": "",
 	"Tools have a function calling system that allows arbitrary code execution": "Werkezuge verfügen über ein Funktionssystem, das die Ausführung beliebigen Codes ermöglicht",
 	"Tools have a function calling system that allows arbitrary code execution.": "Werkzeuge verfügen über ein Funktionssystem, das die Ausführung beliebigen Codes ermöglicht.",
 	"Top K": "Top K",
@@ -1041,6 +1051,7 @@
 	"Web Loader Settings": "Web Loader Einstellungen",
 	"Web Search": "Websuche",
 	"Web Search Engine": "Suchmaschine",
+	"Web Search in Chat": "",
 	"Web Search Query Generation": "",
 	"Webhook URL": "Webhook URL",
 	"WebUI Settings": "WebUI-Einstellungen",
@@ -1070,6 +1081,7 @@
 	"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Personalisieren Sie Interaktionen mit LLMs, indem Sie über die Schaltfläche \"Verwalten\" Erinnerungen hinzufügen.",
 	"You cannot upload an empty file.": "Sie können keine leere Datei hochladen.",
 	"You do not have permission to access this feature.": "",
+	"You do not have permission to upload files": "",
 	"You do not have permission to upload files.": "Sie haben keine Berechtigung zum Hochladen von Dateien.",
 	"You have no archived conversations.": "Du hast keine archivierten Unterhaltungen.",
 	"You have shared this chat": "Sie haben diese Unterhaltung geteilt",

+ 18 - 6
src/lib/i18n/locales/dg-DG/translation.json

@@ -63,8 +63,11 @@
 	"Allowed Endpoints": "",
 	"Already have an account?": "Such account exists?",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "",
+	"Always": "",
 	"Amazing": "",
 	"an assistant": "such assistant",
+	"Analyzed": "",
+	"Analyzing...": "",
 	"and": "and",
 	"and {{COUNT}} more": "",
 	"and create a new shared link.": "",
@@ -169,9 +172,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "",
 	"Clone": "",
 	"Clone Chat": "",
+	"Clone of {{TITLE}}": "",
 	"Close": "Close",
 	"Code execution": "",
 	"Code formatted successfully": "",
+	"Code Interpreter": "",
 	"Collection": "Collection",
 	"Color": "",
 	"ComfyUI": "",
@@ -233,6 +238,7 @@
 	"Default": "Default",
 	"Default (Open AI)": "",
 	"Default (SentenceTransformers)": "",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
 	"Default Model": "",
 	"Default model updated": "Default model much updated",
 	"Default Models": "",
@@ -343,6 +349,7 @@
 	"Enter Chunk Overlap": "Enter Overlap of Chunks",
 	"Enter Chunk Size": "Enter Size of Chunk",
 	"Enter description": "",
+	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "",
 	"Enter Google PSE API Key": "",
 	"Enter Google PSE Engine Id": "",
@@ -391,12 +398,14 @@
 	"Error accessing Google Drive: {{error}}": "",
 	"Error uploading file: {{error}}": "",
 	"Evaluations": "",
+	"Exa API Key": "",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "",
 	"Example: ALL": "",
 	"Example: mail": "",
 	"Example: ou=users,dc=foo,dc=example": "",
 	"Example: sAMAccountName or uid or userPrincipalName": "",
 	"Exclude": "",
+	"Execute code for analysis": "",
 	"Experimental": "Much Experiment",
 	"Explore the cosmos": "",
 	"Export": "",
@@ -449,6 +458,7 @@
 	"Format your variables using brackets like this:": "",
 	"Frequency Penalty": "",
 	"Function": "",
+	"Function Calling": "",
 	"Function created successfully": "",
 	"Function deleted successfully": "",
 	"Function Description": "",
@@ -463,6 +473,7 @@
 	"Functions imported successfully": "",
 	"General": "Woweral",
 	"General Settings": "General Doge Settings",
+	"Generate an image": "",
 	"Generate Image": "",
 	"Generating search query": "",
 	"Get started": "",
@@ -478,7 +489,6 @@
 	"Group Name": "",
 	"Group updated successfully": "",
 	"Groups": "",
-	"h:mm a": "",
 	"Haptic Feedback": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "Much helo, {{name}}",
@@ -496,7 +506,6 @@
 	"Ignite curiosity": "",
 	"Image": "",
 	"Image Compression": "",
-	"Image generation": "",
 	"Image Generation": "",
 	"Image Generation (Experimental)": "Image Wow (Much Experiment)",
 	"Image Generation Engine": "Image Engine",
@@ -595,9 +604,6 @@
 	"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:ss A": "",
 	"Model": "",
 	"Model '{{modelName}}' has been successfully downloaded.": "Model '{{modelName}}' has been successfully downloaded.",
 	"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' is already in queue for downloading.",
@@ -624,6 +630,7 @@
 	"More": "",
 	"Name": "Name",
 	"Name your knowledge base": "",
+	"Native": "",
 	"New Chat": "New Bark",
 	"New Folder": "",
 	"New Password": "New Barkword",
@@ -718,6 +725,7 @@
 	"Please enter a prompt": "",
 	"Please fill in all fields.": "",
 	"Please select a model first.": "",
+	"Please select a model.": "",
 	"Please select a reason": "",
 	"Port": "",
 	"Positive attitude": "",
@@ -726,6 +734,7 @@
 	"Previous 30 days": "",
 	"Previous 7 days": "",
 	"Profile Image": "",
+	"Prompt": "",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "",
 	"Prompt Content": "Prompt Content",
 	"Prompt created successfully": "",
@@ -801,7 +810,7 @@
 	"Search options": "",
 	"Search Prompts": "Search Prompts much wow",
 	"Search Result Count": "",
-	"Search the web": "",
+	"Search the internet": "",
 	"Search Tools": "",
 	"SearchApi API Key": "",
 	"SearchApi Engine": "",
@@ -971,6 +980,7 @@
 	"Tools": "",
 	"Tools Access": "",
 	"Tools are a function calling system with arbitrary code execution": "",
+	"Tools Function Calling Prompt": "",
 	"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 very top",
@@ -1041,6 +1051,7 @@
 	"Web Loader Settings": "",
 	"Web Search": "",
 	"Web Search Engine": "",
+	"Web Search in Chat": "",
 	"Web Search Query Generation": "",
 	"Webhook URL": "",
 	"WebUI Settings": "WebUI Settings much settings",
@@ -1070,6 +1081,7 @@
 	"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 upload an empty file.": "",
 	"You do not have permission to access this feature.": "",
+	"You do not have permission to upload files": "",
 	"You do not have permission to upload files.": "",
 	"You have no archived conversations.": "",
 	"You have shared this chat": "",

+ 18 - 6
src/lib/i18n/locales/el-GR/translation.json

@@ -63,8 +63,11 @@
 	"Allowed Endpoints": "",
 	"Already have an account?": "Έχετε ήδη λογαριασμό;",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "Εναλλακτικό στο top_p, και στοχεύει στη διασφάλιση μιας ισορροπίας μεταξύ ποιότητας και ποικιλίας. Η παράμετρος p αντιπροσωπεύει την ελάχιστη πιθανότητα για ένα token να θεωρηθεί, σε σχέση με την πιθανότητα του πιο πιθανού token. Για παράδειγμα, με p=0.05 και το πιο πιθανό token να έχει πιθανότητα 0.9, τα logits με τιμή μικρότερη από 0.045 φιλτράρονται. (Προεπιλογή: 0.0)",
+	"Always": "",
 	"Amazing": "Καταπληκτικό",
 	"an assistant": "ένας βοηθός",
+	"Analyzed": "",
+	"Analyzing...": "",
 	"and": "και",
 	"and {{COUNT}} more": "και {{COUNT}} ακόμα",
 	"and create a new shared link.": "και δημιουργήστε έναν νέο κοινόχρηστο σύνδεσμο.",
@@ -169,9 +172,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Άρνηση δικαιώματος εγγραφής στο πρόχειρο. Παρακαλώ ελέγξτε τις ρυθμίσεις του περιηγητή σας για να δώσετε την απαραίτητη πρόσβαση.",
 	"Clone": "Κλώνος",
 	"Clone Chat": "",
+	"Clone of {{TITLE}}": "",
 	"Close": "Κλείσιμο",
 	"Code execution": "Εκτέλεση κώδικα",
 	"Code formatted successfully": "Ο κώδικας μορφοποιήθηκε επιτυχώς",
+	"Code Interpreter": "",
 	"Collection": "Συλλογή",
 	"Color": "Χρώμα",
 	"ComfyUI": "ComfyUI",
@@ -233,6 +238,7 @@
 	"Default": "Προεπιλογή",
 	"Default (Open AI)": "Προεπιλογή (Open AI)",
 	"Default (SentenceTransformers)": "Προεπιλογή (SentenceTransformers)",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
 	"Default Model": "Προεπιλεγμένο Μοντέλο",
 	"Default model updated": "Το προεπιλεγμένο μοντέλο ενημερώθηκε",
 	"Default Models": "Προεπιλεγμένα Μοντέλα",
@@ -343,6 +349,7 @@
 	"Enter Chunk Overlap": "Εισάγετε την Επικάλυψη Τμημάτων",
 	"Enter Chunk Size": "Εισάγετε το Μέγεθος Τμημάτων",
 	"Enter description": "Εισάγετε την περιγραφή",
+	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Εισάγετε το Github Raw URL",
 	"Enter Google PSE API Key": "Εισάγετε το Κλειδί API Google PSE",
 	"Enter Google PSE Engine Id": "Εισάγετε το Αναγνωριστικό Μηχανής Google PSE",
@@ -391,12 +398,14 @@
 	"Error accessing Google Drive: {{error}}": "",
 	"Error uploading file: {{error}}": "",
 	"Evaluations": "Αξιολογήσεις",
+	"Exa API Key": "",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "Παράδειγμα: (&(objectClass=inetOrgPerson)(uid=%s))",
 	"Example: ALL": "Παράδειγμα: ALL",
 	"Example: mail": "",
 	"Example: ou=users,dc=foo,dc=example": "Παράδειγμα: ou=users,dc=foo,dc=example",
 	"Example: sAMAccountName or uid or userPrincipalName": "Παράδειγμα: sAMAccountName ή uid ή userPrincipalName",
 	"Exclude": "Εξαίρεση",
+	"Execute code for analysis": "",
 	"Experimental": "Πειραματικό",
 	"Explore the cosmos": "Εξερευνήστε το σύμπαν",
 	"Export": "Εξαγωγή",
@@ -449,6 +458,7 @@
 	"Format your variables using brackets like this:": "Μορφοποιήστε τις μεταβλητές σας χρησιμοποιώντας αγκύλες όπως αυτό:",
 	"Frequency Penalty": "Ποινή Συχνότητας",
 	"Function": "Λειτουργία",
+	"Function Calling": "",
 	"Function created successfully": "Η λειτουργία δημιουργήθηκε με επιτυχία",
 	"Function deleted successfully": "Η λειτουργία διαγράφηκε με επιτυχία",
 	"Function Description": "Περιγραφή Λειτουργίας",
@@ -463,6 +473,7 @@
 	"Functions imported successfully": "Οι λειτουργίες εισήχθησαν με επιτυχία",
 	"General": "Γενικά",
 	"General Settings": "Γενικές Ρυθμίσεις",
+	"Generate an image": "",
 	"Generate Image": "Δημιουργία Εικόνας",
 	"Generating search query": "Γενιά αναζήτησης ερώτησης",
 	"Get started": "Ξεκινήστε",
@@ -478,7 +489,6 @@
 	"Group Name": "Όνομα Ομάδας",
 	"Group updated successfully": "Η ομάδα ενημερώθηκε με επιτυχία",
 	"Groups": "Ομάδες",
-	"h:mm a": "h:mm π.μ./μ.μ.",
 	"Haptic Feedback": "Ανατροφοδότηση Haptic",
 	"has no conversations.": "δεν έχει συνομιλίες.",
 	"Hello, {{name}}": "Γειά σου, {{name}}",
@@ -496,7 +506,6 @@
 	"Ignite curiosity": "Ξύπνημα της περιέργειας",
 	"Image": "",
 	"Image Compression": "",
-	"Image generation": "",
 	"Image Generation": "",
 	"Image Generation (Experimental)": "Δημιουργία Εικόνας (Πειραματικό)",
 	"Image Generation Engine": "Μηχανή Δημιουργίας Εικόνας",
@@ -595,9 +604,6 @@
 	"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": "Μοντέλο",
 	"Model '{{modelName}}' has been successfully downloaded.": "Το μοντέλο '{{modelName}}' κατεβάστηκε με επιτυχία.",
 	"Model '{{modelTag}}' is already in queue for downloading.": "Το μοντέλο '{{modelTag}}' βρίσκεται ήδη στην ουρά για λήψη.",
@@ -624,6 +630,7 @@
 	"More": "Περισσότερα",
 	"Name": "Όνομα",
 	"Name your knowledge base": "Ονομάστε τη βάση γνώσης σας",
+	"Native": "",
 	"New Chat": "Νέα Συνομιλία",
 	"New Folder": "",
 	"New Password": "Νέος Κωδικός",
@@ -718,6 +725,7 @@
 	"Please enter a prompt": "Παρακαλώ εισάγετε μια προτροπή",
 	"Please fill in all fields.": "Παρακαλώ συμπληρώστε όλα τα πεδία.",
 	"Please select a model first.": "",
+	"Please select a model.": "",
 	"Please select a reason": "Παρακαλώ επιλέξτε έναν λόγο",
 	"Port": "Θύρα",
 	"Positive attitude": "Θετική στάση",
@@ -726,6 +734,7 @@
 	"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": "Περιεχόμενο Προτροπής",
 	"Prompt created successfully": "Η προτροπή δημιουργήθηκε με επιτυχία",
@@ -801,7 +810,7 @@
 	"Search options": "Επιλογές Αναζήτησης",
 	"Search Prompts": "Αναζήτηση Προτροπών",
 	"Search Result Count": "Αριθμός Αποτελεσμάτων Αναζήτησης",
-	"Search the web": "Αναζήτηση στο διαδίκτυο",
+	"Search the internet": "",
 	"Search Tools": "Αναζήτηση Εργαλείων",
 	"SearchApi API Key": "Κλειδί API SearchApi",
 	"SearchApi Engine": "Μηχανή SearchApi",
@@ -971,6 +980,7 @@
 	"Tools": "Εργαλεία",
 	"Tools Access": "Πρόσβαση Εργαλείων",
 	"Tools are a function calling system with arbitrary code execution": "Τα εργαλεία είναι ένα σύστημα κλήσης λειτουργιών με αυθαίρετη εκτέλεση κώδικα",
+	"Tools Function Calling Prompt": "",
 	"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",
@@ -1041,6 +1051,7 @@
 	"Web Loader Settings": "Ρυθμίσεις Φόρτωσης Web",
 	"Web Search": "Αναζήτηση στο Διαδίκτυο",
 	"Web Search Engine": "Μηχανή Αναζήτησης στο Διαδίκτυο",
+	"Web Search in Chat": "",
 	"Web Search Query Generation": "",
 	"Webhook URL": "URL Webhook",
 	"WebUI Settings": "Ρυθμίσεις WebUI",
@@ -1070,6 +1081,7 @@
 	"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Μπορείτε να προσωποποιήσετε τις αλληλεπιδράσεις σας με τα LLMs προσθέτοντας αναμνήσεις μέσω του κουμπιού 'Διαχείριση' παρακάτω, κάνοντάς τα πιο χρήσιμα και προσαρμοσμένα σε εσάς.",
 	"You cannot upload an empty file.": "Δεν μπορείτε να ανεβάσετε ένα κενό αρχείο.",
 	"You do not have permission to access this feature.": "",
+	"You do not have permission to upload files": "",
 	"You do not have permission to upload files.": "Δεν έχετε άδεια να ανεβάσετε αρχεία.",
 	"You have no archived conversations.": "Δεν έχετε αρχειοθετημένες συνομιλίες.",
 	"You have shared this chat": "Έχετε μοιραστεί αυτή τη συνομιλία",

+ 18 - 6
src/lib/i18n/locales/en-GB/translation.json

@@ -63,8 +63,11 @@
 	"Allowed Endpoints": "",
 	"Already have an account?": "",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "",
+	"Always": "",
 	"Amazing": "",
 	"an assistant": "",
+	"Analyzed": "",
+	"Analyzing...": "",
 	"and": "",
 	"and {{COUNT}} more": "",
 	"and create a new shared link.": "",
@@ -169,9 +172,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "",
 	"Clone": "",
 	"Clone Chat": "",
+	"Clone of {{TITLE}}": "",
 	"Close": "",
 	"Code execution": "",
 	"Code formatted successfully": "",
+	"Code Interpreter": "",
 	"Collection": "",
 	"Color": "",
 	"ComfyUI": "",
@@ -233,6 +238,7 @@
 	"Default": "",
 	"Default (Open AI)": "",
 	"Default (SentenceTransformers)": "",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
 	"Default Model": "",
 	"Default model updated": "",
 	"Default Models": "",
@@ -343,6 +349,7 @@
 	"Enter Chunk Overlap": "",
 	"Enter Chunk Size": "",
 	"Enter description": "",
+	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "",
 	"Enter Google PSE API Key": "",
 	"Enter Google PSE Engine Id": "",
@@ -391,12 +398,14 @@
 	"Error accessing Google Drive: {{error}}": "",
 	"Error uploading file: {{error}}": "",
 	"Evaluations": "",
+	"Exa API Key": "",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "",
 	"Example: ALL": "",
 	"Example: mail": "",
 	"Example: ou=users,dc=foo,dc=example": "",
 	"Example: sAMAccountName or uid or userPrincipalName": "",
 	"Exclude": "",
+	"Execute code for analysis": "",
 	"Experimental": "",
 	"Explore the cosmos": "",
 	"Export": "",
@@ -449,6 +458,7 @@
 	"Format your variables using brackets like this:": "",
 	"Frequency Penalty": "",
 	"Function": "",
+	"Function Calling": "",
 	"Function created successfully": "",
 	"Function deleted successfully": "",
 	"Function Description": "",
@@ -463,6 +473,7 @@
 	"Functions imported successfully": "",
 	"General": "",
 	"General Settings": "",
+	"Generate an image": "",
 	"Generate Image": "",
 	"Generating search query": "",
 	"Get started": "",
@@ -478,7 +489,6 @@
 	"Group Name": "",
 	"Group updated successfully": "",
 	"Groups": "",
-	"h:mm a": "",
 	"Haptic Feedback": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "",
@@ -496,7 +506,6 @@
 	"Ignite curiosity": "",
 	"Image": "",
 	"Image Compression": "",
-	"Image generation": "",
 	"Image Generation": "",
 	"Image Generation (Experimental)": "",
 	"Image Generation Engine": "",
@@ -595,9 +604,6 @@
 	"Mirostat": "",
 	"Mirostat Eta": "",
 	"Mirostat Tau": "",
-	"MMMM DD, YYYY": "",
-	"MMMM DD, YYYY HH:mm": "",
-	"MMMM DD, YYYY hh:mm:ss A": "",
 	"Model": "",
 	"Model '{{modelName}}' has been successfully downloaded.": "",
 	"Model '{{modelTag}}' is already in queue for downloading.": "",
@@ -624,6 +630,7 @@
 	"More": "",
 	"Name": "",
 	"Name your knowledge base": "",
+	"Native": "",
 	"New Chat": "",
 	"New Folder": "",
 	"New Password": "",
@@ -718,6 +725,7 @@
 	"Please enter a prompt": "",
 	"Please fill in all fields.": "",
 	"Please select a model first.": "",
+	"Please select a model.": "",
 	"Please select a reason": "",
 	"Port": "",
 	"Positive attitude": "",
@@ -726,6 +734,7 @@
 	"Previous 30 days": "",
 	"Previous 7 days": "",
 	"Profile Image": "",
+	"Prompt": "",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "",
 	"Prompt Content": "",
 	"Prompt created successfully": "",
@@ -801,7 +810,7 @@
 	"Search options": "",
 	"Search Prompts": "",
 	"Search Result Count": "",
-	"Search the web": "",
+	"Search the internet": "",
 	"Search Tools": "",
 	"SearchApi API Key": "",
 	"SearchApi Engine": "",
@@ -971,6 +980,7 @@
 	"Tools": "",
 	"Tools Access": "",
 	"Tools are a function calling system with arbitrary code execution": "",
+	"Tools Function Calling Prompt": "",
 	"Tools have a function calling system that allows arbitrary code execution": "",
 	"Tools have a function calling system that allows arbitrary code execution.": "",
 	"Top K": "",
@@ -1041,6 +1051,7 @@
 	"Web Loader Settings": "",
 	"Web Search": "",
 	"Web Search Engine": "",
+	"Web Search in Chat": "",
 	"Web Search Query Generation": "",
 	"Webhook URL": "",
 	"WebUI Settings": "",
@@ -1070,6 +1081,7 @@
 	"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 upload an empty file.": "",
 	"You do not have permission to access this feature.": "",
+	"You do not have permission to upload files": "",
 	"You do not have permission to upload files.": "",
 	"You have no archived conversations.": "",
 	"You have shared this chat": "",

+ 18 - 6
src/lib/i18n/locales/en-US/translation.json

@@ -63,8 +63,11 @@
 	"Allowed Endpoints": "",
 	"Already have an account?": "",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "",
+	"Always": "",
 	"Amazing": "",
 	"an assistant": "",
+	"Analyzed": "",
+	"Analyzing...": "",
 	"and": "",
 	"and {{COUNT}} more": "",
 	"and create a new shared link.": "",
@@ -169,9 +172,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "",
 	"Clone": "",
 	"Clone Chat": "",
+	"Clone of {{TITLE}}": "",
 	"Close": "",
 	"Code execution": "",
 	"Code formatted successfully": "",
+	"Code Interpreter": "",
 	"Collection": "",
 	"Color": "",
 	"ComfyUI": "",
@@ -233,6 +238,7 @@
 	"Default": "",
 	"Default (Open AI)": "",
 	"Default (SentenceTransformers)": "",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
 	"Default Model": "",
 	"Default model updated": "",
 	"Default Models": "",
@@ -343,6 +349,7 @@
 	"Enter Chunk Overlap": "",
 	"Enter Chunk Size": "",
 	"Enter description": "",
+	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "",
 	"Enter Google PSE API Key": "",
 	"Enter Google PSE Engine Id": "",
@@ -391,12 +398,14 @@
 	"Error accessing Google Drive: {{error}}": "",
 	"Error uploading file: {{error}}": "",
 	"Evaluations": "",
+	"Exa API Key": "",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "",
 	"Example: ALL": "",
 	"Example: mail": "",
 	"Example: ou=users,dc=foo,dc=example": "",
 	"Example: sAMAccountName or uid or userPrincipalName": "",
 	"Exclude": "",
+	"Execute code for analysis": "",
 	"Experimental": "",
 	"Explore the cosmos": "",
 	"Export": "",
@@ -449,6 +458,7 @@
 	"Format your variables using brackets like this:": "",
 	"Frequency Penalty": "",
 	"Function": "",
+	"Function Calling": "",
 	"Function created successfully": "",
 	"Function deleted successfully": "",
 	"Function Description": "",
@@ -463,6 +473,7 @@
 	"Functions imported successfully": "",
 	"General": "",
 	"General Settings": "",
+	"Generate an image": "",
 	"Generate Image": "",
 	"Generating search query": "",
 	"Get started": "",
@@ -478,7 +489,6 @@
 	"Group Name": "",
 	"Group updated successfully": "",
 	"Groups": "",
-	"h:mm a": "",
 	"Haptic Feedback": "",
 	"has no conversations.": "",
 	"Hello, {{name}}": "",
@@ -496,7 +506,6 @@
 	"Ignite curiosity": "",
 	"Image": "",
 	"Image Compression": "",
-	"Image generation": "",
 	"Image Generation": "",
 	"Image Generation (Experimental)": "",
 	"Image Generation Engine": "",
@@ -595,9 +604,6 @@
 	"Mirostat": "",
 	"Mirostat Eta": "",
 	"Mirostat Tau": "",
-	"MMMM DD, YYYY": "",
-	"MMMM DD, YYYY HH:mm": "",
-	"MMMM DD, YYYY hh:mm:ss A": "",
 	"Model": "",
 	"Model '{{modelName}}' has been successfully downloaded.": "",
 	"Model '{{modelTag}}' is already in queue for downloading.": "",
@@ -624,6 +630,7 @@
 	"More": "",
 	"Name": "",
 	"Name your knowledge base": "",
+	"Native": "",
 	"New Chat": "",
 	"New Folder": "",
 	"New Password": "",
@@ -718,6 +725,7 @@
 	"Please enter a prompt": "",
 	"Please fill in all fields.": "",
 	"Please select a model first.": "",
+	"Please select a model.": "",
 	"Please select a reason": "",
 	"Port": "",
 	"Positive attitude": "",
@@ -726,6 +734,7 @@
 	"Previous 30 days": "",
 	"Previous 7 days": "",
 	"Profile Image": "",
+	"Prompt": "",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "",
 	"Prompt Content": "",
 	"Prompt created successfully": "",
@@ -801,7 +810,7 @@
 	"Search options": "",
 	"Search Prompts": "",
 	"Search Result Count": "",
-	"Search the web": "",
+	"Search the internet": "",
 	"Search Tools": "",
 	"SearchApi API Key": "",
 	"SearchApi Engine": "",
@@ -971,6 +980,7 @@
 	"Tools": "",
 	"Tools Access": "",
 	"Tools are a function calling system with arbitrary code execution": "",
+	"Tools Function Calling Prompt": "",
 	"Tools have a function calling system that allows arbitrary code execution": "",
 	"Tools have a function calling system that allows arbitrary code execution.": "",
 	"Top K": "",
@@ -1041,6 +1051,7 @@
 	"Web Loader Settings": "",
 	"Web Search": "",
 	"Web Search Engine": "",
+	"Web Search in Chat": "",
 	"Web Search Query Generation": "",
 	"Webhook URL": "",
 	"WebUI Settings": "",
@@ -1070,6 +1081,7 @@
 	"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 upload an empty file.": "",
 	"You do not have permission to access this feature.": "",
+	"You do not have permission to upload files": "",
 	"You do not have permission to upload files.": "",
 	"You have no archived conversations.": "",
 	"You have shared this chat": "",

Vissa filer visades inte eftersom för många filer har ändrats