Browse Source

Merge remote-tracking branch 'upstream/dev' into feat/oauth

Jun Siang Cheah 10 tháng trước cách đây
mục cha
commit
4aab460905
100 tập tin đã thay đổi với 4977 bổ sung1110 xóa
  1. 54 2
      .github/workflows/docker-build.yaml
  2. 33 15
      .github/workflows/integration-test.yml
  3. 24 0
      CHANGELOG.md
  4. 1 1
      README.md
  5. 20 0
      backend/apps/images/main.py
  6. 16 0
      backend/apps/images/utils/comfyui.py
  7. 48 167
      backend/apps/ollama/main.py
  8. 26 9
      backend/apps/openai/main.py
  9. 70 1
      backend/apps/rag/main.py
  10. 8 3
      backend/apps/rag/search/brave.py
  11. 7 4
      backend/apps/rag/search/duckduckgo.py
  12. 9 3
      backend/apps/rag/search/google_pse.py
  13. 12 1
      backend/apps/rag/search/main.py
  14. 9 3
      backend/apps/rag/search/searxng.py
  15. 7 3
      backend/apps/rag/search/serper.py
  16. 5 3
      backend/apps/rag/search/serply.py
  17. 9 3
      backend/apps/rag/search/serpstack.py
  18. 10 10
      backend/apps/rag/utils.py
  19. 21 6
      backend/apps/webui/internal/db.py
  20. 48 0
      backend/apps/webui/internal/migrations/013_add_user_info.py
  21. 55 0
      backend/apps/webui/internal/migrations/014_add_files.py
  22. 61 0
      backend/apps/webui/internal/migrations/015_add_functions.py
  23. 72 0
      backend/apps/webui/internal/wrappers.py
  24. 69 3
      backend/apps/webui/main.py
  25. 112 0
      backend/apps/webui/models/files.py
  26. 141 0
      backend/apps/webui/models/functions.py
  27. 2 0
      backend/apps/webui/models/users.py
  28. 39 4
      backend/apps/webui/routers/auths.py
  29. 219 0
      backend/apps/webui/routers/files.py
  30. 180 0
      backend/apps/webui/routers/functions.py
  31. 5 1
      backend/apps/webui/routers/tools.py
  32. 46 0
      backend/apps/webui/routers/users.py
  33. 23 1
      backend/apps/webui/utils.py
  34. 53 1
      backend/config.py
  35. 512 186
      backend/main.py
  36. 35 1
      backend/utils/misc.py
  37. 17 7
      backend/utils/task.py
  38. 18 5
      backend/utils/utils.py
  39. 40 40
      package-lock.json
  40. 5 3
      package.json
  41. 57 11
      scripts/prepare-pyodide.js
  42. 6 0
      src/app.html
  43. 4 1
      src/lib/apis/auths/index.ts
  44. 183 0
      src/lib/apis/files/index.ts
  45. 193 0
      src/lib/apis/functions/index.ts
  46. 31 0
      src/lib/apis/rag/index.ts
  47. 70 0
      src/lib/apis/users/index.ts
  48. 6 6
      src/lib/components/admin/AddUserModal.svelte
  49. 3 2
      src/lib/components/admin/Settings/Documents.svelte
  50. 59 5
      src/lib/components/admin/UserChatsModal.svelte
  51. 71 41
      src/lib/components/chat/Chat.svelte
  52. 81 66
      src/lib/components/chat/MessageInput.svelte
  53. 120 65
      src/lib/components/chat/MessageInput/CallOverlay.svelte
  54. 6 6
      src/lib/components/chat/MessageInput/Documents.svelte
  55. 8 6
      src/lib/components/chat/MessageInput/Models.svelte
  56. 9 7
      src/lib/components/chat/MessageInput/PromptCommands.svelte
  57. 1 1
      src/lib/components/chat/MessageInput/Suggestions.svelte
  58. 37 24
      src/lib/components/chat/Messages.svelte
  59. 11 1
      src/lib/components/chat/Messages/CodeBlock.svelte
  60. 18 8
      src/lib/components/chat/Messages/Placeholder.svelte
  61. 4 2
      src/lib/components/chat/Messages/ProfileImage.svelte
  62. 76 61
      src/lib/components/chat/Messages/ResponseMessage.svelte
  63. 37 0
      src/lib/components/chat/Messages/UserMessage.svelte
  64. 1 0
      src/lib/components/chat/ModelSelector/Selector.svelte
  65. 2 1
      src/lib/components/chat/Settings/About.svelte
  66. 188 80
      src/lib/components/chat/Settings/Interface.svelte
  67. 29 29
      src/lib/components/chat/SettingsModal.svelte
  68. 6 5
      src/lib/components/common/ConfirmDialog.svelte
  69. 8 8
      src/lib/components/common/Modal.svelte
  70. 1 1
      src/lib/components/layout/Help.svelte
  71. 1 1
      src/lib/components/layout/Navbar.svelte
  72. 27 7
      src/lib/components/layout/Sidebar.svelte
  73. 7 7
      src/lib/components/layout/Sidebar/ChatItem.svelte
  74. 26 0
      src/lib/components/layout/Sidebar/UserMenu.svelte
  75. 377 0
      src/lib/components/workspace/Functions.svelte
  76. 381 0
      src/lib/components/workspace/Functions/FunctionEditor.svelte
  77. 60 0
      src/lib/components/workspace/Models/FiltersSelector.svelte
  78. 5 1
      src/lib/components/workspace/Models/ModelMenu.svelte
  79. 2 2
      src/lib/components/workspace/Models/ToolsSelector.svelte
  80. 14 4
      src/lib/components/workspace/Tools.svelte
  81. 0 127
      src/lib/components/workspace/Tools/CodeEditor.svelte
  82. 104 2
      src/lib/components/workspace/Tools/ToolkitEditor.svelte
  83. 28 2
      src/lib/i18n/locales/ar-BH/translation.json
  84. 28 2
      src/lib/i18n/locales/bg-BG/translation.json
  85. 28 2
      src/lib/i18n/locales/bn-BD/translation.json
  86. 28 2
      src/lib/i18n/locales/ca-ES/translation.json
  87. 28 2
      src/lib/i18n/locales/ceb-PH/translation.json
  88. 28 2
      src/lib/i18n/locales/de-DE/translation.json
  89. 30 2
      src/lib/i18n/locales/dg-DG/translation.json
  90. 28 2
      src/lib/i18n/locales/en-GB/translation.json
  91. 28 2
      src/lib/i18n/locales/en-US/translation.json
  92. 28 2
      src/lib/i18n/locales/es-ES/translation.json
  93. 28 2
      src/lib/i18n/locales/fa-IR/translation.json
  94. 28 2
      src/lib/i18n/locales/fi-FI/translation.json
  95. 28 2
      src/lib/i18n/locales/fr-CA/translation.json
  96. 28 2
      src/lib/i18n/locales/fr-FR/translation.json
  97. 28 2
      src/lib/i18n/locales/he-IL/translation.json
  98. 28 2
      src/lib/i18n/locales/hi-IN/translation.json
  99. 28 2
      src/lib/i18n/locales/hr-HR/translation.json
  100. 28 2
      src/lib/i18n/locales/it-IT/translation.json

+ 54 - 2
.github/workflows/docker-build.yaml

@@ -11,8 +11,6 @@ on:
 
 env:
   REGISTRY: ghcr.io
-  IMAGE_NAME: ${{ github.repository }}
-  FULL_IMAGE_NAME: ghcr.io/${{ github.repository }}
 
 jobs:
   build-main-image:
@@ -28,6 +26,15 @@ jobs:
           - linux/arm64
 
     steps:
+      # GitHub Packages requires the entire repository name to be in lowercase
+      # although the repository owner has a lowercase username, this prevents some people from running actions after forking
+      - name: Set repository and image name to lowercase
+        run: |
+          echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
+          echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
+        env:
+          IMAGE_NAME: '${{ github.repository }}'
+
       - name: Prepare
         run: |
           platform=${{ matrix.platform }}
@@ -116,6 +123,15 @@ jobs:
           - linux/arm64
 
     steps:
+      # GitHub Packages requires the entire repository name to be in lowercase
+      # although the repository owner has a lowercase username, this prevents some people from running actions after forking
+      - name: Set repository and image name to lowercase
+        run: |
+          echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
+          echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
+        env:
+          IMAGE_NAME: '${{ github.repository }}'
+
       - name: Prepare
         run: |
           platform=${{ matrix.platform }}
@@ -207,6 +223,15 @@ jobs:
           - linux/arm64
 
     steps:
+      # GitHub Packages requires the entire repository name to be in lowercase
+      # although the repository owner has a lowercase username, this prevents some people from running actions after forking
+      - name: Set repository and image name to lowercase
+        run: |
+          echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
+          echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
+        env:
+          IMAGE_NAME: '${{ github.repository }}'
+
       - name: Prepare
         run: |
           platform=${{ matrix.platform }}
@@ -289,6 +314,15 @@ jobs:
     runs-on: ubuntu-latest
     needs: [ build-main-image ]
     steps:
+      # GitHub Packages requires the entire repository name to be in lowercase
+      # although the repository owner has a lowercase username, this prevents some people from running actions after forking
+      - name: Set repository and image name to lowercase
+        run: |
+          echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
+          echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
+        env:
+          IMAGE_NAME: '${{ github.repository }}'
+
       - name: Download digests
         uses: actions/download-artifact@v4
         with:
@@ -335,6 +369,15 @@ jobs:
     runs-on: ubuntu-latest
     needs: [ build-cuda-image ]
     steps:
+      # GitHub Packages requires the entire repository name to be in lowercase
+      # although the repository owner has a lowercase username, this prevents some people from running actions after forking
+      - name: Set repository and image name to lowercase
+        run: |
+          echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
+          echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
+        env:
+          IMAGE_NAME: '${{ github.repository }}'
+
       - name: Download digests
         uses: actions/download-artifact@v4
         with:
@@ -382,6 +425,15 @@ jobs:
     runs-on: ubuntu-latest
     needs: [ build-ollama-image ]
     steps:
+      # GitHub Packages requires the entire repository name to be in lowercase
+      # although the repository owner has a lowercase username, this prevents some people from running actions after forking
+      - name: Set repository and image name to lowercase
+        run: |
+          echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
+          echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
+        env:
+          IMAGE_NAME: '${{ github.repository }}'
+
       - name: Download digests
         uses: actions/download-artifact@v4
         with:

+ 33 - 15
.github/workflows/integration-test.yml

@@ -25,7 +25,7 @@ jobs:
             --file docker-compose.api.yaml \
             --file docker-compose.a1111-test.yaml \
             up --detach --build
-          
+
       - name: Wait for Ollama to be up
         timeout-minutes: 5
         run: |
@@ -43,7 +43,7 @@ jobs:
         uses: cypress-io/github-action@v6
         with:
           browser: chrome
-          wait-on: 'http://localhost:3000'
+          wait-on: "http://localhost:3000"
           config: baseUrl=http://localhost:3000
 
       - uses: actions/upload-artifact@v4
@@ -82,18 +82,18 @@ jobs:
           --health-retries 5
         ports:
           - 5432:5432
-#      mysql:
-#        image: mysql
-#        env:
-#          MYSQL_ROOT_PASSWORD: mysql
-#          MYSQL_DATABASE: mysql
-#        options: >-
-#          --health-cmd "mysqladmin ping -h localhost"
-#          --health-interval 10s
-#          --health-timeout 5s
-#          --health-retries 5
-#        ports:
-#          - 3306:3306
+    #      mysql:
+    #        image: mysql
+    #        env:
+    #          MYSQL_ROOT_PASSWORD: mysql
+    #          MYSQL_DATABASE: mysql
+    #        options: >-
+    #          --health-cmd "mysqladmin ping -h localhost"
+    #          --health-interval 10s
+    #          --health-timeout 5s
+    #          --health-retries 5
+    #        ports:
+    #          - 3306:3306
     steps:
       - name: Checkout Repository
         uses: actions/checkout@v4
@@ -142,7 +142,6 @@ jobs:
               echo "Server has stopped"
               exit 1
           fi
-          
 
       - name: Test backend with Postgres
         if: success() || steps.sqlite.conclusion == 'failure'
@@ -171,6 +170,25 @@ jobs:
               exit 1
           fi
 
+          # Check that service will reconnect to postgres when connection will be closed
+          status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health)
+          if [[ "$status_code" -ne 200 ]] ; then
+            echo "Server has failed before postgres reconnect check"
+            exit 1
+          fi
+
+          echo "Terminating all connections to postgres..."
+          python -c "import os, psycopg2 as pg2; \
+            conn = pg2.connect(dsn=os.environ['DATABASE_URL'].replace('+pool', '')); \
+            cur = conn.cursor(); \
+            cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
+
+          status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health)
+          if [[ "$status_code" -ne 200 ]] ; then
+            echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
+            exit 1
+          fi
+
 #      - name: Test backend with MySQL
 #        if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
 #        env:

+ 24 - 0
CHANGELOG.md

@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [0.3.5] - 2024-06-16
+
+### Added
+
+- **📞 Enhanced Voice Call**: Text-to-speech (TTS) callback now operates in real-time for each sentence, reducing latency by not waiting for full completion.
+- **👆 Tap to Interrupt**: During a call, you can now stop the assistant from speaking by simply tapping, instead of using voice. This resolves the issue of the speaker's voice being mistakenly registered as input.
+- **😊 Emoji Call**: Toggle this feature on from the Settings > Interface, allowing LLMs to express emotions using emojis during voice calls for a more dynamic interaction.
+- **🖱️ Quick Archive/Delete**: Use the Shift key + mouseover on the chat list to swiftly archive or delete items.
+- **📝 Markdown Support in Model Descriptions**: You can now format model descriptions with markdown, enabling bold text, links, etc.
+- **🧠 Editable Memories**: Adds the capability to modify memories.
+- **📋 Admin Panel Sorting**: Introduces the ability to sort users/chats within the admin panel.
+- **🌑 Dark Mode for Quick Selectors**: Dark mode now available for chat quick selectors (prompts, models, documents).
+- **🔧 Advanced Parameters**: Adds 'num_keep' and 'num_batch' to advanced parameters for customization.
+- **📅 Dynamic System Prompts**: New variables '{{CURRENT_DATETIME}}', '{{CURRENT_TIME}}', '{{USER_LOCATION}}' added for system prompts. Ensure '{{USER_LOCATION}}' is toggled on from Settings > Interface.
+- **🌐 Tavily Web Search**: Includes Tavily as a web search provider option.
+- **🖊️ Federated Auth Usernames**: Ability to set user names for federated authentication.
+- **🔗 Auto Clean URLs**: When adding connection URLs, trailing slashes are now automatically removed.
+- **🌐 Enhanced Translations**: Improved Chinese and Swedish translations.
+
+### Fixed
+
+- **⏳ AIOHTTP_CLIENT_TIMEOUT**: Introduced a new environment variable 'AIOHTTP_CLIENT_TIMEOUT' for requests to Ollama lasting longer than 5 minutes. Default is 300 seconds; set to blank ('') for no timeout.
+- **❌ Message Delete Freeze**: Resolved an issue where message deletion would sometimes cause the web UI to freeze.
+
 ## [0.3.4] - 2024-06-12
 
 ### Fixed

+ 1 - 1
README.md

@@ -160,7 +160,7 @@ Check our Migration Guide available in our [Open WebUI Documentation](https://do
 If you want to try out the latest bleeding-edge features and are okay with occasional instability, you can use the `:dev` tag like this:
 
 ```bash
-docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:dev
+docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --add-host=host.docker.internal:host-gateway --restart always ghcr.io/open-webui/open-webui:dev
 ```
 
 ## What's Next? 🌟

+ 20 - 0
backend/apps/images/main.py

@@ -37,6 +37,10 @@ from config import (
     ENABLE_IMAGE_GENERATION,
     AUTOMATIC1111_BASE_URL,
     COMFYUI_BASE_URL,
+    COMFYUI_CFG_SCALE,
+    COMFYUI_SAMPLER,
+    COMFYUI_SCHEDULER,
+    COMFYUI_SD3,
     IMAGES_OPENAI_API_BASE_URL,
     IMAGES_OPENAI_API_KEY,
     IMAGE_GENERATION_MODEL,
@@ -78,6 +82,10 @@ app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
 
 app.state.config.IMAGE_SIZE = IMAGE_SIZE
 app.state.config.IMAGE_STEPS = IMAGE_STEPS
+app.state.config.COMFYUI_CFG_SCALE = COMFYUI_CFG_SCALE
+app.state.config.COMFYUI_SAMPLER = COMFYUI_SAMPLER
+app.state.config.COMFYUI_SCHEDULER = COMFYUI_SCHEDULER
+app.state.config.COMFYUI_SD3 = COMFYUI_SD3
 
 
 @app.get("/config")
@@ -457,6 +465,18 @@ def generate_image(
             if form_data.negative_prompt is not None:
                 data["negative_prompt"] = form_data.negative_prompt
 
+            if app.state.config.COMFYUI_CFG_SCALE:
+                data["cfg_scale"] = app.state.config.COMFYUI_CFG_SCALE
+
+            if app.state.config.COMFYUI_SAMPLER is not None:
+                data["sampler"] = app.state.config.COMFYUI_SAMPLER
+
+            if app.state.config.COMFYUI_SCHEDULER is not None:
+                data["scheduler"] = app.state.config.COMFYUI_SCHEDULER
+
+            if app.state.config.COMFYUI_SD3 is not None:
+                data["sd3"] = app.state.config.COMFYUI_SD3
+
             data = ImageGenerationPayload(**data)
 
             res = comfyui_generate_image(

+ 16 - 0
backend/apps/images/utils/comfyui.py

@@ -190,6 +190,10 @@ class ImageGenerationPayload(BaseModel):
     width: int
     height: int
     n: int = 1
+    cfg_scale: Optional[float] = None
+    sampler: Optional[str] = None
+    scheduler: Optional[str] = None
+    sd3: Optional[bool] = None
 
 
 def comfyui_generate_image(
@@ -199,6 +203,18 @@ def comfyui_generate_image(
 
     comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
 
+    if payload.cfg_scale:
+        comfyui_prompt["3"]["inputs"]["cfg"] = payload.cfg_scale
+
+    if payload.sampler:
+        comfyui_prompt["3"]["inputs"]["sampler"] = payload.sampler
+
+    if payload.scheduler:
+        comfyui_prompt["3"]["inputs"]["scheduler"] = payload.scheduler
+
+    if payload.sd3:
+        comfyui_prompt["5"]["class_type"] = "EmptySD3LatentImage"
+
     comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
     comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n
     comfyui_prompt["5"]["inputs"]["width"] = payload.width

+ 48 - 167
backend/apps/ollama/main.py

@@ -40,6 +40,7 @@ from utils.utils import (
     get_verified_user,
     get_admin_user,
 )
+from utils.task import prompt_template
 
 
 from config import (
@@ -52,7 +53,7 @@ from config import (
     UPLOAD_DIR,
     AppConfig,
 )
-from utils.misc import calculate_sha256
+from utils.misc import calculate_sha256, add_or_update_system_message
 
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
@@ -199,9 +200,6 @@ def merge_models_lists(model_lists):
     return list(merged_models.values())
 
 
-# user=Depends(get_current_user)
-
-
 async def get_all_models():
     log.info("get_all_models()")
 
@@ -817,24 +815,28 @@ async def generate_chat_completion(
                     "num_thread", None
                 )
 
-        if model_info.params.get("system", None):
+        system = model_info.params.get("system", None)
+        if system:
             # Check if the payload already has a system message
             # If not, add a system message to the payload
+            system = prompt_template(
+                system,
+                **(
+                    {
+                        "user_name": user.name,
+                        "user_location": (
+                            user.info.get("location") if user.info else None
+                        ),
+                    }
+                    if user
+                    else {}
+                ),
+            )
+
             if payload.get("messages"):
-                for message in payload["messages"]:
-                    if message.get("role") == "system":
-                        message["content"] = (
-                            model_info.params.get("system", None) + message["content"]
-                        )
-                        break
-                else:
-                    payload["messages"].insert(
-                        0,
-                        {
-                            "role": "system",
-                            "content": model_info.params.get("system", None),
-                        },
-                    )
+                payload["messages"] = add_or_update_system_message(
+                    system, payload["messages"]
+                )
 
     if url_idx == None:
         if ":" not in payload["model"]:
@@ -850,8 +852,7 @@ async def generate_chat_completion(
 
     url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.info(f"url: {url}")
-
-    print(payload)
+    log.debug(payload)
 
     return await post_streaming_url(f"{url}/api/chat", json.dumps(payload))
 
@@ -879,10 +880,11 @@ class OpenAIChatCompletionForm(BaseModel):
 @app.post("/v1/chat/completions")
 @app.post("/v1/chat/completions/{url_idx}")
 async def generate_openai_chat_completion(
-    form_data: OpenAIChatCompletionForm,
+    form_data: dict,
     url_idx: Optional[int] = None,
     user=Depends(get_verified_user),
 ):
+    form_data = OpenAIChatCompletionForm(**form_data)
 
     payload = {
         **form_data.model_dump(exclude_none=True),
@@ -914,22 +916,35 @@ async def generate_openai_chat_completion(
                 else None
             )
 
-        if model_info.params.get("system", None):
+        system = model_info.params.get("system", None)
+
+        if system:
+            system = prompt_template(
+                system,
+                **(
+                    {
+                        "user_name": user.name,
+                        "user_location": (
+                            user.info.get("location") if user.info else None
+                        ),
+                    }
+                    if user
+                    else {}
+                ),
+            )
             # Check if the payload already has a system message
             # If not, add a system message to the payload
             if payload.get("messages"):
                 for message in payload["messages"]:
                     if message.get("role") == "system":
-                        message["content"] = (
-                            model_info.params.get("system", None) + message["content"]
-                        )
+                        message["content"] = system + message["content"]
                         break
                 else:
                     payload["messages"].insert(
                         0,
                         {
                             "role": "system",
-                            "content": model_info.params.get("system", None),
+                            "content": system,
                         },
                     )
 
@@ -1095,17 +1110,13 @@ async def download_file_stream(
                         raise "Ollama: Could not create blob, Please try again."
 
 
-# def number_generator():
-#     for i in range(1, 101):
-#         yield f"data: {i}\n"
-
-
 # url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf"
 @app.post("/models/download")
 @app.post("/models/download/{url_idx}")
 async def download_model(
     form_data: UrlForm,
     url_idx: Optional[int] = None,
+    user=Depends(get_admin_user),
 ):
 
     allowed_hosts = ["https://huggingface.co/", "https://github.com/"]
@@ -1134,7 +1145,11 @@ async def download_model(
 
 @app.post("/models/upload")
 @app.post("/models/upload/{url_idx}")
-def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
+def upload_model(
+    file: UploadFile = File(...),
+    url_idx: Optional[int] = None,
+    user=Depends(get_admin_user),
+):
     if url_idx == None:
         url_idx = 0
     ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx]
@@ -1197,137 +1212,3 @@ def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
             yield f"data: {json.dumps(res)}\n\n"
 
     return StreamingResponse(file_process_stream(), media_type="text/event-stream")
-
-
-# async def upload_model(file: UploadFile = File(), url_idx: Optional[int] = None):
-#     if url_idx == None:
-#         url_idx = 0
-#     url = app.state.config.OLLAMA_BASE_URLS[url_idx]
-
-#     file_location = os.path.join(UPLOAD_DIR, file.filename)
-#     total_size = file.size
-
-#     async def file_upload_generator(file):
-#         print(file)
-#         try:
-#             async with aiofiles.open(file_location, "wb") as f:
-#                 completed_size = 0
-#                 while True:
-#                     chunk = await file.read(1024*1024)
-#                     if not chunk:
-#                         break
-#                     await f.write(chunk)
-#                     completed_size += len(chunk)
-#                     progress = (completed_size / total_size) * 100
-
-#                     print(progress)
-#                     yield f'data: {json.dumps({"status": "uploading", "percentage": progress, "total": total_size, "completed": completed_size, "done": False})}\n'
-#         except Exception as e:
-#             print(e)
-#             yield f"data: {json.dumps({'status': 'error', 'message': str(e)})}\n"
-#         finally:
-#             await file.close()
-#             print("done")
-#             yield f'data: {json.dumps({"status": "completed", "percentage": 100, "total": total_size, "completed": completed_size, "done": True})}\n'
-
-#     return StreamingResponse(
-#         file_upload_generator(copy.deepcopy(file)), media_type="text/event-stream"
-#     )
-
-
-@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
-async def deprecated_proxy(
-    path: str, request: Request, user=Depends(get_verified_user)
-):
-    url = app.state.config.OLLAMA_BASE_URLS[0]
-    target_url = f"{url}/{path}"
-
-    body = await request.body()
-    headers = dict(request.headers)
-
-    if user.role in ["user", "admin"]:
-        if path in ["pull", "delete", "push", "copy", "create"]:
-            if user.role != "admin":
-                raise HTTPException(
-                    status_code=status.HTTP_401_UNAUTHORIZED,
-                    detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
-                )
-    else:
-        raise HTTPException(
-            status_code=status.HTTP_401_UNAUTHORIZED,
-            detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
-        )
-
-    headers.pop("host", None)
-    headers.pop("authorization", None)
-    headers.pop("origin", None)
-    headers.pop("referer", None)
-
-    r = None
-
-    def get_request():
-        nonlocal r
-
-        request_id = str(uuid.uuid4())
-        try:
-            REQUEST_POOL.append(request_id)
-
-            def stream_content():
-                try:
-                    if path == "generate":
-                        data = json.loads(body.decode("utf-8"))
-
-                        if data.get("stream", True):
-                            yield json.dumps({"id": request_id, "done": False}) + "\n"
-
-                    elif path == "chat":
-                        yield json.dumps({"id": request_id, "done": False}) + "\n"
-
-                    for chunk in r.iter_content(chunk_size=8192):
-                        if request_id in REQUEST_POOL:
-                            yield chunk
-                        else:
-                            log.warning("User: canceled request")
-                            break
-                finally:
-                    if hasattr(r, "close"):
-                        r.close()
-                        if request_id in REQUEST_POOL:
-                            REQUEST_POOL.remove(request_id)
-
-            r = requests.request(
-                method=request.method,
-                url=target_url,
-                data=body,
-                headers=headers,
-                stream=True,
-            )
-
-            r.raise_for_status()
-
-            # r.close()
-
-            return StreamingResponse(
-                stream_content(),
-                status_code=r.status_code,
-                headers=dict(r.headers),
-            )
-        except Exception as e:
-            raise e
-
-    try:
-        return await run_in_threadpool(get_request)
-    except Exception as e:
-        error_detail = "Open WebUI: Server Connection Error"
-        if r is not None:
-            try:
-                res = r.json()
-                if "error" in res:
-                    error_detail = f"Ollama: {res['error']}"
-            except:
-                error_detail = f"Ollama: {e}"
-
-        raise HTTPException(
-            status_code=r.status_code if r else 500,
-            detail=error_detail,
-        )

+ 26 - 9
backend/apps/openai/main.py

@@ -20,6 +20,8 @@ from utils.utils import (
     get_verified_user,
     get_admin_user,
 )
+from utils.task import prompt_template
+
 from config import (
     SRC_LOG_LEVELS,
     ENABLE_OPENAI_API,
@@ -392,22 +394,34 @@ async def generate_chat_completion(
                     else None
                 )
 
-        if model_info.params.get("system", None):
+        system = model_info.params.get("system", None)
+        if system:
+            system = prompt_template(
+                system,
+                **(
+                    {
+                        "user_name": user.name,
+                        "user_location": (
+                            user.info.get("location") if user.info else None
+                        ),
+                    }
+                    if user
+                    else {}
+                ),
+            )
             # Check if the payload already has a system message
             # If not, add a system message to the payload
             if payload.get("messages"):
                 for message in payload["messages"]:
                     if message.get("role") == "system":
-                        message["content"] = (
-                            model_info.params.get("system", None) + message["content"]
-                        )
+                        message["content"] = system + message["content"]
                         break
                 else:
                     payload["messages"].insert(
                         0,
                         {
                             "role": "system",
-                            "content": model_info.params.get("system", None),
+                            "content": system,
                         },
                     )
 
@@ -418,7 +432,12 @@ async def generate_chat_completion(
     idx = model["urlIdx"]
 
     if "pipeline" in model and model.get("pipeline"):
-        payload["user"] = {"name": user.name, "id": user.id}
+        payload["user"] = {
+            "name": user.name,
+            "id": user.id,
+            "email": user.email,
+            "role": user.role,
+        }
 
     # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
     # This is a workaround until OpenAI fixes the issue with this model
@@ -430,13 +449,11 @@ async def generate_chat_completion(
     # Convert the modified body back to JSON
     payload = json.dumps(payload)
 
-    print(payload)
+    log.debug(payload)
 
     url = app.state.config.OPENAI_API_BASE_URLS[idx]
     key = app.state.config.OPENAI_API_KEYS[idx]
 
-    print(payload)
-
     headers = {}
     headers["Authorization"] = f"Bearer {key}"
     headers["Content-Type"] = "application/json"

+ 70 - 1
backend/apps/rag/main.py

@@ -55,6 +55,9 @@ from apps.webui.models.documents import (
     DocumentForm,
     DocumentResponse,
 )
+from apps.webui.models.files import (
+    Files,
+)
 
 from apps.rag.utils import (
     get_model_path,
@@ -112,6 +115,7 @@ from config import (
     YOUTUBE_LOADER_LANGUAGE,
     ENABLE_RAG_WEB_SEARCH,
     RAG_WEB_SEARCH_ENGINE,
+    RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
     SEARXNG_QUERY_URL,
     GOOGLE_PSE_API_KEY,
     GOOGLE_PSE_ENGINE_ID,
@@ -165,6 +169,7 @@ app.state.YOUTUBE_LOADER_TRANSLATION = None
 
 app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
 app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
+app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
 
 app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
 app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
@@ -775,6 +780,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
                 app.state.config.SEARXNG_QUERY_URL,
                 query,
                 app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
+                app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
             )
         else:
             raise Exception("No SEARXNG_QUERY_URL found in environment variables")
@@ -788,6 +794,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
                 app.state.config.GOOGLE_PSE_ENGINE_ID,
                 query,
                 app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
+                app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
             )
         else:
             raise Exception(
@@ -799,6 +806,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
                 app.state.config.BRAVE_SEARCH_API_KEY,
                 query,
                 app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
+                app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
             )
         else:
             raise Exception("No BRAVE_SEARCH_API_KEY found in environment variables")
@@ -808,6 +816,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
                 app.state.config.SERPSTACK_API_KEY,
                 query,
                 app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
+                app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
                 https_enabled=app.state.config.SERPSTACK_HTTPS,
             )
         else:
@@ -818,6 +827,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
                 app.state.config.SERPER_API_KEY,
                 query,
                 app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
+                app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
             )
         else:
             raise Exception("No SERPER_API_KEY found in environment variables")
@@ -827,11 +837,16 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
                 app.state.config.SERPLY_API_KEY,
                 query,
                 app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
+                app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
             )
         else:
             raise Exception("No SERPLY_API_KEY found in environment variables")
     elif engine == "duckduckgo":
-        return search_duckduckgo(query, app.state.config.RAG_WEB_SEARCH_RESULT_COUNT)
+        return search_duckduckgo(
+            query,
+            app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
+            app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+        )
     elif engine == "tavily":
         if app.state.config.TAVILY_API_KEY:
             return search_tavily(
@@ -1119,6 +1134,60 @@ def store_doc(
             )
 
 
+class ProcessDocForm(BaseModel):
+    file_id: str
+    collection_name: Optional[str] = None
+
+
+@app.post("/process/doc")
+def process_doc(
+    form_data: ProcessDocForm,
+    user=Depends(get_current_user),
+):
+    try:
+        file = Files.get_file_by_id(form_data.file_id)
+        file_path = file.meta.get("path", f"{UPLOAD_DIR}/{file.filename}")
+
+        f = open(file_path, "rb")
+
+        collection_name = form_data.collection_name
+        if collection_name == None:
+            collection_name = calculate_sha256(f)[:63]
+        f.close()
+
+        loader, known_type = get_loader(
+            file.filename, file.meta.get("content_type"), file_path
+        )
+        data = loader.load()
+
+        try:
+            result = store_data_in_vector_db(data, collection_name)
+
+            if result:
+                return {
+                    "status": True,
+                    "collection_name": collection_name,
+                    "known_type": known_type,
+                }
+        except Exception as e:
+            raise HTTPException(
+                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+                detail=e,
+            )
+    except Exception as e:
+        log.exception(e)
+        if "No pandoc was found" in str(e):
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED,
+            )
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT(e),
+            )
+
+
 class TextRAGForm(BaseModel):
     name: str
     content: str

+ 8 - 3
backend/apps/rag/search/brave.py

@@ -1,15 +1,17 @@
 import logging
-
+from typing import List, Optional
 import requests
 
-from apps.rag.search.main import SearchResult
+from apps.rag.search.main import SearchResult, get_filtered_results
 from config import SRC_LOG_LEVELS
 
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["RAG"])
 
 
-def search_brave(api_key: str, query: str, count: int) -> list[SearchResult]:
+def search_brave(
+    api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None
+) -> list[SearchResult]:
     """Search using Brave's Search API and return the results as a list of SearchResult objects.
 
     Args:
@@ -29,6 +31,9 @@ def search_brave(api_key: str, query: str, count: int) -> list[SearchResult]:
 
     json_response = response.json()
     results = json_response.get("web", {}).get("results", [])
+    if filter_list:
+        results = get_filtered_results(results, filter_list)
+
     return [
         SearchResult(
             link=result["url"], title=result.get("title"), snippet=result.get("snippet")

+ 7 - 4
backend/apps/rag/search/duckduckgo.py

@@ -1,6 +1,6 @@
 import logging
-
-from apps.rag.search.main import SearchResult
+from typing import List, Optional
+from apps.rag.search.main import SearchResult, get_filtered_results
 from duckduckgo_search import DDGS
 from config import SRC_LOG_LEVELS
 
@@ -8,7 +8,9 @@ log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["RAG"])
 
 
-def search_duckduckgo(query: str, count: int) -> list[SearchResult]:
+def search_duckduckgo(
+    query: str, count: int, filter_list: Optional[List[str]] = None
+) -> list[SearchResult]:
     """
     Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects.
     Args:
@@ -41,6 +43,7 @@ def search_duckduckgo(query: str, count: int) -> list[SearchResult]:
                 snippet=result.get("body"),
             )
         )
-    print(results)
+    if filter_list:
+        results = get_filtered_results(results, filter_list)
     # Return the list of search results
     return results

+ 9 - 3
backend/apps/rag/search/google_pse.py

@@ -1,9 +1,9 @@
 import json
 import logging
-
+from typing import List, Optional
 import requests
 
-from apps.rag.search.main import SearchResult
+from apps.rag.search.main import SearchResult, get_filtered_results
 from config import SRC_LOG_LEVELS
 
 log = logging.getLogger(__name__)
@@ -11,7 +11,11 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
 
 
 def search_google_pse(
-    api_key: str, search_engine_id: str, query: str, count: int
+    api_key: str,
+    search_engine_id: str,
+    query: str,
+    count: int,
+    filter_list: Optional[List[str]] = None,
 ) -> list[SearchResult]:
     """Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
 
@@ -35,6 +39,8 @@ def search_google_pse(
 
     json_response = response.json()
     results = json_response.get("items", [])
+    if filter_list:
+        results = get_filtered_results(results, filter_list)
     return [
         SearchResult(
             link=result["link"],

+ 12 - 1
backend/apps/rag/search/main.py

@@ -1,8 +1,19 @@
 from typing import Optional
-
+from urllib.parse import urlparse
 from pydantic import BaseModel
 
 
+def get_filtered_results(results, filter_list):
+    if not filter_list:
+        return results
+    filtered_results = []
+    for result in results:
+        domain = urlparse(result["url"]).netloc
+        if any(domain.endswith(filtered_domain) for filtered_domain in filter_list):
+            filtered_results.append(result)
+    return filtered_results
+
+
 class SearchResult(BaseModel):
     link: str
     title: Optional[str]

+ 9 - 3
backend/apps/rag/search/searxng.py

@@ -1,9 +1,9 @@
 import logging
 import requests
 
-from typing import List
+from typing import List, Optional
 
-from apps.rag.search.main import SearchResult
+from apps.rag.search.main import SearchResult, get_filtered_results
 from config import SRC_LOG_LEVELS
 
 log = logging.getLogger(__name__)
@@ -11,7 +11,11 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
 
 
 def search_searxng(
-    query_url: str, query: str, count: int, **kwargs
+    query_url: str,
+    query: str,
+    count: int,
+    filter_list: Optional[List[str]] = None,
+    **kwargs,
 ) -> List[SearchResult]:
     """
     Search a SearXNG instance for a given query and return the results as a list of SearchResult objects.
@@ -78,6 +82,8 @@ def search_searxng(
     json_response = response.json()
     results = json_response.get("results", [])
     sorted_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True)
+    if filter_list:
+        sorted_results = get_filtered_results(sorted_results, filter_list)
     return [
         SearchResult(
             link=result["url"], title=result.get("title"), snippet=result.get("content")

+ 7 - 3
backend/apps/rag/search/serper.py

@@ -1,16 +1,18 @@
 import json
 import logging
-
+from typing import List, Optional
 import requests
 
-from apps.rag.search.main import SearchResult
+from apps.rag.search.main import SearchResult, get_filtered_results
 from config import SRC_LOG_LEVELS
 
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["RAG"])
 
 
-def search_serper(api_key: str, query: str, count: int) -> list[SearchResult]:
+def search_serper(
+    api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None
+) -> list[SearchResult]:
     """Search using serper.dev's API and return the results as a list of SearchResult objects.
 
     Args:
@@ -29,6 +31,8 @@ def search_serper(api_key: str, query: str, count: int) -> list[SearchResult]:
     results = sorted(
         json_response.get("organic", []), key=lambda x: x.get("position", 0)
     )
+    if filter_list:
+        results = get_filtered_results(results, filter_list)
     return [
         SearchResult(
             link=result["link"],

+ 5 - 3
backend/apps/rag/search/serply.py

@@ -1,10 +1,10 @@
 import json
 import logging
-
+from typing import List, Optional
 import requests
 from urllib.parse import urlencode
 
-from apps.rag.search.main import SearchResult
+from apps.rag.search.main import SearchResult, get_filtered_results
 from config import SRC_LOG_LEVELS
 
 log = logging.getLogger(__name__)
@@ -19,6 +19,7 @@ def search_serply(
     limit: int = 10,
     device_type: str = "desktop",
     proxy_location: str = "US",
+    filter_list: Optional[List[str]] = None,
 ) -> list[SearchResult]:
     """Search using serper.dev's API and return the results as a list of SearchResult objects.
 
@@ -57,7 +58,8 @@ def search_serply(
     results = sorted(
         json_response.get("results", []), key=lambda x: x.get("realPosition", 0)
     )
-
+    if filter_list:
+        results = get_filtered_results(results, filter_list)
     return [
         SearchResult(
             link=result["link"],

+ 9 - 3
backend/apps/rag/search/serpstack.py

@@ -1,9 +1,9 @@
 import json
 import logging
-
+from typing import List, Optional
 import requests
 
-from apps.rag.search.main import SearchResult
+from apps.rag.search.main import SearchResult, get_filtered_results
 from config import SRC_LOG_LEVELS
 
 log = logging.getLogger(__name__)
@@ -11,7 +11,11 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
 
 
 def search_serpstack(
-    api_key: str, query: str, count: int, https_enabled: bool = True
+    api_key: str,
+    query: str,
+    count: int,
+    filter_list: Optional[List[str]] = None,
+    https_enabled: bool = True,
 ) -> list[SearchResult]:
     """Search using serpstack.com's and return the results as a list of SearchResult objects.
 
@@ -35,6 +39,8 @@ def search_serpstack(
     results = sorted(
         json_response.get("organic_results", []), key=lambda x: x.get("position", 0)
     )
+    if filter_list:
+        results = get_filtered_results(results, filter_list)
     return [
         SearchResult(
             link=result["url"], title=result.get("title"), snippet=result.get("snippet")

+ 10 - 10
backend/apps/rag/utils.py

@@ -237,7 +237,7 @@ def get_embedding_function(
 
 
 def get_rag_context(
-    docs,
+    files,
     messages,
     embedding_function,
     k,
@@ -245,29 +245,29 @@ def get_rag_context(
     r,
     hybrid_search,
 ):
-    log.debug(f"docs: {docs} {messages} {embedding_function} {reranking_function}")
+    log.debug(f"files: {files} {messages} {embedding_function} {reranking_function}")
     query = get_last_user_message(messages)
 
     extracted_collections = []
     relevant_contexts = []
 
-    for doc in docs:
+    for file in files:
         context = None
 
         collection_names = (
-            doc["collection_names"]
-            if doc["type"] == "collection"
-            else [doc["collection_name"]]
+            file["collection_names"]
+            if file["type"] == "collection"
+            else [file["collection_name"]]
         )
 
         collection_names = set(collection_names).difference(extracted_collections)
         if not collection_names:
-            log.debug(f"skipping {doc} as it has already been extracted")
+            log.debug(f"skipping {file} as it has already been extracted")
             continue
 
         try:
-            if doc["type"] == "text":
-                context = doc["content"]
+            if file["type"] == "text":
+                context = file["content"]
             else:
                 if hybrid_search:
                     context = query_collection_with_hybrid_search(
@@ -290,7 +290,7 @@ def get_rag_context(
             context = None
 
         if context:
-            relevant_contexts.append({**context, "source": doc})
+            relevant_contexts.append({**context, "source": file})
 
         extracted_collections.extend(collection_names)
 

+ 21 - 6
backend/apps/webui/internal/db.py

@@ -1,11 +1,12 @@
+import os
+import logging
 import json
 
 from peewee import *
 from peewee_migrate import Router
-from playhouse.db_url import connect
+
+from apps.webui.internal.wrappers import register_connection
 from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR
-import os
-import logging
 
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["DB"])
@@ -28,12 +29,26 @@ if os.path.exists(f"{DATA_DIR}/ollama.db"):
 else:
     pass
 
-DB = connect(DATABASE_URL)
-log.info(f"Connected to a {DB.__class__.__name__} database.")
+
+# The `register_connection` function encapsulates the logic for setting up
+# the database connection based on the connection string, while `connect`
+# is a Peewee-specific method to manage the connection state and avoid errors
+# when a connection is already open.
+try:
+    DB = register_connection(DATABASE_URL)
+    log.info(f"Connected to a {DB.__class__.__name__} database.")
+except Exception as e:
+    log.error(f"Failed to initialize the database connection: {e}")
+    raise
+
 router = Router(
     DB,
     migrate_dir=BACKEND_DIR / "apps" / "webui" / "internal" / "migrations",
     logger=log,
 )
 router.run()
-DB.connect(reuse_if_open=True)
+try:
+    DB.connect(reuse_if_open=True)
+except OperationalError as e:
+    log.info(f"Failed to connect to database again due to: {e}")
+    pass

+ 48 - 0
backend/apps/webui/internal/migrations/013_add_user_info.py

@@ -0,0 +1,48 @@
+"""Peewee migrations -- 002_add_local_sharing.py.
+
+Some examples (model - class or model name)::
+
+    > Model = migrator.orm['table_name']            # Return model in current state by name
+    > Model = migrator.ModelClass                   # Return model in current state by name
+
+    > migrator.sql(sql)                             # Run custom SQL
+    > migrator.run(func, *args, **kwargs)           # Run python function with the given args
+    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
+    > migrator.remove_model(model, cascade=True)    # Remove a model
+    > migrator.add_fields(model, **fields)          # Add fields to a model
+    > migrator.change_fields(model, **fields)       # Change fields
+    > migrator.remove_fields(model, *field_names, cascade=True)
+    > migrator.rename_field(model, old_field_name, new_field_name)
+    > migrator.rename_table(model, new_table_name)
+    > migrator.add_index(model, *col_names, unique=False)
+    > migrator.add_not_null(model, *field_names)
+    > migrator.add_default(model, field_name, default)
+    > migrator.add_constraint(model, name, sql)
+    > migrator.drop_index(model, *col_names)
+    > migrator.drop_not_null(model, *field_names)
+    > migrator.drop_constraints(model, *constraints)
+
+"""
+
+from contextlib import suppress
+
+import peewee as pw
+from peewee_migrate import Migrator
+
+
+with suppress(ImportError):
+    import playhouse.postgres_ext as pw_pext
+
+
+def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
+    """Write your migrations here."""
+
+    # Adding fields info to the 'user' table
+    migrator.add_fields("user", info=pw.TextField(null=True))
+
+
+def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
+    """Write your rollback migrations here."""
+
+    # Remove the settings field
+    migrator.remove_fields("user", "info")

+ 55 - 0
backend/apps/webui/internal/migrations/014_add_files.py

@@ -0,0 +1,55 @@
+"""Peewee migrations -- 009_add_models.py.
+
+Some examples (model - class or model name)::
+
+    > Model = migrator.orm['table_name']            # Return model in current state by name
+    > Model = migrator.ModelClass                   # Return model in current state by name
+
+    > migrator.sql(sql)                             # Run custom SQL
+    > migrator.run(func, *args, **kwargs)           # Run python function with the given args
+    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
+    > migrator.remove_model(model, cascade=True)    # Remove a model
+    > migrator.add_fields(model, **fields)          # Add fields to a model
+    > migrator.change_fields(model, **fields)       # Change fields
+    > migrator.remove_fields(model, *field_names, cascade=True)
+    > migrator.rename_field(model, old_field_name, new_field_name)
+    > migrator.rename_table(model, new_table_name)
+    > migrator.add_index(model, *col_names, unique=False)
+    > migrator.add_not_null(model, *field_names)
+    > migrator.add_default(model, field_name, default)
+    > migrator.add_constraint(model, name, sql)
+    > migrator.drop_index(model, *col_names)
+    > migrator.drop_not_null(model, *field_names)
+    > migrator.drop_constraints(model, *constraints)
+
+"""
+
+from contextlib import suppress
+
+import peewee as pw
+from peewee_migrate import Migrator
+
+
+with suppress(ImportError):
+    import playhouse.postgres_ext as pw_pext
+
+
+def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
+    """Write your migrations here."""
+
+    @migrator.create_model
+    class File(pw.Model):
+        id = pw.TextField(unique=True)
+        user_id = pw.TextField()
+        filename = pw.TextField()
+        meta = pw.TextField()
+        created_at = pw.BigIntegerField(null=False)
+
+        class Meta:
+            table_name = "file"
+
+
+def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
+    """Write your rollback migrations here."""
+
+    migrator.remove_model("file")

+ 61 - 0
backend/apps/webui/internal/migrations/015_add_functions.py

@@ -0,0 +1,61 @@
+"""Peewee migrations -- 009_add_models.py.
+
+Some examples (model - class or model name)::
+
+    > Model = migrator.orm['table_name']            # Return model in current state by name
+    > Model = migrator.ModelClass                   # Return model in current state by name
+
+    > migrator.sql(sql)                             # Run custom SQL
+    > migrator.run(func, *args, **kwargs)           # Run python function with the given args
+    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
+    > migrator.remove_model(model, cascade=True)    # Remove a model
+    > migrator.add_fields(model, **fields)          # Add fields to a model
+    > migrator.change_fields(model, **fields)       # Change fields
+    > migrator.remove_fields(model, *field_names, cascade=True)
+    > migrator.rename_field(model, old_field_name, new_field_name)
+    > migrator.rename_table(model, new_table_name)
+    > migrator.add_index(model, *col_names, unique=False)
+    > migrator.add_not_null(model, *field_names)
+    > migrator.add_default(model, field_name, default)
+    > migrator.add_constraint(model, name, sql)
+    > migrator.drop_index(model, *col_names)
+    > migrator.drop_not_null(model, *field_names)
+    > migrator.drop_constraints(model, *constraints)
+
+"""
+
+from contextlib import suppress
+
+import peewee as pw
+from peewee_migrate import Migrator
+
+
+with suppress(ImportError):
+    import playhouse.postgres_ext as pw_pext
+
+
+def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
+    """Write your migrations here."""
+
+    @migrator.create_model
+    class Function(pw.Model):
+        id = pw.TextField(unique=True)
+        user_id = pw.TextField()
+
+        name = pw.TextField()
+        type = pw.TextField()
+
+        content = pw.TextField()
+        meta = pw.TextField()
+
+        created_at = pw.BigIntegerField(null=False)
+        updated_at = pw.BigIntegerField(null=False)
+
+        class Meta:
+            table_name = "function"
+
+
+def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
+    """Write your rollback migrations here."""
+
+    migrator.remove_model("function")

+ 72 - 0
backend/apps/webui/internal/wrappers.py

@@ -0,0 +1,72 @@
+from contextvars import ContextVar
+from peewee import *
+from peewee import PostgresqlDatabase, InterfaceError as PeeWeeInterfaceError
+
+import logging
+from playhouse.db_url import connect, parse
+from playhouse.shortcuts import ReconnectMixin
+
+from config import SRC_LOG_LEVELS
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["DB"])
+
+db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None}
+db_state = ContextVar("db_state", default=db_state_default.copy())
+
+
+class PeeweeConnectionState(object):
+    def __init__(self, **kwargs):
+        super().__setattr__("_state", db_state)
+        super().__init__(**kwargs)
+
+    def __setattr__(self, name, value):
+        self._state.get()[name] = value
+
+    def __getattr__(self, name):
+        value = self._state.get()[name]
+        return value
+
+
+class CustomReconnectMixin(ReconnectMixin):
+    reconnect_errors = (
+        # psycopg2
+        (OperationalError, "termin"),
+        (InterfaceError, "closed"),
+        # peewee
+        (PeeWeeInterfaceError, "closed"),
+    )
+
+
+class ReconnectingPostgresqlDatabase(CustomReconnectMixin, PostgresqlDatabase):
+    pass
+
+
+def register_connection(db_url):
+    db = connect(db_url)
+    if isinstance(db, PostgresqlDatabase):
+        # Enable autoconnect for SQLite databases, managed by Peewee
+        db.autoconnect = True
+        db.reuse_if_open = True
+        log.info("Connected to PostgreSQL database")
+
+        # Get the connection details
+        connection = parse(db_url)
+
+        # Use our custom database class that supports reconnection
+        db = ReconnectingPostgresqlDatabase(
+            connection["database"],
+            user=connection["user"],
+            password=connection["password"],
+            host=connection["host"],
+            port=connection["port"],
+        )
+        db.connect(reuse_if_open=True)
+    elif isinstance(db, SqliteDatabase):
+        # Enable autoconnect for SQLite databases, managed by Peewee
+        db.autoconnect = True
+        db.reuse_if_open = True
+        log.info("Connected to SQLite database")
+    else:
+        raise ValueError("Unsupported database connection")
+    return db

+ 69 - 3
backend/apps/webui/main.py

@@ -14,7 +14,12 @@ from apps.webui.routers import (
     configs,
     memories,
     utils,
+    files,
+    functions,
 )
+from apps.webui.models.functions import Functions
+from apps.webui.utils import load_function_module_by_id
+
 from config import (
     WEBUI_BUILD_HASH,
     SHOW_ADMIN_DETAILS,
@@ -27,6 +32,7 @@ from config import (
     USER_PERMISSIONS,
     WEBHOOK_URL,
     WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
+    WEBUI_AUTH_TRUSTED_NAME_HEADER,
     JWT_EXPIRES_IN,
     WEBUI_BANNERS,
     ENABLE_COMMUNITY_SHARING,
@@ -42,6 +48,7 @@ app.state.config = AppConfig()
 app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
 app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
 app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
+app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
 
 
 app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
@@ -59,7 +66,7 @@ app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
 
 app.state.MODELS = {}
 app.state.TOOLS = {}
-
+app.state.FUNCTIONS = {}
 
 app.add_middleware(
     CORSMiddleware,
@@ -69,17 +76,21 @@ app.add_middleware(
     allow_headers=["*"],
 )
 
+
+app.include_router(configs.router, prefix="/configs", tags=["configs"])
 app.include_router(auths.router, prefix="/auths", tags=["auths"])
 app.include_router(users.router, prefix="/users", tags=["users"])
 app.include_router(chats.router, prefix="/chats", tags=["chats"])
 
 app.include_router(documents.router, prefix="/documents", tags=["documents"])
-app.include_router(tools.router, prefix="/tools", tags=["tools"])
 app.include_router(models.router, prefix="/models", tags=["models"])
 app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
+
 app.include_router(memories.router, prefix="/memories", tags=["memories"])
+app.include_router(files.router, prefix="/files", tags=["files"])
+app.include_router(tools.router, prefix="/tools", tags=["tools"])
+app.include_router(functions.router, prefix="/functions", tags=["functions"])
 
-app.include_router(configs.router, prefix="/configs", tags=["configs"])
 app.include_router(utils.router, prefix="/utils", tags=["utils"])
 
 
@@ -91,3 +102,58 @@ async def get_status():
         "default_models": app.state.config.DEFAULT_MODELS,
         "default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
     }
+
+
+async def get_pipe_models():
+    pipes = Functions.get_functions_by_type("pipe")
+    pipe_models = []
+
+    for pipe in pipes:
+        # Check if function is already loaded
+        if pipe.id not in app.state.FUNCTIONS:
+            function_module, function_type = load_function_module_by_id(pipe.id)
+            app.state.FUNCTIONS[pipe.id] = function_module
+        else:
+            function_module = app.state.FUNCTIONS[pipe.id]
+
+        # Check if function is a manifold
+        if hasattr(function_module, "type"):
+            if function_module.type == "manifold":
+                manifold_pipes = []
+
+                # Check if pipes is a function or a list
+                if callable(function_module.pipes):
+                    manifold_pipes = function_module.pipes()
+                else:
+                    manifold_pipes = function_module.pipes
+
+                for p in manifold_pipes:
+                    manifold_pipe_id = f'{pipe.id}.{p["id"]}'
+                    manifold_pipe_name = p["name"]
+
+                    if hasattr(function_module, "name"):
+                        manifold_pipe_name = f"{pipe.name}{manifold_pipe_name}"
+
+                    pipe_models.append(
+                        {
+                            "id": manifold_pipe_id,
+                            "name": manifold_pipe_name,
+                            "object": "model",
+                            "created": pipe.created_at,
+                            "owned_by": "openai",
+                            "pipe": {"type": pipe.type},
+                        }
+                    )
+        else:
+            pipe_models.append(
+                {
+                    "id": pipe.id,
+                    "name": pipe.name,
+                    "object": "model",
+                    "created": pipe.created_at,
+                    "owned_by": "openai",
+                    "pipe": {"type": "pipe"},
+                }
+            )
+
+    return pipe_models

+ 112 - 0
backend/apps/webui/models/files.py

@@ -0,0 +1,112 @@
+from pydantic import BaseModel
+from peewee import *
+from playhouse.shortcuts import model_to_dict
+from typing import List, Union, Optional
+import time
+import logging
+from apps.webui.internal.db import DB, JSONField
+
+import json
+
+from config import SRC_LOG_LEVELS
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["MODELS"])
+
+####################
+# Files DB Schema
+####################
+
+
+class File(Model):
+    id = CharField(unique=True)
+    user_id = CharField()
+    filename = TextField()
+    meta = JSONField()
+    created_at = BigIntegerField()
+
+    class Meta:
+        database = DB
+
+
+class FileModel(BaseModel):
+    id: str
+    user_id: str
+    filename: str
+    meta: dict
+    created_at: int  # timestamp in epoch
+
+
+####################
+# Forms
+####################
+
+
+class FileModelResponse(BaseModel):
+    id: str
+    user_id: str
+    filename: str
+    meta: dict
+    created_at: int  # timestamp in epoch
+
+
+class FileForm(BaseModel):
+    id: str
+    filename: str
+    meta: dict = {}
+
+
+class FilesTable:
+    def __init__(self, db):
+        self.db = db
+        self.db.create_tables([File])
+
+    def insert_new_file(self, user_id: str, form_data: FileForm) -> Optional[FileModel]:
+        file = FileModel(
+            **{
+                **form_data.model_dump(),
+                "user_id": user_id,
+                "created_at": int(time.time()),
+            }
+        )
+
+        try:
+            result = File.create(**file.model_dump())
+            if result:
+                return file
+            else:
+                return None
+        except Exception as e:
+            print(f"Error creating tool: {e}")
+            return None
+
+    def get_file_by_id(self, id: str) -> Optional[FileModel]:
+        try:
+            file = File.get(File.id == id)
+            return FileModel(**model_to_dict(file))
+        except:
+            return None
+
+    def get_files(self) -> List[FileModel]:
+        return [FileModel(**model_to_dict(file)) for file in File.select()]
+
+    def delete_file_by_id(self, id: str) -> bool:
+        try:
+            query = File.delete().where((File.id == id))
+            query.execute()  # Remove the rows, return number of rows removed.
+
+            return True
+        except:
+            return False
+
+    def delete_all_files(self) -> bool:
+        try:
+            query = File.delete()
+            query.execute()  # Remove the rows, return number of rows removed.
+
+            return True
+        except:
+            return False
+
+
+Files = FilesTable(DB)

+ 141 - 0
backend/apps/webui/models/functions.py

@@ -0,0 +1,141 @@
+from pydantic import BaseModel
+from peewee import *
+from playhouse.shortcuts import model_to_dict
+from typing import List, Union, Optional
+import time
+import logging
+from apps.webui.internal.db import DB, JSONField
+
+import json
+
+from config import SRC_LOG_LEVELS
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["MODELS"])
+
+####################
+# Functions DB Schema
+####################
+
+
+class Function(Model):
+    id = CharField(unique=True)
+    user_id = CharField()
+    name = TextField()
+    type = TextField()
+    content = TextField()
+    meta = JSONField()
+    updated_at = BigIntegerField()
+    created_at = BigIntegerField()
+
+    class Meta:
+        database = DB
+
+
+class FunctionMeta(BaseModel):
+    description: Optional[str] = None
+
+
+class FunctionModel(BaseModel):
+    id: str
+    user_id: str
+    name: str
+    type: str
+    content: str
+    meta: FunctionMeta
+    updated_at: int  # timestamp in epoch
+    created_at: int  # timestamp in epoch
+
+
+####################
+# Forms
+####################
+
+
+class FunctionResponse(BaseModel):
+    id: str
+    user_id: str
+    type: str
+    name: str
+    meta: FunctionMeta
+    updated_at: int  # timestamp in epoch
+    created_at: int  # timestamp in epoch
+
+
+class FunctionForm(BaseModel):
+    id: str
+    name: str
+    content: str
+    meta: FunctionMeta
+
+
+class FunctionsTable:
+    def __init__(self, db):
+        self.db = db
+        self.db.create_tables([Function])
+
+    def insert_new_function(
+        self, user_id: str, type: str, form_data: FunctionForm
+    ) -> Optional[FunctionModel]:
+        function = FunctionModel(
+            **{
+                **form_data.model_dump(),
+                "user_id": user_id,
+                "type": type,
+                "updated_at": int(time.time()),
+                "created_at": int(time.time()),
+            }
+        )
+
+        try:
+            result = Function.create(**function.model_dump())
+            if result:
+                return function
+            else:
+                return None
+        except Exception as e:
+            print(f"Error creating tool: {e}")
+            return None
+
+    def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
+        try:
+            function = Function.get(Function.id == id)
+            return FunctionModel(**model_to_dict(function))
+        except:
+            return None
+
+    def get_functions(self) -> List[FunctionModel]:
+        return [
+            FunctionModel(**model_to_dict(function)) for function in Function.select()
+        ]
+
+    def get_functions_by_type(self, type: str) -> List[FunctionModel]:
+        return [
+            FunctionModel(**model_to_dict(function))
+            for function in Function.select().where(Function.type == type)
+        ]
+
+    def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
+        try:
+            query = Function.update(
+                **updated,
+                updated_at=int(time.time()),
+            ).where(Function.id == id)
+            query.execute()
+
+            function = Function.get(Function.id == id)
+            return FunctionModel(**model_to_dict(function))
+        except:
+            return None
+
+    def delete_function_by_id(self, id: str) -> bool:
+        try:
+            query = Function.delete().where((Function.id == id))
+            query.execute()  # Remove the rows, return number of rows removed.
+
+            return True
+        except:
+            return False
+
+
+Functions = FunctionsTable(DB)

+ 2 - 0
backend/apps/webui/models/users.py

@@ -26,6 +26,7 @@ class User(Model):
 
     api_key = CharField(null=True, unique=True)
     settings = JSONField(null=True)
+    info = JSONField(null=True)
 
     oauth_sub = TextField(null=True, unique=True)
 
@@ -52,6 +53,7 @@ class UserModel(BaseModel):
 
     api_key: Optional[str] = None
     settings: Optional[UserSettings] = None
+    info: Optional[dict] = None
 
     oauth_sub: Optional[str] = None
 

+ 39 - 4
backend/apps/webui/routers/auths.py

@@ -2,6 +2,7 @@ import logging
 
 from fastapi import Request, UploadFile, File
 from fastapi import Depends, HTTPException, status
+from fastapi.responses import Response
 
 from fastapi import APIRouter
 from pydantic import BaseModel
@@ -35,6 +36,7 @@ from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
 from config import (
     WEBUI_AUTH,
     WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
+    WEBUI_AUTH_TRUSTED_NAME_HEADER,
 )
 
 router = APIRouter()
@@ -45,7 +47,21 @@ router = APIRouter()
 
 
 @router.get("/", response_model=UserResponse)
-async def get_session_user(user=Depends(get_current_user)):
+async def get_session_user(
+    request: Request, response: Response, user=Depends(get_current_user)
+):
+    token = create_token(
+        data={"id": user.id},
+        expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
+    )
+
+    # Set the cookie token
+    response.set_cookie(
+        key="token",
+        value=token,
+        httponly=True,  # Ensures the cookie is not accessible via JavaScript
+    )
+
     return {
         "id": user.id,
         "email": user.email,
@@ -106,17 +122,22 @@ async def update_password(
 
 
 @router.post("/signin", response_model=SigninResponse)
-async def signin(request: Request, form_data: SigninForm):
+async def signin(request: Request, response: Response, form_data: SigninForm):
     if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
         if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers:
             raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER)
 
         trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
+        trusted_name = trusted_email
+        if WEBUI_AUTH_TRUSTED_NAME_HEADER:
+            trusted_name = request.headers.get(
+                WEBUI_AUTH_TRUSTED_NAME_HEADER, trusted_email
+            )
         if not Users.get_user_by_email(trusted_email.lower()):
             await signup(
                 request,
                 SignupForm(
-                    email=trusted_email, password=str(uuid.uuid4()), name=trusted_email
+                    email=trusted_email, password=str(uuid.uuid4()), name=trusted_name
                 ),
             )
         user = Auths.authenticate_user_by_trusted_header(trusted_email)
@@ -145,6 +166,13 @@ async def signin(request: Request, form_data: SigninForm):
             expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
         )
 
+        # Set the cookie token
+        response.set_cookie(
+            key="token",
+            value=token,
+            httponly=True,  # Ensures the cookie is not accessible via JavaScript
+        )
+
         return {
             "token": token,
             "token_type": "Bearer",
@@ -164,7 +192,7 @@ async def signin(request: Request, form_data: SigninForm):
 
 
 @router.post("/signup", response_model=SigninResponse)
-async def signup(request: Request, form_data: SignupForm):
+async def signup(request: Request, response: Response, form_data: SignupForm):
     if not request.app.state.config.ENABLE_SIGNUP and WEBUI_AUTH:
         raise HTTPException(
             status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
@@ -200,6 +228,13 @@ async def signup(request: Request, form_data: SignupForm):
             )
             # response.set_cookie(key='token', value=token, httponly=True)
 
+            # Set the cookie token
+            response.set_cookie(
+                key="token",
+                value=token,
+                httponly=True,  # Ensures the cookie is not accessible via JavaScript
+            )
+
             if request.app.state.config.WEBHOOK_URL:
                 post_webhook(
                     request.app.state.config.WEBHOOK_URL,

+ 219 - 0
backend/apps/webui/routers/files.py

@@ -0,0 +1,219 @@
+from fastapi import (
+    Depends,
+    FastAPI,
+    HTTPException,
+    status,
+    Request,
+    UploadFile,
+    File,
+    Form,
+)
+
+
+from datetime import datetime, timedelta
+from typing import List, Union, Optional
+from pathlib import Path
+
+from fastapi import APIRouter
+from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
+
+from pydantic import BaseModel
+import json
+
+from apps.webui.models.files import (
+    Files,
+    FileForm,
+    FileModel,
+    FileModelResponse,
+)
+from utils.utils import get_verified_user, get_admin_user
+from constants import ERROR_MESSAGES
+
+from importlib import util
+import os
+import uuid
+import os, shutil, logging, re
+
+
+from config import SRC_LOG_LEVELS, UPLOAD_DIR
+
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["MODELS"])
+
+
+router = APIRouter()
+
+############################
+# Upload File
+############################
+
+
+@router.post("/")
+def upload_file(
+    file: UploadFile = File(...),
+    user=Depends(get_verified_user),
+):
+    log.info(f"file.content_type: {file.content_type}")
+    try:
+        unsanitized_filename = file.filename
+        filename = os.path.basename(unsanitized_filename)
+
+        # replace filename with uuid
+        id = str(uuid.uuid4())
+        filename = f"{id}_{filename}"
+        file_path = f"{UPLOAD_DIR}/{filename}"
+
+        contents = file.file.read()
+        with open(file_path, "wb") as f:
+            f.write(contents)
+            f.close()
+
+        file = Files.insert_new_file(
+            user.id,
+            FileForm(
+                **{
+                    "id": id,
+                    "filename": filename,
+                    "meta": {
+                        "content_type": file.content_type,
+                        "size": len(contents),
+                        "path": file_path,
+                    },
+                }
+            ),
+        )
+
+        if file:
+            return file
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT("Error uploading file"),
+            )
+
+    except Exception as e:
+        log.exception(e)
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.DEFAULT(e),
+        )
+
+
+############################
+# List Files
+############################
+
+
+@router.get("/", response_model=List[FileModel])
+async def list_files(user=Depends(get_verified_user)):
+    files = Files.get_files()
+    return files
+
+
+############################
+# Delete All Files
+############################
+
+
+@router.delete("/all")
+async def delete_all_files(user=Depends(get_admin_user)):
+    result = Files.delete_all_files()
+
+    if result:
+        folder = f"{UPLOAD_DIR}"
+        try:
+            # Check if the directory exists
+            if os.path.exists(folder):
+                # Iterate over all the files and directories in the specified directory
+                for filename in os.listdir(folder):
+                    file_path = os.path.join(folder, filename)
+                    try:
+                        if os.path.isfile(file_path) or os.path.islink(file_path):
+                            os.unlink(file_path)  # Remove the file or link
+                        elif os.path.isdir(file_path):
+                            shutil.rmtree(file_path)  # Remove the directory
+                    except Exception as e:
+                        print(f"Failed to delete {file_path}. Reason: {e}")
+            else:
+                print(f"The directory {folder} does not exist")
+        except Exception as e:
+            print(f"Failed to process the directory {folder}. Reason: {e}")
+
+        return {"message": "All files deleted successfully"}
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.DEFAULT("Error deleting files"),
+        )
+
+
+############################
+# Get File By Id
+############################
+
+
+@router.get("/{id}", response_model=Optional[FileModel])
+async def get_file_by_id(id: str, user=Depends(get_verified_user)):
+    file = Files.get_file_by_id(id)
+
+    if file:
+        return file
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+
+############################
+# Get File Content By Id
+############################
+
+
+@router.get("/{id}/content", response_model=Optional[FileModel])
+async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
+    file = Files.get_file_by_id(id)
+
+    if file:
+        file_path = Path(file.meta["path"])
+
+        # Check if the file already exists in the cache
+        if file_path.is_file():
+            print(f"file_path: {file_path}")
+            return FileResponse(file_path)
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_404_NOT_FOUND,
+                detail=ERROR_MESSAGES.NOT_FOUND,
+            )
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+
+############################
+# Delete File By Id
+############################
+
+
+@router.delete("/{id}")
+async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
+    file = Files.get_file_by_id(id)
+
+    if file:
+        result = Files.delete_file_by_id(id)
+        if result:
+            return {"message": "File deleted successfully"}
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT("Error deleting file"),
+            )
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )

+ 180 - 0
backend/apps/webui/routers/functions.py

@@ -0,0 +1,180 @@
+from fastapi import Depends, FastAPI, HTTPException, status, Request
+from datetime import datetime, timedelta
+from typing import List, Union, Optional
+
+from fastapi import APIRouter
+from pydantic import BaseModel
+import json
+
+from apps.webui.models.functions import (
+    Functions,
+    FunctionForm,
+    FunctionModel,
+    FunctionResponse,
+)
+from apps.webui.utils import load_function_module_by_id
+from utils.utils import get_verified_user, get_admin_user
+from constants import ERROR_MESSAGES
+
+from importlib import util
+import os
+from pathlib import Path
+
+from config import DATA_DIR, CACHE_DIR, FUNCTIONS_DIR
+
+
+router = APIRouter()
+
+############################
+# GetFunctions
+############################
+
+
+@router.get("/", response_model=List[FunctionResponse])
+async def get_functions(user=Depends(get_verified_user)):
+    return Functions.get_functions()
+
+
+############################
+# ExportFunctions
+############################
+
+
+@router.get("/export", response_model=List[FunctionModel])
+async def get_functions(user=Depends(get_admin_user)):
+    return Functions.get_functions()
+
+
+############################
+# CreateNewFunction
+############################
+
+
+@router.post("/create", response_model=Optional[FunctionResponse])
+async def create_new_function(
+    request: Request, form_data: FunctionForm, user=Depends(get_admin_user)
+):
+    if not form_data.id.isidentifier():
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail="Only alphanumeric characters and underscores are allowed in the id",
+        )
+
+    form_data.id = form_data.id.lower()
+
+    function = Functions.get_function_by_id(form_data.id)
+    if function == None:
+        function_path = os.path.join(FUNCTIONS_DIR, f"{form_data.id}.py")
+        try:
+            with open(function_path, "w") as function_file:
+                function_file.write(form_data.content)
+
+            function_module, function_type = load_function_module_by_id(form_data.id)
+
+            FUNCTIONS = request.app.state.FUNCTIONS
+            FUNCTIONS[form_data.id] = function_module
+
+            function = Functions.insert_new_function(user.id, function_type, form_data)
+
+            function_cache_dir = Path(CACHE_DIR) / "functions" / form_data.id
+            function_cache_dir.mkdir(parents=True, exist_ok=True)
+
+            if function:
+                return function
+            else:
+                raise HTTPException(
+                    status_code=status.HTTP_400_BAD_REQUEST,
+                    detail=ERROR_MESSAGES.DEFAULT("Error creating function"),
+                )
+        except Exception as e:
+            print(e)
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT(e),
+            )
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.ID_TAKEN,
+        )
+
+
+############################
+# GetFunctionById
+############################
+
+
+@router.get("/id/{id}", response_model=Optional[FunctionModel])
+async def get_function_by_id(id: str, user=Depends(get_admin_user)):
+    function = Functions.get_function_by_id(id)
+
+    if function:
+        return function
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+
+############################
+# UpdateFunctionById
+############################
+
+
+@router.post("/id/{id}/update", response_model=Optional[FunctionModel])
+async def update_toolkit_by_id(
+    request: Request, id: str, form_data: FunctionForm, user=Depends(get_admin_user)
+):
+    function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
+
+    try:
+        with open(function_path, "w") as function_file:
+            function_file.write(form_data.content)
+
+        function_module, function_type = load_function_module_by_id(id)
+
+        FUNCTIONS = request.app.state.FUNCTIONS
+        FUNCTIONS[id] = function_module
+
+        updated = {**form_data.model_dump(exclude={"id"}), "type": function_type}
+        print(updated)
+
+        function = Functions.update_function_by_id(id, updated)
+
+        if function:
+            return function
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
+            )
+
+    except Exception as e:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.DEFAULT(e),
+        )
+
+
+############################
+# DeleteFunctionById
+############################
+
+
+@router.delete("/id/{id}/delete", response_model=bool)
+async def delete_function_by_id(
+    request: Request, id: str, user=Depends(get_admin_user)
+):
+    result = Functions.delete_function_by_id(id)
+
+    if result:
+        FUNCTIONS = request.app.state.FUNCTIONS
+        if id in FUNCTIONS:
+            del FUNCTIONS[id]
+
+        # delete the function file
+        function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
+        os.remove(function_path)
+
+    return result

+ 5 - 1
backend/apps/webui/routers/tools.py

@@ -15,8 +15,9 @@ from constants import ERROR_MESSAGES
 
 from importlib import util
 import os
+from pathlib import Path
 
-from config import DATA_DIR
+from config import DATA_DIR, CACHE_DIR
 
 
 TOOLS_DIR = f"{DATA_DIR}/tools"
@@ -79,6 +80,9 @@ async def create_new_toolkit(
             specs = get_tools_specs(TOOLS[form_data.id])
             toolkit = Tools.insert_new_tool(user.id, form_data, specs)
 
+            tool_cache_dir = Path(CACHE_DIR) / "tools" / form_data.id
+            tool_cache_dir.mkdir(parents=True, exist_ok=True)
+
             if toolkit:
                 return toolkit
             else:

+ 46 - 0
backend/apps/webui/routers/users.py

@@ -115,6 +115,52 @@ async def update_user_settings_by_session_user(
         )
 
 
+############################
+# GetUserInfoBySessionUser
+############################
+
+
+@router.get("/user/info", response_model=Optional[dict])
+async def get_user_info_by_session_user(user=Depends(get_verified_user)):
+    user = Users.get_user_by_id(user.id)
+    if user:
+        return user.info
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.USER_NOT_FOUND,
+        )
+
+
+############################
+# UpdateUserInfoBySessionUser
+############################
+
+
+@router.post("/user/info/update", response_model=Optional[dict])
+async def update_user_settings_by_session_user(
+    form_data: dict, user=Depends(get_verified_user)
+):
+    user = Users.get_user_by_id(user.id)
+    if user:
+        if user.info is None:
+            user.info = {}
+
+        user = Users.update_user_by_id(user.id, {"info": {**user.info, **form_data}})
+        if user:
+            return user.info
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.USER_NOT_FOUND,
+            )
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.USER_NOT_FOUND,
+        )
+
+
 ############################
 # GetUserById
 ############################

+ 23 - 1
backend/apps/webui/utils.py

@@ -1,7 +1,7 @@
 from importlib import util
 import os
 
-from config import TOOLS_DIR
+from config import TOOLS_DIR, FUNCTIONS_DIR
 
 
 def load_toolkit_module_by_id(toolkit_id):
@@ -21,3 +21,25 @@ def load_toolkit_module_by_id(toolkit_id):
         # Move the file to the error folder
         os.rename(toolkit_path, f"{toolkit_path}.error")
         raise e
+
+
+def load_function_module_by_id(function_id):
+    function_path = os.path.join(FUNCTIONS_DIR, f"{function_id}.py")
+
+    spec = util.spec_from_file_location(function_id, function_path)
+    module = util.module_from_spec(spec)
+
+    try:
+        spec.loader.exec_module(module)
+        print(f"Loaded module: {module.__name__}")
+        if hasattr(module, "Pipe"):
+            return module.Pipe(), "pipe"
+        elif hasattr(module, "Filter"):
+            return module.Filter(), "filter"
+        else:
+            raise Exception("No Function class found")
+    except Exception as e:
+        print(f"Error loading module: {function_id}")
+        # Move the file to the error folder
+        os.rename(function_path, f"{function_path}.error")
+        raise e

+ 53 - 1
backend/config.py

@@ -294,6 +294,7 @@ WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
 WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
     "WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
 )
+WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
 JWT_EXPIRES_IN = PersistentConfig(
     "JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
 )
@@ -505,6 +506,14 @@ TOOLS_DIR = os.getenv("TOOLS_DIR", f"{DATA_DIR}/tools")
 Path(TOOLS_DIR).mkdir(parents=True, exist_ok=True)
 
 
+####################################
+# Functions DIR
+####################################
+
+FUNCTIONS_DIR = os.getenv("FUNCTIONS_DIR", f"{DATA_DIR}/functions")
+Path(FUNCTIONS_DIR).mkdir(parents=True, exist_ok=True)
+
+
 ####################################
 # LITELLM_CONFIG
 ####################################
@@ -554,7 +563,14 @@ OLLAMA_API_BASE_URL = os.environ.get(
 )
 
 OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
-AIOHTTP_CLIENT_TIMEOUT = int(os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "300"))
+AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "300")
+
+if AIOHTTP_CLIENT_TIMEOUT == "":
+    AIOHTTP_CLIENT_TIMEOUT = None
+else:
+    AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
+
+
 K8S_FLAG = os.environ.get("K8S_FLAG", "")
 USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false")
 
@@ -1034,6 +1050,18 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig(
     os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
 )
 
+# You can provide a list of your own websites to filter after performing a web search.
+# This ensures the highest level of safety and reliability of the information sources.
+RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
+    "RAG_WEB_SEARCH_DOMAIN_FILTER_LIST",
+    "rag.rag.web.search.domain.filter_list",
+    [
+        # "wikipedia.com",
+        # "wikimedia.org",
+        # "wikidata.org",
+    ],
+)
+
 SEARXNG_QUERY_URL = PersistentConfig(
     "SEARXNG_QUERY_URL",
     "rag.web.search.searxng_query_url",
@@ -1139,6 +1167,30 @@ COMFYUI_BASE_URL = PersistentConfig(
     os.getenv("COMFYUI_BASE_URL", ""),
 )
 
+COMFYUI_CFG_SCALE = PersistentConfig(
+    "COMFYUI_CFG_SCALE",
+    "image_generation.comfyui.cfg_scale",
+    os.getenv("COMFYUI_CFG_SCALE", ""),
+)
+
+COMFYUI_SAMPLER = PersistentConfig(
+    "COMFYUI_SAMPLER",
+    "image_generation.comfyui.sampler",
+    os.getenv("COMFYUI_SAMPLER", ""),
+)
+
+COMFYUI_SCHEDULER = PersistentConfig(
+    "COMFYUI_SCHEDULER",
+    "image_generation.comfyui.scheduler",
+    os.getenv("COMFYUI_SCHEDULER", ""),
+)
+
+COMFYUI_SD3 = PersistentConfig(
+    "COMFYUI_SD3",
+    "image_generation.comfyui.sd3",
+    os.environ.get("COMFYUI_SD3", "").lower() == "true",
+)
+
 IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
     "IMAGES_OPENAI_API_BASE_URL",
     "image_generation.openai.api_base_url",

+ 512 - 186
backend/main.py

@@ -15,9 +15,11 @@ import requests
 import mimetypes
 import shutil
 import os
+import uuid
 import inspect
 import asyncio
 
+from fastapi.concurrency import run_in_threadpool
 from fastapi import FastAPI, Request, Depends, status, UploadFile, File, Form
 from fastapi.staticfiles import StaticFiles
 from fastapi.responses import JSONResponse
@@ -46,16 +48,19 @@ from apps.openai.main import (
 from apps.audio.main import app as audio_app
 from apps.images.main import app as images_app
 from apps.rag.main import app as rag_app
-from apps.webui.main import app as webui_app
+from apps.webui.main import app as webui_app, get_pipe_models
 
 
 from pydantic import BaseModel
-from typing import List, Optional
+from typing import List, Optional, Iterator, Generator, Union
 
 from apps.webui.models.auths import Auths
 from apps.webui.models.models import Models, ModelModel
 from apps.webui.models.tools import Tools
+from apps.webui.models.functions import Functions
 from apps.webui.models.users import Users
+
+from apps.webui.utils import load_toolkit_module_by_id, load_function_module_by_id
 from apps.webui.utils import load_toolkit_module_by_id
 
 from utils.misc import parse_duration
@@ -72,7 +77,11 @@ from utils.task import (
     search_query_generation_template,
     tools_function_calling_generation_template,
 )
-from utils.misc import get_last_user_message, add_or_update_system_message
+from utils.misc import (
+    get_last_user_message,
+    add_or_update_system_message,
+    stream_message_template,
+)
 
 from apps.rag.utils import get_rag_context, rag_template
 
@@ -85,6 +94,7 @@ from config import (
     VERSION,
     CHANGELOG,
     FRONTEND_BUILD_DIR,
+    UPLOAD_DIR,
     CACHE_DIR,
     STATIC_DIR,
     ENABLE_OPENAI_API,
@@ -184,7 +194,16 @@ app.state.MODELS = {}
 origins = ["*"]
 
 
-async def get_function_call_response(messages, tool_id, template, task_model_id, user):
+##################################
+#
+# ChatCompletion Middleware
+#
+##################################
+
+
+async def get_function_call_response(
+    messages, files, tool_id, template, task_model_id, user
+):
     tool = Tools.get_tool_by_id(tool_id)
     tools_specs = json.dumps(tool.specs, indent=2)
     content = tools_function_calling_generation_template(template, tools_specs)
@@ -222,9 +241,7 @@ async def get_function_call_response(messages, tool_id, template, task_model_id,
     response = None
     try:
         if model["owned_by"] == "ollama":
-            response = await generate_ollama_chat_completion(
-                OpenAIChatCompletionForm(**payload), user=user
-            )
+            response = await generate_ollama_chat_completion(payload, user=user)
         else:
             response = await generate_openai_chat_completion(payload, user=user)
 
@@ -247,6 +264,7 @@ async def get_function_call_response(messages, tool_id, template, task_model_id,
             result = json.loads(content)
             print(result)
 
+            citation = None
             # Call the function
             if "name" in result:
                 if tool_id in webui_app.state.TOOLS:
@@ -255,76 +273,170 @@ async def get_function_call_response(messages, tool_id, template, task_model_id,
                     toolkit_module = load_toolkit_module_by_id(tool_id)
                     webui_app.state.TOOLS[tool_id] = toolkit_module
 
+                file_handler = False
+                # check if toolkit_module has file_handler self variable
+                if hasattr(toolkit_module, "file_handler"):
+                    file_handler = True
+                    print("file_handler: ", file_handler)
+
                 function = getattr(toolkit_module, result["name"])
                 function_result = None
                 try:
                     # Get the signature of the function
                     sig = inspect.signature(function)
-                    # Check if '__user__' is a parameter of the function
+                    params = result["parameters"]
+
                     if "__user__" in sig.parameters:
                         # Call the function with the '__user__' parameter included
-                        function_result = function(
-                            **{
-                                **result["parameters"],
-                                "__user__": {
-                                    "id": user.id,
-                                    "email": user.email,
-                                    "name": user.name,
-                                    "role": user.role,
-                                },
-                            }
-                        )
+                        params = {
+                            **params,
+                            "__user__": {
+                                "id": user.id,
+                                "email": user.email,
+                                "name": user.name,
+                                "role": user.role,
+                            },
+                        }
+
+                    if "__messages__" in sig.parameters:
+                        # Call the function with the '__messages__' parameter included
+                        params = {
+                            **params,
+                            "__messages__": messages,
+                        }
+
+                    if "__files__" in sig.parameters:
+                        # Call the function with the '__files__' parameter included
+                        params = {
+                            **params,
+                            "__files__": files,
+                        }
+
+                    if "__model__" in sig.parameters:
+                        # Call the function with the '__model__' parameter included
+                        params = {
+                            **params,
+                            "__model__": model,
+                        }
+
+                    if "__id__" in sig.parameters:
+                        # Call the function with the '__id__' parameter included
+                        params = {
+                            **params,
+                            "__id__": tool_id,
+                        }
+
+                    if inspect.iscoroutinefunction(function):
+                        function_result = await function(**params)
                     else:
-                        # Call the function without modifying the parameters
-                        function_result = function(**result["parameters"])
+                        function_result = function(**params)
+
+                    if hasattr(toolkit_module, "citation") and toolkit_module.citation:
+                        citation = {
+                            "source": {"name": f"TOOL:{tool.name}/{result['name']}"},
+                            "document": [function_result],
+                            "metadata": [{"source": result["name"]}],
+                        }
                 except Exception as e:
                     print(e)
 
                 # Add the function result to the system prompt
-                if function_result:
-                    return function_result
+                if function_result is not None:
+                    return function_result, citation, file_handler
     except Exception as e:
         print(f"Error: {e}")
 
-    return None
+    return None, None, False
 
 
 class ChatCompletionMiddleware(BaseHTTPMiddleware):
     async def dispatch(self, request: Request, call_next):
-        return_citations = False
+        data_items = []
 
-        if request.method == "POST" and (
-            "/ollama/api/chat" in request.url.path
-            or "/chat/completions" in request.url.path
+        show_citations = False
+        citations = []
+
+        if request.method == "POST" and any(
+            endpoint in request.url.path
+            for endpoint in ["/ollama/api/chat", "/chat/completions"]
         ):
             log.debug(f"request.url.path: {request.url.path}")
 
             # Read the original request body
             body = await request.body()
-            # Decode body to string
             body_str = body.decode("utf-8")
-            # Parse string to JSON
             data = json.loads(body_str) if body_str else {}
 
             user = get_current_user(
-                get_http_authorization_cred(request.headers.get("Authorization"))
+                request,
+                get_http_authorization_cred(request.headers.get("Authorization")),
             )
-
-            # Remove the citations from the body
-            return_citations = data.get("citations", False)
-            if "citations" in data:
+            # Flag to skip RAG completions if file_handler is present in tools/functions
+            skip_files = False
+            if data.get("citations"):
+                show_citations = True
                 del data["citations"]
 
-            # Set the task model
-            task_model_id = data["model"]
-            if task_model_id not in app.state.MODELS:
+            model_id = data["model"]
+            if model_id not in app.state.MODELS:
                 raise HTTPException(
                     status_code=status.HTTP_404_NOT_FOUND,
                     detail="Model not found",
                 )
+            model = app.state.MODELS[model_id]
+
+            # Check if the model has any filters
+            if "info" in model and "meta" in model["info"]:
+                for filter_id in model["info"]["meta"].get("filterIds", []):
+                    filter = Functions.get_function_by_id(filter_id)
+                    if filter:
+                        if filter_id in webui_app.state.FUNCTIONS:
+                            function_module = webui_app.state.FUNCTIONS[filter_id]
+                        else:
+                            function_module, function_type = load_function_module_by_id(
+                                filter_id
+                            )
+                            webui_app.state.FUNCTIONS[filter_id] = function_module
+
+                        # Check if the function has a file_handler variable
+                        if hasattr(function_module, "file_handler"):
+                            skip_files = function_module.file_handler
+
+                        try:
+                            if hasattr(function_module, "inlet"):
+                                inlet = function_module.inlet
+
+                                if inspect.iscoroutinefunction(inlet):
+                                    data = await inlet(
+                                        data,
+                                        {
+                                            "id": user.id,
+                                            "email": user.email,
+                                            "name": user.name,
+                                            "role": user.role,
+                                        },
+                                    )
+                                else:
+                                    data = inlet(
+                                        data,
+                                        {
+                                            "id": user.id,
+                                            "email": user.email,
+                                            "name": user.name,
+                                            "role": user.role,
+                                        },
+                                    )
+
+                        except Exception as e:
+                            print(f"Error: {e}")
+                            return JSONResponse(
+                                status_code=status.HTTP_400_BAD_REQUEST,
+                                content={"detail": str(e)},
+                            )
 
-            # Check if the user has a custom task model
-            # If the user has a custom task model, use that model
+            # Set the task model
+            task_model_id = data["model"]
+            # Check if the user has a custom task model and use that model
             if app.state.MODELS[task_model_id]["owned_by"] == "ollama":
                 if (
                     app.state.config.TASK_MODEL
@@ -347,55 +459,71 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
                 for tool_id in data["tool_ids"]:
                     print(tool_id)
                     try:
-                        response = await get_function_call_response(
-                            messages=data["messages"],
-                            tool_id=tool_id,
-                            template=app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
-                            task_model_id=task_model_id,
-                            user=user,
+                        response, citation, file_handler = (
+                            await get_function_call_response(
+                                messages=data["messages"],
+                                files=data.get("files", []),
+                                tool_id=tool_id,
+                                template=app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
+                                task_model_id=task_model_id,
+                                user=user,
+                            )
                         )
 
-                        if response:
+                        print(file_handler)
+                        if isinstance(response, str):
                             context += ("\n" if context != "" else "") + response
+
+                        if citation:
+                            citations.append(citation)
+                            show_citations = True
+
+                        if file_handler:
+                            skip_files = True
+
                     except Exception as e:
                         print(f"Error: {e}")
                 del data["tool_ids"]
 
                 print(f"tool_context: {context}")
 
-            # If docs field is present, generate RAG completions
-            if "docs" in data:
-                data = {**data}
-                rag_context, citations = get_rag_context(
-                    docs=data["docs"],
-                    messages=data["messages"],
-                    embedding_function=rag_app.state.EMBEDDING_FUNCTION,
-                    k=rag_app.state.config.TOP_K,
-                    reranking_function=rag_app.state.sentence_transformer_rf,
-                    r=rag_app.state.config.RELEVANCE_THRESHOLD,
-                    hybrid_search=rag_app.state.config.ENABLE_RAG_HYBRID_SEARCH,
-                )
+            # If files field is present, generate RAG completions
+            # If skip_files is True, skip the RAG completions
+            if "files" in data:
+                if not skip_files:
+                    data = {**data}
+                    rag_context, rag_citations = get_rag_context(
+                        files=data["files"],
+                        messages=data["messages"],
+                        embedding_function=rag_app.state.EMBEDDING_FUNCTION,
+                        k=rag_app.state.config.TOP_K,
+                        reranking_function=rag_app.state.sentence_transformer_rf,
+                        r=rag_app.state.config.RELEVANCE_THRESHOLD,
+                        hybrid_search=rag_app.state.config.ENABLE_RAG_HYBRID_SEARCH,
+                    )
+                    if rag_context:
+                        context += ("\n" if context != "" else "") + rag_context
+
+                    log.debug(f"rag_context: {rag_context}, citations: {citations}")
 
-                if rag_context:
-                    context += ("\n" if context != "" else "") + rag_context
+                    if rag_citations:
+                        citations.extend(rag_citations)
 
-                del data["docs"]
+                del data["files"]
 
-                log.debug(f"rag_context: {rag_context}, citations: {citations}")
+            if show_citations and len(citations) > 0:
+                data_items.append({"citations": citations})
 
             if context != "":
                 system_prompt = rag_template(
                     rag_app.state.config.RAG_TEMPLATE, context, prompt
                 )
-
                 print(system_prompt)
-
                 data["messages"] = add_or_update_system_message(
-                    f"\n{system_prompt}", data["messages"]
+                    system_prompt, data["messages"]
                 )
 
             modified_body_bytes = json.dumps(data).encode("utf-8")
-
             # Replace the request body with the modified one
             request._body = modified_body_bytes
             # Set custom header to ensure content-length matches new body length
@@ -408,43 +536,54 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
                 ],
             ]
 
-        response = await call_next(request)
-
-        if return_citations:
-            # Inject the citations into the response
+            response = await call_next(request)
             if isinstance(response, StreamingResponse):
                 # If it's a streaming response, inject it as SSE event or NDJSON line
                 content_type = response.headers.get("Content-Type")
                 if "text/event-stream" in content_type:
                     return StreamingResponse(
-                        self.openai_stream_wrapper(response.body_iterator, citations),
+                        self.openai_stream_wrapper(response.body_iterator, data_items),
                     )
                 if "application/x-ndjson" in content_type:
                     return StreamingResponse(
-                        self.ollama_stream_wrapper(response.body_iterator, citations),
+                        self.ollama_stream_wrapper(response.body_iterator, data_items),
                     )
+            else:
+                return response
 
+        # If it's not a chat completion request, just pass it through
+        response = await call_next(request)
         return response
 
     async def _receive(self, body: bytes):
         return {"type": "http.request", "body": body, "more_body": False}
 
-    async def openai_stream_wrapper(self, original_generator, citations):
-        yield f"data: {json.dumps({'citations': citations})}\n\n"
+    async def openai_stream_wrapper(self, original_generator, data_items):
+        for item in data_items:
+            yield f"data: {json.dumps(item)}\n\n"
+
         async for data in original_generator:
             yield data
 
-    async def ollama_stream_wrapper(self, original_generator, citations):
-        yield f"{json.dumps({'citations': citations})}\n"
+    async def ollama_stream_wrapper(self, original_generator, data_items):
+        for item in data_items:
+            yield f"{json.dumps(item)}\n"
+
         async for data in original_generator:
             yield data
 
 
 app.add_middleware(ChatCompletionMiddleware)
 
+##################################
+#
+# Pipeline Middleware
+#
+##################################
+
 
 def filter_pipeline(payload, user):
-    user = {"id": user.id, "name": user.name, "role": user.role}
+    user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
     model_id = payload["model"]
     filters = [
         model
@@ -532,7 +671,8 @@ class PipelineMiddleware(BaseHTTPMiddleware):
             data = json.loads(body_str) if body_str else {}
 
             user = get_current_user(
-                get_http_authorization_cred(request.headers.get("Authorization"))
+                request,
+                get_http_authorization_cred(request.headers.get("Authorization")),
             )
 
             try:
@@ -600,7 +740,6 @@ async def update_embedding_function(request: Request, call_next):
 
 app.mount("/ws", socket_app)
 
-
 app.mount("/ollama", ollama_app)
 app.mount("/openai", openai_app)
 
@@ -614,17 +753,18 @@ webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION
 
 
 async def get_all_models():
+    pipe_models = []
     openai_models = []
     ollama_models = []
 
+    pipe_models = await get_pipe_models()
+
     if app.state.config.ENABLE_OPENAI_API:
         openai_models = await get_openai_models()
-
         openai_models = openai_models["data"]
 
     if app.state.config.ENABLE_OLLAMA_API:
         ollama_models = await get_ollama_models()
-
         ollama_models = [
             {
                 "id": model["model"],
@@ -637,9 +777,9 @@ async def get_all_models():
             for model in ollama_models["models"]
         ]
 
-    models = openai_models + ollama_models
-    custom_models = Models.get_all_models()
+    models = pipe_models + openai_models + ollama_models
 
+    custom_models = Models.get_all_models()
     for custom_model in custom_models:
         if custom_model.base_model_id == None:
             for model in models:
@@ -702,6 +842,253 @@ async def get_models(user=Depends(get_verified_user)):
     return {"data": models}
 
 
+@app.post("/api/chat/completions")
+async def generate_chat_completions(form_data: dict, user=Depends(get_verified_user)):
+    model_id = form_data["model"]
+    if model_id not in app.state.MODELS:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail="Model not found",
+        )
+
+    model = app.state.MODELS[model_id]
+    print(model)
+
+    pipe = model.get("pipe")
+    if pipe:
+        form_data["user"] = {
+            "id": user.id,
+            "email": user.email,
+            "name": user.name,
+            "role": user.role,
+        }
+
+        async def job():
+            pipe_id = form_data["model"]
+            if "." in pipe_id:
+                pipe_id, sub_pipe_id = pipe_id.split(".", 1)
+            print(pipe_id)
+
+            pipe = webui_app.state.FUNCTIONS[pipe_id].pipe
+            if form_data["stream"]:
+
+                async def stream_content():
+                    if inspect.iscoroutinefunction(pipe):
+                        res = await pipe(body=form_data)
+                    else:
+                        res = pipe(body=form_data)
+
+                    if isinstance(res, str):
+                        message = stream_message_template(form_data["model"], res)
+                        yield f"data: {json.dumps(message)}\n\n"
+
+                    if isinstance(res, Iterator):
+                        for line in res:
+                            if isinstance(line, BaseModel):
+                                line = line.model_dump_json()
+                                line = f"data: {line}"
+                            try:
+                                line = line.decode("utf-8")
+                            except:
+                                pass
+
+                            if line.startswith("data:"):
+                                yield f"{line}\n\n"
+                            else:
+                                line = stream_message_template(form_data["model"], line)
+                                yield f"data: {json.dumps(line)}\n\n"
+
+                    if isinstance(res, str) or isinstance(res, Generator):
+                        finish_message = {
+                            "id": f"{form_data['model']}-{str(uuid.uuid4())}",
+                            "object": "chat.completion.chunk",
+                            "created": int(time.time()),
+                            "model": form_data["model"],
+                            "choices": [
+                                {
+                                    "index": 0,
+                                    "delta": {},
+                                    "logprobs": None,
+                                    "finish_reason": "stop",
+                                }
+                            ],
+                        }
+
+                        yield f"data: {json.dumps(finish_message)}\n\n"
+                        yield f"data: [DONE]"
+
+                return StreamingResponse(
+                    stream_content(), media_type="text/event-stream"
+                )
+            else:
+                if inspect.iscoroutinefunction(pipe):
+                    res = await pipe(body=form_data)
+                else:
+                    res = pipe(body=form_data)
+
+                if isinstance(res, dict):
+                    return res
+                elif isinstance(res, BaseModel):
+                    return res.model_dump()
+                else:
+                    message = ""
+                    if isinstance(res, str):
+                        message = res
+                    if isinstance(res, Generator):
+                        for stream in res:
+                            message = f"{message}{stream}"
+
+                    return {
+                        "id": f"{form_data['model']}-{str(uuid.uuid4())}",
+                        "object": "chat.completion",
+                        "created": int(time.time()),
+                        "model": form_data["model"],
+                        "choices": [
+                            {
+                                "index": 0,
+                                "message": {
+                                    "role": "assistant",
+                                    "content": message,
+                                },
+                                "logprobs": None,
+                                "finish_reason": "stop",
+                            }
+                        ],
+                    }
+
+        return await job()
+    if model["owned_by"] == "ollama":
+        return await generate_ollama_chat_completion(form_data, user=user)
+    else:
+        return await generate_openai_chat_completion(form_data, user=user)
+
+
+@app.post("/api/chat/completed")
+async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
+    data = form_data
+    model_id = data["model"]
+    if model_id not in app.state.MODELS:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail="Model not found",
+        )
+    model = app.state.MODELS[model_id]
+
+    filters = [
+        model
+        for model in app.state.MODELS.values()
+        if "pipeline" in model
+        and "type" in model["pipeline"]
+        and model["pipeline"]["type"] == "filter"
+        and (
+            model["pipeline"]["pipelines"] == ["*"]
+            or any(
+                model_id == target_model_id
+                for target_model_id in model["pipeline"]["pipelines"]
+            )
+        )
+    ]
+
+    sorted_filters = sorted(filters, key=lambda x: x["pipeline"]["priority"])
+    if "pipeline" in model:
+        sorted_filters = [model] + sorted_filters
+
+    for filter in sorted_filters:
+        r = None
+        try:
+            urlIdx = filter["urlIdx"]
+
+            url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
+            key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
+
+            if key != "":
+                headers = {"Authorization": f"Bearer {key}"}
+                r = requests.post(
+                    f"{url}/{filter['id']}/filter/outlet",
+                    headers=headers,
+                    json={
+                        "user": {"id": user.id, "name": user.name, "role": user.role},
+                        "body": data,
+                    },
+                )
+
+                r.raise_for_status()
+                data = r.json()
+        except Exception as e:
+            # Handle connection error here
+            print(f"Connection error: {e}")
+
+            if r is not None:
+                try:
+                    res = r.json()
+                    if "detail" in res:
+                        return JSONResponse(
+                            status_code=r.status_code,
+                            content=res,
+                        )
+                except:
+                    pass
+
+            else:
+                pass
+
+    # Check if the model has any filters
+    if "info" in model and "meta" in model["info"]:
+        for filter_id in model["info"]["meta"].get("filterIds", []):
+            filter = Functions.get_function_by_id(filter_id)
+            if filter:
+                if filter_id in webui_app.state.FUNCTIONS:
+                    function_module = webui_app.state.FUNCTIONS[filter_id]
+                else:
+                    function_module, function_type = load_function_module_by_id(
+                        filter_id
+                    )
+                    webui_app.state.FUNCTIONS[filter_id] = function_module
+
+                try:
+                    if hasattr(function_module, "outlet"):
+                        outlet = function_module.outlet
+                        if inspect.iscoroutinefunction(outlet):
+                            data = await outlet(
+                                data,
+                                {
+                                    "id": user.id,
+                                    "email": user.email,
+                                    "name": user.name,
+                                    "role": user.role,
+                                },
+                            )
+                        else:
+                            data = outlet(
+                                data,
+                                {
+                                    "id": user.id,
+                                    "email": user.email,
+                                    "name": user.name,
+                                    "role": user.role,
+                                },
+                            )
+
+                except Exception as e:
+                    print(f"Error: {e}")
+                    return JSONResponse(
+                        status_code=status.HTTP_400_BAD_REQUEST,
+                        content={"detail": str(e)},
+                    )
+
+    return data
+
+
+##################################
+#
+# Task Endpoints
+#
+##################################
+
+
+# TODO: Refactor task API endpoints below into a separate file
+
+
 @app.get("/api/task/config")
 async def get_task_config(user=Depends(get_verified_user)):
     return {
@@ -780,7 +1167,12 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
     template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
 
     content = title_generation_template(
-        template, form_data["prompt"], user.model_dump()
+        template,
+        form_data["prompt"],
+        {
+            "name": user.name,
+            "location": user.info.get("location") if user.info else None,
+        },
     )
 
     payload = {
@@ -792,7 +1184,7 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
         "title": True,
     }
 
-    print(payload)
+    log.debug(payload)
 
     try:
         payload = filter_pipeline(payload, user)
@@ -803,9 +1195,7 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
         )
 
     if model["owned_by"] == "ollama":
-        return await generate_ollama_chat_completion(
-            OpenAIChatCompletionForm(**payload), user=user
-        )
+        return await generate_ollama_chat_completion(payload, user=user)
     else:
         return await generate_openai_chat_completion(payload, user=user)
 
@@ -846,7 +1236,7 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
     template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
 
     content = search_query_generation_template(
-        template, form_data["prompt"], user.model_dump()
+        template, form_data["prompt"], {"name": user.name}
     )
 
     payload = {
@@ -868,9 +1258,7 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
         )
 
     if model["owned_by"] == "ollama":
-        return await generate_ollama_chat_completion(
-            OpenAIChatCompletionForm(**payload), user=user
-        )
+        return await generate_ollama_chat_completion(payload, user=user)
     else:
         return await generate_openai_chat_completion(payload, user=user)
 
@@ -909,7 +1297,12 @@ Message: """{{prompt}}"""
 '''
 
     content = title_generation_template(
-        template, form_data["prompt"], user.model_dump()
+        template,
+        form_data["prompt"],
+        {
+            "name": user.name,
+            "location": user.info.get("location") if user.info else None,
+        },
     )
 
     payload = {
@@ -921,7 +1314,7 @@ Message: """{{prompt}}"""
         "task": True,
     }
 
-    print(payload)
+    log.debug(payload)
 
     try:
         payload = filter_pipeline(payload, user)
@@ -932,9 +1325,7 @@ Message: """{{prompt}}"""
         )
 
     if model["owned_by"] == "ollama":
-        return await generate_ollama_chat_completion(
-            OpenAIChatCompletionForm(**payload), user=user
-        )
+        return await generate_ollama_chat_completion(payload, user=user)
     else:
         return await generate_openai_chat_completion(payload, user=user)
 
@@ -967,8 +1358,13 @@ async def get_tools_function_calling(form_data: dict, user=Depends(get_verified_
     template = app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
 
     try:
-        context = await get_function_call_response(
-            form_data["messages"], form_data["tool_id"], template, model_id, user
+        context, citation, file_handler = await get_function_call_response(
+            form_data["messages"],
+            form_data.get("files", []),
+            form_data["tool_id"],
+            template,
+            model_id,
+            user,
         )
         return context
     except Exception as e:
@@ -978,94 +1374,14 @@ async def get_tools_function_calling(form_data: dict, user=Depends(get_verified_
         )
 
 
-@app.post("/api/chat/completions")
-async def generate_chat_completions(form_data: dict, user=Depends(get_verified_user)):
-    model_id = form_data["model"]
-    if model_id not in app.state.MODELS:
-        raise HTTPException(
-            status_code=status.HTTP_404_NOT_FOUND,
-            detail="Model not found",
-        )
-
-    model = app.state.MODELS[model_id]
-    print(model)
+##################################
+#
+# Pipelines Endpoints
+#
+##################################
 
-    if model["owned_by"] == "ollama":
-        return await generate_ollama_chat_completion(
-            OpenAIChatCompletionForm(**form_data), user=user
-        )
-    else:
-        return await generate_openai_chat_completion(form_data, user=user)
 
-
-@app.post("/api/chat/completed")
-async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
-    data = form_data
-    model_id = data["model"]
-
-    filters = [
-        model
-        for model in app.state.MODELS.values()
-        if "pipeline" in model
-        and "type" in model["pipeline"]
-        and model["pipeline"]["type"] == "filter"
-        and (
-            model["pipeline"]["pipelines"] == ["*"]
-            or any(
-                model_id == target_model_id
-                for target_model_id in model["pipeline"]["pipelines"]
-            )
-        )
-    ]
-    sorted_filters = sorted(filters, key=lambda x: x["pipeline"]["priority"])
-
-    print(model_id)
-
-    if model_id in app.state.MODELS:
-        model = app.state.MODELS[model_id]
-        if "pipeline" in model:
-            sorted_filters = [model] + sorted_filters
-
-    for filter in sorted_filters:
-        r = None
-        try:
-            urlIdx = filter["urlIdx"]
-
-            url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
-            key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
-
-            if key != "":
-                headers = {"Authorization": f"Bearer {key}"}
-                r = requests.post(
-                    f"{url}/{filter['id']}/filter/outlet",
-                    headers=headers,
-                    json={
-                        "user": {"id": user.id, "name": user.name, "role": user.role},
-                        "body": data,
-                    },
-                )
-
-                r.raise_for_status()
-                data = r.json()
-        except Exception as e:
-            # Handle connection error here
-            print(f"Connection error: {e}")
-
-            if r is not None:
-                try:
-                    res = r.json()
-                    if "detail" in res:
-                        return JSONResponse(
-                            status_code=r.status_code,
-                            content=res,
-                        )
-                except:
-                    pass
-
-            else:
-                pass
-
-    return data
+# TODO: Refactor pipelines API endpoints below into a separate file
 
 
 @app.get("/api/pipelines/list")
@@ -1388,6 +1704,13 @@ async def update_pipeline_valves(
         )
 
 
+##################################
+#
+# Config Endpoints
+#
+##################################
+
+
 @app.get("/api/config")
 async def get_app_config():
     # Checking and Handling the Absence of 'ui' in CONFIG_DATA
@@ -1457,6 +1780,9 @@ async def update_model_filter_config(
     }
 
 
+# TODO: webhook endpoint should be under config endpoints
+
+
 @app.get("/api/webhook")
 async def get_webhook_url(user=Depends(get_admin_user)):
     return {

+ 35 - 1
backend/utils/misc.py

@@ -3,7 +3,9 @@ import hashlib
 import json
 import re
 from datetime import timedelta
-from typing import Optional, List
+from typing import Optional, List, Tuple
+import uuid
+import time
 
 
 def get_last_user_message(messages: List[dict]) -> str:
@@ -28,6 +30,21 @@ def get_last_assistant_message(messages: List[dict]) -> str:
     return None
 
 
+def get_system_message(messages: List[dict]) -> dict:
+    for message in messages:
+        if message["role"] == "system":
+            return message
+    return None
+
+
+def remove_system_message(messages: List[dict]) -> List[dict]:
+    return [message for message in messages if message["role"] != "system"]
+
+
+def pop_system_message(messages: List[dict]) -> Tuple[dict, List[dict]]:
+    return get_system_message(messages), remove_system_message(messages)
+
+
 def add_or_update_system_message(content: str, messages: List[dict]):
     """
     Adds a new system message at the beginning of the messages list
@@ -47,6 +64,23 @@ def add_or_update_system_message(content: str, messages: List[dict]):
     return messages
 
 
+def stream_message_template(model: str, message: str):
+    return {
+        "id": f"{model}-{str(uuid.uuid4())}",
+        "object": "chat.completion.chunk",
+        "created": int(time.time()),
+        "model": model,
+        "choices": [
+            {
+                "index": 0,
+                "delta": {"content": message},
+                "logprobs": None,
+                "finish_reason": None,
+            }
+        ],
+    }
+
+
 def get_gravatar_url(email):
     # Trim leading and trailing whitespace from
     # an email address and force all characters

+ 17 - 7
backend/utils/task.py

@@ -6,24 +6,34 @@ from typing import Optional
 
 
 def prompt_template(
-    template: str, user_name: str = None, current_location: str = None
+    template: str, user_name: str = None, user_location: str = None
 ) -> str:
     # Get the current date
     current_date = datetime.now()
 
     # Format the date to YYYY-MM-DD
     formatted_date = current_date.strftime("%Y-%m-%d")
+    formatted_time = current_date.strftime("%I:%M:%S %p")
 
-    # Replace {{CURRENT_DATE}} in the template with the formatted date
     template = template.replace("{{CURRENT_DATE}}", formatted_date)
+    template = template.replace("{{CURRENT_TIME}}", formatted_time)
+    template = template.replace(
+        "{{CURRENT_DATETIME}}", f"{formatted_date} {formatted_time}"
+    )
 
     if user_name:
         # Replace {{USER_NAME}} in the template with the user's name
         template = template.replace("{{USER_NAME}}", user_name)
+    else:
+        # Replace {{USER_NAME}} in the template with "Unknown"
+        template = template.replace("{{USER_NAME}}", "Unknown")
 
-    if current_location:
-        # Replace {{CURRENT_LOCATION}} in the template with the current location
-        template = template.replace("{{CURRENT_LOCATION}}", current_location)
+    if user_location:
+        # Replace {{USER_LOCATION}} in the template with the current location
+        template = template.replace("{{USER_LOCATION}}", user_location)
+    else:
+        # Replace {{USER_LOCATION}} in the template with "Unknown"
+        template = template.replace("{{USER_LOCATION}}", "Unknown")
 
     return template
 
@@ -61,7 +71,7 @@ def title_generation_template(
     template = prompt_template(
         template,
         **(
-            {"user_name": user.get("name"), "current_location": user.get("location")}
+            {"user_name": user.get("name"), "user_location": user.get("location")}
             if user
             else {}
         ),
@@ -104,7 +114,7 @@ def search_query_generation_template(
     template = prompt_template(
         template,
         **(
-            {"user_name": user.get("name"), "current_location": user.get("location")}
+            {"user_name": user.get("name"), "user_location": user.get("location")}
             if user
             else {}
         ),

+ 18 - 5
backend/utils/utils.py

@@ -1,5 +1,5 @@
 from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
-from fastapi import HTTPException, status, Depends
+from fastapi import HTTPException, status, Depends, Request
 
 from apps.webui.models.users import Users
 
@@ -24,7 +24,7 @@ ALGORITHM = "HS256"
 # Auth Utils
 ##############
 
-bearer_security = HTTPBearer()
+bearer_security = HTTPBearer(auto_error=False)
 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
 
 
@@ -75,13 +75,26 @@ def get_http_authorization_cred(auth_header: str):
 
 
 def get_current_user(
+    request: Request,
     auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
 ):
+    token = None
+
+    if auth_token is not None:
+        token = auth_token.credentials
+
+    if token is None and "token" in request.cookies:
+        token = request.cookies.get("token")
+
+    if token is None:
+        raise HTTPException(status_code=403, detail="Not authenticated")
+
     # auth by api key
-    if auth_token.credentials.startswith("sk-"):
-        return get_current_user_by_api_key(auth_token.credentials)
+    if token.startswith("sk-"):
+        return get_current_user_by_api_key(token)
+
     # auth by jwt token
-    data = decode_token(auth_token.credentials)
+    data = decode_token(token)
     if data != None and "id" in data:
         user = Users.get_user_by_id(data["id"])
         if user is None:

+ 40 - 40
package-lock.json

@@ -1,12 +1,12 @@
 {
 	"name": "open-webui",
-	"version": "0.3.4",
+	"version": "0.3.5",
 	"lockfileVersion": 3,
 	"requires": true,
 	"packages": {
 		"": {
 			"name": "open-webui",
-			"version": "0.3.4",
+			"version": "0.3.5",
 			"dependencies": {
 				"@codemirror/lang-javascript": "^6.2.2",
 				"@codemirror/lang-python": "^6.1.6",
@@ -16,6 +16,7 @@
 				"async": "^3.2.5",
 				"bits-ui": "^0.19.7",
 				"codemirror": "^6.0.1",
+				"crc-32": "^1.2.2",
 				"dayjs": "^1.11.10",
 				"eventsource-parser": "^1.1.2",
 				"file-saver": "^2.0.5",
@@ -28,11 +29,12 @@
 				"katex": "^0.16.9",
 				"marked": "^9.1.0",
 				"mermaid": "^10.9.1",
-				"pyodide": "^0.26.0-alpha.4",
-				"socket.io-client": "^4.7.5",
+				"pyodide": "^0.26.1",
+				"socket.io-client": "^4.2.0",
 				"sortablejs": "^1.15.2",
 				"svelte-sonner": "^0.3.19",
 				"tippy.js": "^6.3.7",
+				"turndown": "^7.2.0",
 				"uuid": "^9.0.1"
 			},
 			"devDependencies": {
@@ -999,6 +1001,11 @@
 				"svelte": ">=3 <5"
 			}
 		},
+		"node_modules/@mixmark-io/domino": {
+			"version": "2.2.0",
+			"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
+			"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="
+		},
 		"node_modules/@nodelib/fs.scandir": {
 			"version": "2.1.5",
 			"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2266,11 +2273,6 @@
 			"dev": true,
 			"optional": true
 		},
-		"node_modules/base-64": {
-			"version": "1.0.0",
-			"resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
-			"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="
-		},
 		"node_modules/base64-js": {
 			"version": "1.5.1",
 			"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3063,6 +3065,17 @@
 				"layout-base": "^1.0.0"
 			}
 		},
+		"node_modules/crc-32": {
+			"version": "1.2.2",
+			"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+			"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+			"bin": {
+				"crc32": "bin/crc32.njs"
+			},
+			"engines": {
+				"node": ">=0.8"
+			}
+		},
 		"node_modules/crelt": {
 			"version": "1.0.6",
 			"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
@@ -3984,37 +3997,17 @@
 			}
 		},
 		"node_modules/engine.io-client": {
-			"version": "6.5.3",
-			"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
-			"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
+			"version": "6.5.4",
+			"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
+			"integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==",
 			"dependencies": {
 				"@socket.io/component-emitter": "~3.1.0",
 				"debug": "~4.3.1",
 				"engine.io-parser": "~5.2.1",
-				"ws": "~8.11.0",
+				"ws": "~8.17.1",
 				"xmlhttprequest-ssl": "~2.0.0"
 			}
 		},
-		"node_modules/engine.io-client/node_modules/ws": {
-			"version": "8.11.0",
-			"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
-			"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
-			"engines": {
-				"node": ">=10.0.0"
-			},
-			"peerDependencies": {
-				"bufferutil": "^4.0.1",
-				"utf-8-validate": "^5.0.2"
-			},
-			"peerDependenciesMeta": {
-				"bufferutil": {
-					"optional": true
-				},
-				"utf-8-validate": {
-					"optional": true
-				}
-			}
-		},
 		"node_modules/engine.io-parser": {
 			"version": "5.2.2",
 			"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
@@ -7551,11 +7544,10 @@
 			}
 		},
 		"node_modules/pyodide": {
-			"version": "0.26.0-alpha.4",
-			"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.26.0-alpha.4.tgz",
-			"integrity": "sha512-Ixuczq99DwhQlE+Bt0RaS6Ln9MHSZOkbU6iN8azwaeorjHtr7ukaxh+FeTxViFrp2y+ITyKgmcobY+JnBPcULw==",
+			"version": "0.26.1",
+			"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.26.1.tgz",
+			"integrity": "sha512-P+Gm88nwZqY7uBgjbQH8CqqU6Ei/rDn7pS1t02sNZsbyLJMyE2OVXjgNuqVT3KqYWnyGREUN0DbBUCJqk8R0ew==",
 			"dependencies": {
-				"base-64": "^1.0.0",
 				"ws": "^8.5.0"
 			},
 			"engines": {
@@ -9065,6 +9057,14 @@
 				"node": "*"
 			}
 		},
+		"node_modules/turndown": {
+			"version": "7.2.0",
+			"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz",
+			"integrity": "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==",
+			"dependencies": {
+				"@mixmark-io/domino": "^2.2.0"
+			}
+		},
 		"node_modules/tweetnacl": {
 			"version": "0.14.5",
 			"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
@@ -10382,9 +10382,9 @@
 			"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
 		},
 		"node_modules/ws": {
-			"version": "8.17.0",
-			"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
-			"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
+			"version": "8.17.1",
+			"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+			"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
 			"engines": {
 				"node": ">=10.0.0"
 			},

+ 5 - 3
package.json

@@ -1,6 +1,6 @@
 {
 	"name": "open-webui",
-	"version": "0.3.4",
+	"version": "0.3.5",
 	"private": true,
 	"scripts": {
 		"dev": "npm run pyodide:fetch && vite dev --host",
@@ -56,6 +56,7 @@
 		"async": "^3.2.5",
 		"bits-ui": "^0.19.7",
 		"codemirror": "^6.0.1",
+		"crc-32": "^1.2.2",
 		"dayjs": "^1.11.10",
 		"eventsource-parser": "^1.1.2",
 		"file-saver": "^2.0.5",
@@ -68,11 +69,12 @@
 		"katex": "^0.16.9",
 		"marked": "^9.1.0",
 		"mermaid": "^10.9.1",
-		"pyodide": "^0.26.0-alpha.4",
-		"socket.io-client": "^4.7.5",
+		"pyodide": "^0.26.1",
+		"socket.io-client": "^4.2.0",
 		"sortablejs": "^1.15.2",
 		"svelte-sonner": "^0.3.19",
 		"tippy.js": "^6.3.7",
+		"turndown": "^7.2.0",
 		"uuid": "^9.0.1"
 	}
 }

+ 57 - 11
scripts/prepare-pyodide.js

@@ -1,4 +1,6 @@
 const packages = [
+	'micropip',
+	'packaging',
 	'requests',
 	'beautifulsoup4',
 	'numpy',
@@ -11,20 +13,64 @@ const packages = [
 ];
 
 import { loadPyodide } from 'pyodide';
-import { writeFile, copyFile, readdir } from 'fs/promises';
+import { writeFile, readFile, copyFile, readdir, rmdir } from 'fs/promises';
 
 async function downloadPackages() {
 	console.log('Setting up pyodide + micropip');
-	const pyodide = await loadPyodide({
-		packageCacheDir: 'static/pyodide'
-	});
-	await pyodide.loadPackage('micropip');
-	const micropip = pyodide.pyimport('micropip');
-	console.log('Downloading Pyodide packages:', packages);
-	await micropip.install(packages);
-	console.log('Pyodide packages downloaded, freezing into lock file');
-	const lockFile = await micropip.freeze();
-	await writeFile('static/pyodide/pyodide-lock.json', lockFile);
+
+	let pyodide;
+	try {
+		pyodide = await loadPyodide({
+			packageCacheDir: 'static/pyodide'
+		});
+	} catch (err) {
+		console.error('Failed to load Pyodide:', err);
+		return;
+	}
+
+	const packageJson = JSON.parse(await readFile('package.json'));
+	const pyodideVersion = packageJson.dependencies.pyodide.replace('^', '');
+
+	try {
+		const pyodidePackageJson = JSON.parse(await readFile('static/pyodide/package.json'));
+		const pyodidePackageVersion = pyodidePackageJson.version.replace('^', '');
+
+		if (pyodideVersion !== pyodidePackageVersion) {
+			console.log('Pyodide version mismatch, removing static/pyodide directory');
+			await rmdir('static/pyodide', { recursive: true });
+		}
+	} catch (e) {
+		console.log('Pyodide package not found, proceeding with download.');
+	}
+
+	try {
+		console.log('Loading micropip package');
+		await pyodide.loadPackage('micropip');
+
+		const micropip = pyodide.pyimport('micropip');
+		console.log('Downloading Pyodide packages:', packages);
+
+		try {
+			for (const pkg of packages) {
+				console.log(`Installing package: ${pkg}`);
+				await micropip.install(pkg);
+			}
+		} catch (err) {
+			console.error('Package installation failed:', err);
+			return;
+		}
+
+		console.log('Pyodide packages downloaded, freezing into lock file');
+
+		try {
+			const lockFile = await micropip.freeze();
+			await writeFile('static/pyodide/pyodide-lock.json', lockFile);
+		} catch (err) {
+			console.error('Failed to write lock file:', err);
+		}
+	} catch (err) {
+		console.error('Failed to load or install micropip:', err);
+	}
 }
 
 async function copyPyodide() {

+ 6 - 0
src/app.html

@@ -13,6 +13,12 @@
 			href="/opensearch.xml"
 		/>
 
+		<script>
+			function resizeIframe(obj) {
+				obj.style.height = obj.contentWindow.document.documentElement.scrollHeight + 'px';
+			}
+		</script>
+
 		<script>
 			// On page load or when changing themes, best to add inline in `head` to avoid FOUC
 			(() => {

+ 4 - 1
src/lib/apis/auths/index.ts

@@ -90,7 +90,8 @@ export const getSessionUser = async (token: string) => {
 		headers: {
 			'Content-Type': 'application/json',
 			Authorization: `Bearer ${token}`
-		}
+		},
+		credentials: 'include'
 	})
 		.then(async (res) => {
 			if (!res.ok) throw await res.json();
@@ -117,6 +118,7 @@ export const userSignIn = async (email: string, password: string) => {
 		headers: {
 			'Content-Type': 'application/json'
 		},
+		credentials: 'include',
 		body: JSON.stringify({
 			email: email,
 			password: password
@@ -153,6 +155,7 @@ export const userSignUp = async (
 		headers: {
 			'Content-Type': 'application/json'
 		},
+		credentials: 'include',
 		body: JSON.stringify({
 			name: name,
 			email: email,

+ 183 - 0
src/lib/apis/files/index.ts

@@ -0,0 +1,183 @@
+import { WEBUI_API_BASE_URL } from '$lib/constants';
+
+export const uploadFile = async (token: string, file: File) => {
+	const data = new FormData();
+	data.append('file', file);
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/files/`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: data
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const getFiles = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/files/`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const getFileById = async (token: string, id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const getFileContentById = async (id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}/content`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json'
+		},
+		credentials: 'include'
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return await res.blob();
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const deleteFileById = async (token: string, id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}`, {
+		method: 'DELETE',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const deleteAllFiles = async (token: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/files/all`, {
+		method: 'DELETE',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};

+ 193 - 0
src/lib/apis/functions/index.ts

@@ -0,0 +1,193 @@
+import { WEBUI_API_BASE_URL } from '$lib/constants';
+
+export const createNewFunction = async (token: string, func: object) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/functions/create`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			...func
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const getFunctions = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/functions/`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const exportFunctions = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/functions/export`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const getFunctionById = async (token: string, id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const updateFunctionById = async (token: string, id: string, func: object) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/update`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			...func
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const deleteFunctionById = async (token: string, id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/delete`, {
+		method: 'DELETE',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};

+ 31 - 0
src/lib/apis/rag/index.ts

@@ -164,6 +164,37 @@ export const updateQuerySettings = async (token: string, settings: QuerySettings
 	return res;
 };
 
+export const processDocToVectorDB = async (token: string, file_id: string) => {
+	let error = null;
+
+	const res = await fetch(`${RAG_API_BASE_URL}/process/doc`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			file_id: file_id
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
 export const uploadDocToVectorDB = async (token: string, collection_name: string, file: File) => {
 	const data = new FormData();
 	data.append('file', file);

+ 70 - 0
src/lib/apis/users/index.ts

@@ -1,4 +1,5 @@
 import { WEBUI_API_BASE_URL } from '$lib/constants';
+import { getUserPosition } from '$lib/utils';
 
 export const getUserPermissions = async (token: string) => {
 	let error = null;
@@ -198,6 +199,75 @@ export const getUserById = async (token: string, userId: string) => {
 	return res;
 };
 
+export const getUserInfo = async (token: string) => {
+	let error = null;
+	const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/info`, {
+		method: 'GET',
+		headers: {
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			error = err.detail;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const updateUserInfo = async (token: string, info: object) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/info/update`, {
+		method: 'POST',
+		headers: {
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			...info
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			error = err.detail;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const getAndUpdateUserLocation = async (token: string) => {
+	const location = await getUserPosition().catch((err) => {
+		throw err;
+	});
+
+	if (location) {
+		await updateUserInfo(token, { location: location });
+		return location;
+	} else {
+		throw new Error('Failed to get user location');
+	}
+};
+
 export const deleteUserById = async (token: string, userId: string) => {
 	let error = null;
 

+ 6 - 6
src/lib/components/admin/AddUserModal.svelte

@@ -153,7 +153,7 @@
 							type="button"
 							on:click={() => {
 								tab = '';
-							}}>Form</button
+							}}>{$i18n.t('Form')}</button
 						>
 
 						<button
@@ -161,7 +161,7 @@
 							type="button"
 							on:click={() => {
 								tab = 'import';
-							}}>CSV Import</button
+							}}>{$i18n.t('CSV Import')}</button
 						>
 					</div>
 					<div class="px-1">
@@ -176,9 +176,9 @@
 										placeholder={$i18n.t('Enter Your Role')}
 										required
 									>
-										<option value="pending"> pending </option>
-										<option value="user"> user </option>
-										<option value="admin"> admin </option>
+										<option value="pending"> {$i18n.t('pending')} </option>
+										<option value="user"> {$i18n.t('user')} </option>
+										<option value="admin"> {$i18n.t('admin')} </option>
 									</select>
 								</div>
 							</div>
@@ -262,7 +262,7 @@
 										class="underline dark:text-gray-200"
 										href="{WEBUI_BASE_URL}/static/user-import.csv"
 									>
-										Click here to download user import template file.
+										{$i18n.t('Click here to download user import template file.')}
 									</a>
 								</div>
 							</div>

+ 3 - 2
src/lib/components/admin/Settings/Documents.svelte

@@ -1,5 +1,6 @@
 <script lang="ts">
 	import { getDocs } from '$lib/apis/documents';
+	import { deleteAllFiles, deleteFileById } from '$lib/apis/files';
 	import {
 		getQuerySettings,
 		scanDocs,
@@ -217,8 +218,8 @@
 
 <ResetUploadDirConfirmDialog
 	bind:show={showResetUploadDirConfirm}
-	on:confirm={() => {
-		const res = resetUploadDir(localStorage.token).catch((error) => {
+	on:confirm={async () => {
+		const res = await deleteAllFiles(localStorage.token).catch((error) => {
 			toast.error(error);
 			return null;
 		});

+ 59 - 5
src/lib/components/admin/UserChatsModal.svelte

@@ -31,6 +31,17 @@
 			}
 		})();
 	}
+
+	let sortKey = 'updated_at'; // default sort key
+	let sortOrder = 'desc'; // default sort order
+	function setSortKey(key) {
+		if (sortKey === key) {
+			sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
+		} else {
+			sortKey = key;
+			sortOrder = 'asc';
+		}
+	}
 </script>
 
 <Modal size="lg" bind:show>
@@ -69,18 +80,56 @@
 									class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800"
 								>
 									<tr>
-										<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
-										<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created at')} </th>
+										<th
+											scope="col"
+											class="px-3 py-2 cursor-pointer select-none"
+											on:click={() => setSortKey('title')}
+										>
+											{$i18n.t('Title')}
+											{#if sortKey === 'title'}
+												{sortOrder === 'asc' ? '▲' : '▼'}
+											{:else}
+												<span class="invisible">▲</span>
+											{/if}
+										</th>
+										<th
+											scope="col"
+											class="px-3 py-2 cursor-pointer select-none"
+											on:click={() => setSortKey('created_at')}
+										>
+											{$i18n.t('Created at')}
+											{#if sortKey === 'created_at'}
+												{sortOrder === 'asc' ? '▲' : '▼'}
+											{:else}
+												<span class="invisible">▲</span>
+											{/if}
+										</th>
+										<th
+											scope="col"
+											class="px-3 py-2 hidden md:flex cursor-pointer select-none"
+											on:click={() => setSortKey('updated_at')}
+										>
+											{$i18n.t('Updated at')}
+											{#if sortKey === 'updated_at'}
+												{sortOrder === 'asc' ? '▲' : '▼'}
+											{:else}
+												<span class="invisible">▲</span>
+											{/if}
+										</th>
 										<th scope="col" class="px-3 py-2 text-right" />
 									</tr>
 								</thead>
 								<tbody>
-									{#each chats as chat, idx}
+									{#each chats.sort((a, b) => {
+										if (a[sortKey] < b[sortKey]) return sortOrder === 'asc' ? -1 : 1;
+										if (a[sortKey] > b[sortKey]) return sortOrder === 'asc' ? 1 : -1;
+										return 0;
+									}) as chat, idx}
 										<tr
 											class="bg-transparent {idx !== chats.length - 1 &&
 												'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs"
 										>
-											<td class="px-3 py-1 w-2/3">
+											<td class="px-3 py-1">
 												<a href="/s/{chat.id}" target="_blank">
 													<div class=" underline line-clamp-1">
 														{chat.title}
@@ -88,11 +137,16 @@
 												</a>
 											</td>
 
-											<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
+											<td class=" px-3 py-1 h-[2.5rem]">
 												<div class="my-auto">
 													{dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
 												</div>
 											</td>
+											<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
+												<div class="my-auto">
+													{dayjs(chat.updated_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
+												</div>
+											</td>
 
 											<td class="px-3 py-1 text-right">
 												<div class="flex justify-end w-full">

+ 71 - 41
src/lib/components/chat/Chat.svelte

@@ -31,6 +31,7 @@
 		convertMessagesToHistory,
 		copyToClipboard,
 		extractSentencesForAudio,
+		getUserPosition,
 		promptTemplate,
 		splitStream
 	} from '$lib/utils';
@@ -50,7 +51,7 @@
 	import { runWebSearch } from '$lib/apis/rag';
 	import { createOpenAITextStream } from '$lib/apis/streaming';
 	import { queryMemory } from '$lib/apis/memories';
-	import { getUserSettings } from '$lib/apis/users';
+	import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
 	import { chatCompleted, generateTitle, generateSearchQuery } from '$lib/apis';
 
 	import Banner from '../common/Banner.svelte';
@@ -272,11 +273,14 @@
 				id: m.id,
 				role: m.role,
 				content: m.content,
+				info: m.info ? m.info : undefined,
 				timestamp: m.timestamp
 			})),
 			chat_id: $chatId
 		}).catch((error) => {
-			console.error(error);
+			toast.error(error);
+			messages.at(-1).error = { content: error };
+
 			return null;
 		});
 
@@ -321,9 +325,16 @@
 		} else if (messages.length != 0 && messages.at(-1).done != true) {
 			// Response not done
 			console.log('wait');
+		} else if (messages.length != 0 && messages.at(-1).error) {
+			// Error in response
+			toast.error(
+				$i18n.t(
+					`Oops! There was an error in the previous response. Please try again or contact admin.`
+				)
+			);
 		} else if (
 			files.length > 0 &&
-			files.filter((file) => file.upload_status === false).length > 0
+			files.filter((file) => file.type !== 'image' && file.status !== 'processed').length > 0
 		) {
 			// Upload not done
 			toast.error(
@@ -533,7 +544,13 @@
 			$settings.system || (responseMessage?.userContext ?? null)
 				? {
 						role: 'system',
-						content: `${promptTemplate($settings?.system ?? '', $user.name)}${
+						content: `${promptTemplate(
+							$settings?.system ?? '',
+							$user.name,
+							$settings?.userLocation
+								? await getAndUpdateUserLocation(localStorage.token)
+								: undefined
+						)}${
 							responseMessage?.userContext ?? null
 								? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
 								: ''
@@ -578,23 +595,18 @@
 			}
 		});
 
-		let docs = [];
-
+		let files = [];
 		if (model?.info?.meta?.knowledge ?? false) {
-			docs = model.info.meta.knowledge;
+			files = model.info.meta.knowledge;
 		}
-
-		docs = [
-			...docs,
-			...messages
-				.filter((message) => message?.files ?? null)
-				.map((message) =>
-					message.files.filter((item) =>
-						['doc', 'collection', 'web_search_results'].includes(item.type)
-					)
-				)
-				.flat(1)
+		const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
+		files = [
+			...files,
+			...(lastUserMessage?.files?.filter((item) =>
+				['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
+			) ?? [])
 		].filter(
+			// Remove duplicates
 			(item, index, array) =>
 				array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
 		);
@@ -626,8 +638,8 @@
 			format: $settings.requestFormat ?? undefined,
 			keep_alive: $settings.keepAlive ?? undefined,
 			tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
-			docs: docs.length > 0 ? docs : undefined,
-			citations: docs.length > 0,
+			files: files.length > 0 ? files : undefined,
+			citations: files.length > 0 ? true : undefined,
 			chat_id: $chatId
 		});
 
@@ -823,23 +835,18 @@
 		let _response = null;
 		const responseMessage = history.messages[responseMessageId];
 
-		let docs = [];
-
+		let files = [];
 		if (model?.info?.meta?.knowledge ?? false) {
-			docs = model.info.meta.knowledge;
+			files = model.info.meta.knowledge;
 		}
-
-		docs = [
-			...docs,
-			...messages
-				.filter((message) => message?.files ?? null)
-				.map((message) =>
-					message.files.filter((item) =>
-						['doc', 'collection', 'web_search_results'].includes(item.type)
-					)
-				)
-				.flat(1)
+		const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
+		files = [
+			...files,
+			...(lastUserMessage?.files?.filter((item) =>
+				['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
+			) ?? [])
 		].filter(
+			// Remove duplicates
 			(item, index, array) =>
 				array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
 		);
@@ -871,7 +878,13 @@
 						$settings.system || (responseMessage?.userContext ?? null)
 							? {
 									role: 'system',
-									content: `${promptTemplate($settings?.system ?? '', $user.name)}${
+									content: `${promptTemplate(
+										$settings?.system ?? '',
+										$user.name,
+										$settings?.userLocation
+											? await getAndUpdateUserLocation(localStorage.token)
+											: undefined
+									)}${
 										responseMessage?.userContext ?? null
 											? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
 											: ''
@@ -923,11 +936,12 @@
 					frequency_penalty: $settings?.params?.frequency_penalty ?? undefined,
 					max_tokens: $settings?.params?.max_tokens ?? undefined,
 					tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
-					docs: docs.length > 0 ? docs : undefined,
-					citations: docs.length > 0,
+					files: files.length > 0 ? files : undefined,
+					citations: files.length > 0 ? true : undefined,
+
 					chat_id: $chatId
 				},
-				`${OPENAI_API_BASE_URL}`
+				`${WEBUI_BASE_URL}/api`
 			);
 
 			// Wait until history/message have been updated
@@ -1309,6 +1323,19 @@
 			? 'md:max-w-[calc(100%-260px)]'
 			: ''} w-full max-w-full flex flex-col"
 	>
+		{#if $settings?.backgroundImageUrl ?? null}
+			<div
+				class="absolute {$showSidebar
+					? 'md:max-w-[calc(100%-260px)] md:translate-x-[260px]'
+					: ''} top-0 left-0 w-full h-full bg-cover bg-center bg-no-repeat"
+				style="background-image: url({$settings.backgroundImageUrl})  "
+			/>
+
+			<div
+				class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-white to-white/85 dark:from-gray-900 dark:to-[#171717]/90 z-0"
+			/>
+		{/if}
+
 		<Navbar
 			{title}
 			bind:selectedModels
@@ -1320,7 +1347,9 @@
 
 		{#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1}
 			<div
-				class="absolute top-[4.25rem] w-full {$showSidebar ? 'md:max-w-[calc(100%-260px)]' : ''}"
+				class="absolute top-[4.25rem] w-full {$showSidebar
+					? 'md:max-w-[calc(100%-260px)]'
+					: ''} z-20"
 			>
 				<div class=" flex flex-col gap-1 w-full">
 					{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
@@ -1345,9 +1374,9 @@
 			</div>
 		{/if}
 
-		<div class="flex flex-col flex-auto">
+		<div class="flex flex-col flex-auto z-10">
 			<div
-				class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full"
+				class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10"
 				id="messages-container"
 				bind:this={messagesContainerElement}
 				on:scroll={(e) => {
@@ -1386,6 +1415,7 @@
 					}
 					return a;
 				}, [])}
+				transparentBackground={$settings?.backgroundImageUrl ?? false}
 				{selectedModels}
 				{messages}
 				{submitPrompt}

+ 81 - 66
src/lib/components/chat/MessageInput.svelte

@@ -15,11 +15,19 @@
 	import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
 
 	import {
+		processDocToVectorDB,
 		uploadDocToVectorDB,
 		uploadWebToVectorDB,
 		uploadYoutubeTranscriptionToVectorDB
 	} from '$lib/apis/rag';
-	import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS, WEBUI_BASE_URL } from '$lib/constants';
+
+	import { uploadFile } from '$lib/apis/files';
+	import {
+		SUPPORTED_FILE_TYPE,
+		SUPPORTED_FILE_EXTENSIONS,
+		WEBUI_BASE_URL,
+		WEBUI_API_BASE_URL
+	} from '$lib/constants';
 
 	import Prompts from './MessageInput/PromptCommands.svelte';
 	import Suggestions from './MessageInput/Suggestions.svelte';
@@ -35,6 +43,8 @@
 
 	const i18n = getContext('i18n');
 
+	export let transparentBackground = false;
+
 	export let submitPrompt: Function;
 	export let stopResponse: Function;
 
@@ -84,44 +94,75 @@
 		element.scrollTop = element.scrollHeight;
 	};
 
-	const uploadDoc = async (file) => {
+	const uploadFileHandler = async (file) => {
 		console.log(file);
+		// Check if the file is an audio file and transcribe/convert it to text file
+		if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
+			const res = await transcribeAudio(localStorage.token, file).catch((error) => {
+				toast.error(error);
+				return null;
+			});
 
-		const doc = {
-			type: 'doc',
-			name: file.name,
-			collection_name: '',
-			upload_status: false,
-			error: ''
-		};
-
-		try {
-			files = [...files, doc];
-
-			if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
-				const res = await transcribeAudio(localStorage.token, file).catch((error) => {
-					toast.error(error);
-					return null;
-				});
+			if (res) {
+				console.log(res);
+				const blob = new Blob([res.text], { type: 'text/plain' });
+				file = blobToFile(blob, `${file.name}.txt`);
+			}
+		}
 
-				if (res) {
-					console.log(res);
-					const blob = new Blob([res.text], { type: 'text/plain' });
-					file = blobToFile(blob, `${file.name}.txt`);
-				}
+		// Upload the file to the server
+		const uploadedFile = await uploadFile(localStorage.token, file).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (uploadedFile) {
+			const fileItem = {
+				type: 'file',
+				file: uploadedFile,
+				id: uploadedFile.id,
+				url: `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`,
+				name: file.name,
+				collection_name: '',
+				status: 'uploaded',
+				error: ''
+			};
+			files = [...files, fileItem];
+
+			// TODO: Check if tools & functions have files support to skip this step to delegate file processing
+			// Default Upload to VectorDB
+			if (
+				SUPPORTED_FILE_TYPE.includes(file['type']) ||
+				SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
+			) {
+				processFileItem(fileItem);
+			} else {
+				toast.error(
+					$i18n.t(`Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.`, {
+						file_type: file['type']
+					})
+				);
+				processFileItem(fileItem);
 			}
+		}
+	};
 
-			const res = await uploadDocToVectorDB(localStorage.token, '', file);
+	const processFileItem = async (fileItem) => {
+		try {
+			const res = await processDocToVectorDB(localStorage.token, fileItem.id);
 
 			if (res) {
-				doc.upload_status = true;
-				doc.collection_name = res.collection_name;
+				fileItem.status = 'processed';
+				fileItem.collection_name = res.collection_name;
 				files = files;
 			}
 		} catch (e) {
 			// Remove the failed doc from the files array
-			files = files.filter((f) => f.name !== file.name);
+			// files = files.filter((f) => f.id !== fileItem.id);
 			toast.error(e);
+
+			fileItem.status = 'processed';
+			files = files;
 		}
 	};
 
@@ -132,7 +173,7 @@
 			type: 'doc',
 			name: url,
 			collection_name: '',
-			upload_status: false,
+			status: false,
 			url: url,
 			error: ''
 		};
@@ -142,7 +183,7 @@
 			const res = await uploadWebToVectorDB(localStorage.token, '', url);
 
 			if (res) {
-				doc.upload_status = true;
+				doc.status = 'processed';
 				doc.collection_name = res.collection_name;
 				files = files;
 			}
@@ -160,7 +201,7 @@
 			type: 'doc',
 			name: url,
 			collection_name: '',
-			upload_status: false,
+			status: false,
 			url: url,
 			error: ''
 		};
@@ -170,7 +211,7 @@
 			const res = await uploadYoutubeTranscriptionToVectorDB(localStorage.token, url);
 
 			if (res) {
-				doc.upload_status = true;
+				doc.status = 'processed';
 				doc.collection_name = res.collection_name;
 				files = files;
 			}
@@ -228,19 +269,8 @@
 								];
 							};
 							reader.readAsDataURL(file);
-						} else if (
-							SUPPORTED_FILE_TYPE.includes(file['type']) ||
-							SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
-						) {
-							uploadDoc(file);
 						} else {
-							toast.error(
-								$i18n.t(
-									`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
-									{ file_type: file['type'] }
-								)
-							);
-							uploadDoc(file);
+							uploadFileHandler(file);
 						}
 					});
 				} else {
@@ -336,9 +366,9 @@
 							files = [
 								...files,
 								{
-									type: e?.detail?.type ?? 'doc',
+									type: e?.detail?.type ?? 'file',
 									...e.detail,
-									upload_status: true
+									status: 'processed'
 								}
 							];
 						}}
@@ -391,7 +421,7 @@
 		</div>
 	</div>
 
-	<div class="bg-white dark:bg-gray-900">
+	<div class="{transparentBackground ? 'bg-transparent' : 'bg-white dark:bg-gray-900'} ">
 		<div class="max-w-6xl px-2.5 md:px-6 mx-auto inset-x-0">
 			<div class=" pb-2">
 				<input
@@ -407,8 +437,6 @@
 								if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
 									if (visionCapableModels.length === 0) {
 										toast.error($i18n.t('Selected model(s) do not support image inputs'));
-										inputFiles = null;
-										filesInputElement.value = '';
 										return;
 									}
 									let reader = new FileReader();
@@ -420,30 +448,17 @@
 												url: `${event.target.result}`
 											}
 										];
-										inputFiles = null;
-										filesInputElement.value = '';
 									};
 									reader.readAsDataURL(file);
-								} else if (
-									SUPPORTED_FILE_TYPE.includes(file['type']) ||
-									SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
-								) {
-									uploadDoc(file);
-									filesInputElement.value = '';
 								} else {
-									toast.error(
-										$i18n.t(
-											`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
-											{ file_type: file['type'] }
-										)
-									);
-									uploadDoc(file);
-									filesInputElement.value = '';
+									uploadFileHandler(file);
 								}
 							});
 						} else {
 							toast.error($i18n.t(`File not found.`));
 						}
+
+						filesInputElement.value = '';
 					}}
 				/>
 
@@ -517,12 +532,12 @@
 														</Tooltip>
 													{/if}
 												</div>
-											{:else if file.type === 'doc'}
+											{:else if ['doc', 'file'].includes(file.type)}
 												<div
 													class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
 												>
 													<div class="p-2.5 bg-red-400 text-white rounded-lg">
-														{#if file.upload_status}
+														{#if file.status === 'processed'}
 															<svg
 																xmlns="http://www.w3.org/2000/svg"
 																viewBox="0 0 24 24"

+ 120 - 65
src/lib/components/chat/MessageInput/CallOverlay.svelte

@@ -1,5 +1,5 @@
 <script lang="ts">
-	import { config, settings, showCallOverlay } from '$lib/stores';
+	import { config, models, settings, showCallOverlay } from '$lib/stores';
 	import { onMount, tick, getContext } from 'svelte';
 
 	import {
@@ -28,9 +28,12 @@
 	export let chatId;
 	export let modelId;
 
+	let model = null;
+
 	let loading = false;
 	let confirmed = false;
 	let interrupted = false;
+	let assistantSpeaking = false;
 
 	let emoji = null;
 
@@ -268,6 +271,15 @@
 					return;
 				}
 
+				if (assistantSpeaking) {
+					// Mute the audio if the assistant is speaking
+					analyser.maxDecibels = 0;
+					analyser.minDecibels = -1;
+				} else {
+					analyser.minDecibels = MIN_DECIBELS;
+					analyser.maxDecibels = -30;
+				}
+
 				analyser.getByteTimeDomainData(timeDomainData);
 				analyser.getByteFrequencyData(domainData);
 
@@ -379,6 +391,7 @@
 	};
 
 	const stopAllAudio = async () => {
+		assistantSpeaking = false;
 		interrupted = true;
 
 		if (chatStreaming) {
@@ -485,6 +498,7 @@
 				}
 			} else if (finishedMessages[id] && messages[id] && messages[id].length === 0) {
 				// If the message is finished and there are no more messages to process, break the loop
+				assistantSpeaking = false;
 				break;
 			} else {
 				// No messages to process, sleep for a bit
@@ -495,6 +509,8 @@
 	};
 
 	onMount(async () => {
+		model = $models.find((m) => m.id === modelId);
+
 		startRecording();
 
 		const chatStartHandler = async (e) => {
@@ -511,6 +527,7 @@
 				}
 				audioAbortController = new AbortController();
 
+				assistantSpeaking = true;
 				// Start monitoring and playing audio for the message ID
 				monitorAndPlayAudio(id, audioAbortController.signal);
 			}
@@ -545,9 +562,9 @@
 		const chatFinishHandler = async (e) => {
 			const { id, content } = e.detail;
 			// "content" here is the entire message from the assistant
+			finishedMessages[id] = true;
 
 			chatStreaming = false;
-			finishedMessages[id] = true;
 		};
 
 		eventTarget.addEventListener('chat:start', chatStartHandler);
@@ -577,7 +594,15 @@
 		>
 			<div class="max-w-lg w-full h-screen max-h-[100dvh] flex flex-col justify-between p-3 md:p-6">
 				{#if camera}
-					<div class="flex justify-center items-center w-full h-20 min-h-20">
+					<button
+						type="button"
+						class="flex justify-center items-center w-full h-20 min-h-20"
+						on:click={() => {
+							if (assistantSpeaking) {
+								stopAllAudio();
+							}
+						}}
+					>
 						{#if emoji}
 							<div
 								class="  transition-all rounded-full"
@@ -591,7 +616,7 @@
 							>
 								{emoji}
 							</div>
-						{:else if loading}
+						{:else if loading || assistantSpeaking}
 							<svg
 								class="size-12 text-gray-900 dark:text-gray-400"
 								viewBox="0 0 24 24"
@@ -636,76 +661,97 @@
 									? ' size-16'
 									: rmsLevel * 100 > 1
 									? 'size-14'
-									: 'size-12'}  transition-all bg-black dark:bg-white rounded-full"
+									: 'size-12'}  transition-all rounded-full {(model?.info?.meta
+									?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
+									? ' bg-cover bg-center bg-no-repeat'
+									: 'bg-black dark:bg-white'}  bg-black dark:bg-white"
+								style={(model?.info?.meta?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
+									? `background-image: url('${model?.info?.meta?.profile_image_url}');`
+									: ''}
 							/>
 						{/if}
 						<!-- navbar -->
-					</div>
+					</button>
 				{/if}
 
 				<div class="flex justify-center items-center flex-1 h-full w-full max-h-full">
 					{#if !camera}
-						{#if emoji}
-							<div
-								class="  transition-all rounded-full"
-								style="font-size:{rmsLevel * 100 > 4
-									? '13'
-									: rmsLevel * 100 > 2
-									? '12'
-									: rmsLevel * 100 > 1
-									? '11.5'
-									: '11'}rem;width:100%;text-align:center;"
-							>
-								{emoji}
-							</div>
-						{:else if loading}
-							<svg
-								class="size-44 text-gray-900 dark:text-gray-400"
-								viewBox="0 0 24 24"
-								fill="currentColor"
-								xmlns="http://www.w3.org/2000/svg"
-								><style>
-									.spinner_qM83 {
-										animation: spinner_8HQG 1.05s infinite;
-									}
-									.spinner_oXPr {
-										animation-delay: 0.1s;
-									}
-									.spinner_ZTLf {
-										animation-delay: 0.2s;
-									}
-									@keyframes spinner_8HQG {
-										0%,
-										57.14% {
-											animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
-											transform: translate(0);
+						<button
+							type="button"
+							on:click={() => {
+								if (assistantSpeaking) {
+									stopAllAudio();
+								}
+							}}
+						>
+							{#if emoji}
+								<div
+									class="  transition-all rounded-full"
+									style="font-size:{rmsLevel * 100 > 4
+										? '13'
+										: rmsLevel * 100 > 2
+										? '12'
+										: rmsLevel * 100 > 1
+										? '11.5'
+										: '11'}rem;width:100%;text-align:center;"
+								>
+									{emoji}
+								</div>
+							{:else if loading || assistantSpeaking}
+								<svg
+									class="size-44 text-gray-900 dark:text-gray-400"
+									viewBox="0 0 24 24"
+									fill="currentColor"
+									xmlns="http://www.w3.org/2000/svg"
+									><style>
+										.spinner_qM83 {
+											animation: spinner_8HQG 1.05s infinite;
 										}
-										28.57% {
-											animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
-											transform: translateY(-6px);
+										.spinner_oXPr {
+											animation-delay: 0.1s;
 										}
-										100% {
-											transform: translate(0);
+										.spinner_ZTLf {
+											animation-delay: 0.2s;
 										}
-									}
-								</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
-									class="spinner_qM83 spinner_oXPr"
-									cx="12"
-									cy="12"
-									r="3"
-								/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
-							>
-						{:else}
-							<div
-								class=" {rmsLevel * 100 > 4
-									? ' size-52'
-									: rmsLevel * 100 > 2
-									? 'size-48'
-									: rmsLevel * 100 > 1
-									? 'size-[11.5rem]'
-									: 'size-44'}  transition-all bg-black dark:bg-white rounded-full"
-							/>
-						{/if}
+										@keyframes spinner_8HQG {
+											0%,
+											57.14% {
+												animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
+												transform: translate(0);
+											}
+											28.57% {
+												animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
+												transform: translateY(-6px);
+											}
+											100% {
+												transform: translate(0);
+											}
+										}
+									</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
+										class="spinner_qM83 spinner_oXPr"
+										cx="12"
+										cy="12"
+										r="3"
+									/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
+								>
+							{:else}
+								<div
+									class=" {rmsLevel * 100 > 4
+										? ' size-52'
+										: rmsLevel * 100 > 2
+										? 'size-48'
+										: rmsLevel * 100 > 1
+										? 'size-[11.5rem]'
+										: 'size-44'}  transition-all rounded-full {(model?.info?.meta
+										?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
+										? ' bg-cover bg-center bg-no-repeat'
+										: 'bg-black dark:bg-white'} "
+									style={(model?.info?.meta?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
+										? `background-image: url('${model?.info?.meta?.profile_image_url}');`
+										: ''}
+								/>
+							{/if}
+						</button>
 					{:else}
 						<div
 							class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full"
@@ -805,10 +851,19 @@
 					</div>
 
 					<div>
-						<button type="button">
+						<button
+							type="button"
+							on:click={() => {
+								if (assistantSpeaking) {
+									stopAllAudio();
+								}
+							}}
+						>
 							<div class=" line-clamp-1 text-sm font-medium">
 								{#if loading}
 									{$i18n.t('Thinking...')}
+								{:else if assistantSpeaking}
+									{$i18n.t('Tap to interrupt')}
 								{:else}
 									{$i18n.t('Listening...')}
 								{/if}

+ 6 - 6
src/lib/components/chat/MessageInput/Documents.svelte

@@ -101,20 +101,20 @@
 </script>
 
 {#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
-	<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
-		<div class="flex w-full px-2">
-			<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
+	<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10">
+		<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
+			<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
 				<div class=" text-lg font-semibold mt-2">#</div>
 			</div>
 
 			<div
-				class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-850 dark:text-gray-100"
+				class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-900 dark:text-gray-100"
 			>
-				<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
+				<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden">
 					{#each filteredItems as doc, docIdx}
 						<button
 							class=" px-3 py-1.5 rounded-xl w-full text-left {docIdx === selectedIdx
-								? ' bg-gray-100 dark:bg-gray-600 dark:text-gray-100 selected-command-option-button'
+								? ' bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button'
 								: ''}"
 							type="button"
 							on:click={() => {

+ 8 - 6
src/lib/components/chat/MessageInput/Models.svelte

@@ -133,18 +133,20 @@
 
 {#if prompt.charAt(0) === '@'}
 	{#if filteredModels.length > 0}
-		<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
-			<div class="flex w-full px-2">
-				<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
+		<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10">
+			<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
+				<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
 					<div class=" text-lg font-semibold mt-2">@</div>
 				</div>
 
-				<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-850">
-					<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
+				<div
+					class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
+				>
+					<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
 						{#each filteredModels as model, modelIdx}
 							<button
 								class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
-									? ' bg-gray-100 dark:bg-gray-600 selected-command-option-button'
+									? '  bg-gray-50 dark:bg-gray-850  selected-command-option-button'
 									: ''}"
 								type="button"
 								on:click={() => {

+ 9 - 7
src/lib/components/chat/MessageInput/PromptCommands.svelte

@@ -88,18 +88,20 @@
 </script>
 
 {#if filteredPromptCommands.length > 0}
-	<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
-		<div class="flex w-full px-2">
-			<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
+	<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10">
+		<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
+			<div class="  bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
 				<div class=" text-lg font-semibold mt-2">/</div>
 			</div>
 
-			<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-850">
-				<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
+			<div
+				class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
+			>
+				<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
 					{#each filteredPromptCommands as command, commandIdx}
 						<button
 							class=" px-3 py-1.5 rounded-xl w-full text-left {commandIdx === selectedCommandIdx
-								? ' bg-gray-100 dark:bg-gray-600 selected-command-option-button'
+								? '  bg-gray-50 dark:bg-gray-850 selected-command-option-button'
 								: ''}"
 							type="button"
 							on:click={() => {
@@ -122,7 +124,7 @@
 				</div>
 
 				<div
-					class=" px-2 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-850 rounded-br-xl flex items-center space-x-1"
+					class=" px-2 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-900 rounded-br-xl flex items-center space-x-1"
 				>
 					<div>
 						<svg

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

@@ -62,7 +62,7 @@
 							<div class="text-sm text-gray-600 font-normal line-clamp-2">{prompt.title[1]}</div>
 						{:else}
 							<div
-								class=" self-center text-sm font-medium dark:text-gray-300 dark:group-hover:text-gray-100 transition line-clamp-2"
+								class="  text-sm font-medium dark:text-gray-300 dark:group-hover:text-gray-100 transition line-clamp-2"
 							>
 								{prompt.content}
 							</div>

+ 37 - 24
src/lib/components/chat/Messages.svelte

@@ -202,38 +202,51 @@
 		}, 100);
 	};
 
-	const messageDeleteHandler = async (messageId) => {
+	const deleteMessageHandler = async (messageId) => {
 		const messageToDelete = history.messages[messageId];
-		const messageParentId = messageToDelete.parentId;
-		const messageChildrenIds = messageToDelete.childrenIds ?? [];
-		const hasSibling = messageChildrenIds.some(
+
+		const parentMessageId = messageToDelete.parentId;
+		const childMessageIds = messageToDelete.childrenIds ?? [];
+
+		const hasDescendantMessages = childMessageIds.some(
 			(childId) => history.messages[childId]?.childrenIds?.length > 0
 		);
-		messageChildrenIds.forEach((childId) => {
-			const child = history.messages[childId];
-			if (child && child.childrenIds) {
-				if (child.childrenIds.length === 0 && !hasSibling) {
-					// if last prompt/response pair
-					history.messages[messageParentId].childrenIds = [];
-					history.currentId = messageParentId;
+
+		history.currentId = parentMessageId;
+		await tick();
+
+		// Remove the message itself from the parent message's children array
+		history.messages[parentMessageId].childrenIds = history.messages[
+			parentMessageId
+		].childrenIds.filter((id) => id !== messageId);
+
+		await tick();
+
+		childMessageIds.forEach((childId) => {
+			const childMessage = history.messages[childId];
+
+			if (childMessage && childMessage.childrenIds) {
+				if (childMessage.childrenIds.length === 0 && !hasDescendantMessages) {
+					// If there are no other responses/prompts
+					history.messages[parentMessageId].childrenIds = [];
 				} else {
-					child.childrenIds.forEach((grandChildId) => {
+					childMessage.childrenIds.forEach((grandChildId) => {
 						if (history.messages[grandChildId]) {
-							history.messages[grandChildId].parentId = messageParentId;
-							history.messages[messageParentId].childrenIds.push(grandChildId);
+							history.messages[grandChildId].parentId = parentMessageId;
+							history.messages[parentMessageId].childrenIds.push(grandChildId);
 						}
 					});
 				}
 			}
-			// remove response
-			history.messages[messageParentId].childrenIds = history.messages[
-				messageParentId
+
+			// Remove child message id from the parent message's children array
+			history.messages[parentMessageId].childrenIds = history.messages[
+				parentMessageId
 			].childrenIds.filter((id) => id !== childId);
 		});
-		// remove prompt
-		history.messages[messageParentId].childrenIds = history.messages[
-			messageParentId
-		].childrenIds.filter((id) => id !== messageId);
+
+		await tick();
+
 		await updateChatById(localStorage.token, chatId, {
 			messages: messages,
 			history: history
@@ -292,7 +305,7 @@
 						>
 							{#if message.role === 'user'}
 								<UserMessage
-									on:delete={() => messageDeleteHandler(message.id)}
+									on:delete={() => deleteMessageHandler(message.id)}
 									{user}
 									{readOnly}
 									{message}
@@ -308,7 +321,7 @@
 									copyToClipboard={copyToClipboardWithToast}
 								/>
 							{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
-								{#key message.id}
+								{#key message.id && history.currentId}
 									<ResponseMessage
 										{message}
 										siblings={history.messages[message.parentId]?.childrenIds ?? []}
@@ -372,7 +385,7 @@
 				{/each}
 
 				{#if bottomPadding}
-					<div class="  pb-20" />
+					<div class="  pb-6" />
 				{/if}
 			{/key}
 		</div>

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

@@ -203,8 +203,18 @@ __builtins__.input = input`);
 		};
 	};
 
+	let debounceTimeout;
 	$: if (code) {
-		highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
+		// Function to perform the code highlighting
+		const highlightCode = () => {
+			highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
+		};
+
+		// Clear the previous timeout if it exists
+		clearTimeout(debounceTimeout);
+
+		// Set a new timeout to debounce the code highlighting
+		debounceTimeout = setTimeout(highlightCode, 10);
 	}
 </script>
 

+ 18 - 8
src/lib/components/chat/Messages/Placeholder.svelte

@@ -9,6 +9,7 @@
 
 	import Suggestions from '../MessageInput/Suggestions.svelte';
 	import { sanitizeResponseContent } from '$lib/utils';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -41,14 +42,23 @@
 							selectedModelIdx = modelIdx;
 						}}
 					>
-						<img
-							crossorigin="anonymous"
-							src={model?.info?.meta?.profile_image_url ??
-								($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
-							class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
-							alt="logo"
-							draggable="false"
-						/>
+						<Tooltip
+							content={marked.parse(
+								sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '')
+							)}
+							placement="right"
+						>
+							<img
+								crossorigin="anonymous"
+								src={model?.info?.meta?.profile_image_url ??
+									($i18n.language === 'dg-DG'
+										? `/doge.png`
+										: `${WEBUI_BASE_URL}/static/favicon.png`)}
+								class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
+								alt="logo"
+								draggable="false"
+							/>
+						</Tooltip>
 					</button>
 				{/each}
 			</div>

+ 4 - 2
src/lib/components/chat/Messages/ProfileImage.svelte

@@ -2,10 +2,12 @@
 	import { settings } from '$lib/stores';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 
+	export let className = 'size-8';
+
 	export let src = '/user.png';
 </script>
 
-<div class={($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}>
+<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
 	<img
 		crossorigin="anonymous"
 		src={src.startsWith(WEBUI_BASE_URL) ||
@@ -14,7 +16,7 @@
 		src.startsWith('/')
 			? src
 			: `/user.png`}
-		class=" w-8 object-cover rounded-full"
+		class=" {className} object-cover rounded-full -translate-y-[1px]"
 		alt="profile"
 		draggable="false"
 	/>

+ 76 - 61
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -15,12 +15,13 @@
 
 	const dispatch = createEventDispatcher();
 
-	import { config, models, settings } from '$lib/stores';
+	import { config, models, settings, user } from '$lib/stores';
 	import { synthesizeOpenAISpeech } from '$lib/apis/audio';
 	import { imageGenerations } from '$lib/apis/images';
 	import {
 		approximateToHumanReadable,
 		extractSentences,
+		replaceTokens,
 		revertSanitizedResponseContent,
 		sanitizeResponseContent
 	} from '$lib/utils';
@@ -74,7 +75,9 @@
 
 	let selectedCitation = null;
 
-	$: tokens = marked.lexer(sanitizeResponseContent(message?.content));
+	$: tokens = marked.lexer(
+		replaceTokens(sanitizeResponseContent(message?.content), model?.name, $user?.name)
+	);
 
 	const renderer = new marked.Renderer();
 
@@ -460,6 +463,18 @@
 									e.target.style.height = '';
 									e.target.style.height = `${e.target.scrollHeight}px`;
 								}}
+								on:keydown={(e) => {
+									if (e.key === 'Escape') {
+										document.getElementById('close-edit-message-button')?.click();
+									}
+
+									const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
+									const isEnterPressed = e.key === 'Enter';
+
+									if (isCmdOrCtrlPressed && isEnterPressed) {
+										document.getElementById('save-edit-message-button')?.click();
+									}
+								}}
 							/>
 
 							<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
@@ -940,68 +955,68 @@
 													>
 												</button>
 											</Tooltip>
-										{/if}
 
-										{#if isLastMessage && !readOnly}
-											<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
-												<button
-													type="button"
-													class="{isLastMessage
-														? 'visible'
-														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
-													on:click={() => {
-														continueGeneration();
-													}}
-												>
-													<svg
-														xmlns="http://www.w3.org/2000/svg"
-														fill="none"
-														viewBox="0 0 24 24"
-														stroke-width="2.3"
-														stroke="currentColor"
-														class="w-4 h-4"
+											{#if isLastMessage}
+												<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
+													<button
+														type="button"
+														class="{isLastMessage
+															? 'visible'
+															: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
+														on:click={() => {
+															continueGeneration();
+														}}
 													>
-														<path
-															stroke-linecap="round"
-															stroke-linejoin="round"
-															d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
-														/>
-														<path
-															stroke-linecap="round"
-															stroke-linejoin="round"
-															d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
-														/>
-													</svg>
-												</button>
-											</Tooltip>
-
-											<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
-												<button
-													type="button"
-													class="{isLastMessage
-														? 'visible'
-														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
-													on:click={() => {
-														showRateComment = false;
-														regenerateResponse(message);
-													}}
-												>
-													<svg
-														xmlns="http://www.w3.org/2000/svg"
-														fill="none"
-														viewBox="0 0 24 24"
-														stroke-width="2.3"
-														stroke="currentColor"
-														class="w-4 h-4"
+														<svg
+															xmlns="http://www.w3.org/2000/svg"
+															fill="none"
+															viewBox="0 0 24 24"
+															stroke-width="2.3"
+															stroke="currentColor"
+															class="w-4 h-4"
+														>
+															<path
+																stroke-linecap="round"
+																stroke-linejoin="round"
+																d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
+															/>
+															<path
+																stroke-linecap="round"
+																stroke-linejoin="round"
+																d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
+															/>
+														</svg>
+													</button>
+												</Tooltip>
+
+												<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
+													<button
+														type="button"
+														class="{isLastMessage
+															? 'visible'
+															: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
+														on:click={() => {
+															showRateComment = false;
+															regenerateResponse(message);
+														}}
 													>
-														<path
-															stroke-linecap="round"
-															stroke-linejoin="round"
-															d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
-														/>
-													</svg>
-												</button>
-											</Tooltip>
+														<svg
+															xmlns="http://www.w3.org/2000/svg"
+															fill="none"
+															viewBox="0 0 24 24"
+															stroke-width="2.3"
+															stroke="currentColor"
+															class="w-4 h-4"
+														>
+															<path
+																stroke-linecap="round"
+																stroke-linejoin="round"
+																d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
+															/>
+														</svg>
+													</button>
+												</Tooltip>
+											{/if}
 										{/if}
 									{/if}
 								</div>

+ 37 - 0
src/lib/components/chat/Messages/UserMessage.svelte

@@ -8,6 +8,7 @@
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 
 	import { user as _user } from '$lib/stores';
+	import { getFileContentById } from '$lib/apis/files';
 
 	const i18n = getContext('i18n');
 
@@ -97,6 +98,42 @@
 						<div class={$settings?.chatBubble ?? true ? 'self-end' : ''}>
 							{#if file.type === 'image'}
 								<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
+							{:else if file.type === 'file'}
+								<button
+									class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
+									type="button"
+									on:click={async () => {
+										if (file?.url) {
+											window.open(`${file?.url}/content`, '_blank').focus();
+										}
+									}}
+								>
+									<div class="p-2.5 bg-red-400 text-white rounded-lg">
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 24 24"
+											fill="currentColor"
+											class="w-6 h-6"
+										>
+											<path
+												fill-rule="evenodd"
+												d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
+												clip-rule="evenodd"
+											/>
+											<path
+												d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
+											/>
+										</svg>
+									</div>
+
+									<div class="flex flex-col justify-center -space-y-0.5">
+										<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
+											{file.name}
+										</div>
+
+										<div class=" text-gray-500 text-sm">{$i18n.t('File')}</div>
+									</div>
+								</button>
 							{:else if file.type === 'doc'}
 								<button
 									class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"

+ 1 - 0
src/lib/components/chat/ModelSelector/Selector.svelte

@@ -204,6 +204,7 @@
 		searchValue = '';
 		window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
 	}}
+	closeFocus={false}
 >
 	<DropdownMenu.Trigger class="relative w-full" aria-label={placeholder}>
 		<div

+ 2 - 1
src/lib/components/chat/Settings/About.svelte

@@ -132,7 +132,8 @@
 		<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
 			{#if !$WEBUI_NAME.includes('Open WebUI')}
 				<span class=" text-gray-500 dark:text-gray-300 font-medium">{$WEBUI_NAME}</span> -
-			{/if}{$i18n.t('Created by')}
+			{/if}
+			{$i18n.t('Created by')}
 			<a
 				class=" text-gray-500 dark:text-gray-300 font-medium"
 				href="https://github.com/tjbck"

+ 188 - 80
src/lib/components/chat/Settings/Interface.svelte

@@ -5,17 +5,24 @@
 	import { createEventDispatcher, onMount, getContext } from 'svelte';
 	import { toast } from 'svelte-sonner';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import { updateUserInfo } from '$lib/apis/users';
+	import { getUserPosition } from '$lib/utils';
 	const dispatch = createEventDispatcher();
 
 	const i18n = getContext('i18n');
 
 	export let saveSettings: Function;
 
+	let backgroundImageUrl = null;
+	let inputFiles = null;
+	let filesInputElement;
+
 	// Addons
 	let titleAutoGenerate = true;
 	let responseAutoCopy = false;
 	let widescreenMode = false;
 	let splitLargeChunks = false;
+	let userLocation = false;
 
 	// Interface
 	let defaultModelId = '';
@@ -51,6 +58,26 @@
 		saveSettings({ showEmojiInCall: showEmojiInCall });
 	};
 
+	const toggleUserLocation = async () => {
+		userLocation = !userLocation;
+
+		if (userLocation) {
+			const position = await getUserPosition().catch((error) => {
+				toast.error(error.message);
+				return null;
+			});
+
+			if (position) {
+				await updateUserInfo(localStorage.token, { location: position });
+				toast.success('User location successfully retrieved.');
+			} else {
+				userLocation = false;
+			}
+		}
+
+		saveSettings({ userLocation });
+	};
+
 	const toggleTitleAutoGenerate = async () => {
 		titleAutoGenerate = !titleAutoGenerate;
 		saveSettings({
@@ -106,8 +133,11 @@
 		widescreenMode = $settings.widescreenMode ?? false;
 		splitLargeChunks = $settings.splitLargeChunks ?? false;
 		chatDirection = $settings.chatDirection ?? 'LTR';
+		userLocation = $settings.userLocation ?? false;
 
 		defaultModelId = ($settings?.models ?? ['']).at(0);
+
+		backgroundImageUrl = $settings.backgroundImageUrl ?? null;
 	});
 </script>
 
@@ -118,13 +148,63 @@
 		dispatch('save');
 	}}
 >
-	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
+	<input
+		bind:this={filesInputElement}
+		bind:files={inputFiles}
+		type="file"
+		hidden
+		accept="image/*"
+		on:change={() => {
+			let reader = new FileReader();
+			reader.onload = (event) => {
+				let originalImageUrl = `${event.target.result}`;
+
+				backgroundImageUrl = originalImageUrl;
+				saveSettings({ backgroundImageUrl });
+			};
+
+			if (
+				inputFiles &&
+				inputFiles.length > 0 &&
+				['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type'])
+			) {
+				reader.readAsDataURL(inputFiles[0]);
+			} else {
+				console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`);
+				inputFiles = null;
+			}
+		}}
+	/>
+
+	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem] scrollbar-hidden">
+		<div class=" space-y-1 mb-3">
+			<div class="mb-2">
+				<div class="flex justify-between items-center text-xs">
+					<div class=" text-sm font-medium">{$i18n.t('Default Model')}</div>
+				</div>
+			</div>
+
+			<div class="flex-1 mr-2">
+				<select
+					class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+					bind:value={defaultModelId}
+					placeholder="Select a model"
+				>
+					<option value="" disabled selected>{$i18n.t('Select a model')}</option>
+					{#each $models.filter((model) => model.id) as model}
+						<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
+					{/each}
+				</select>
+			</div>
+		</div>
+		<hr class=" dark:border-gray-850" />
+
 		<div>
-			<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div>
+			<div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
 
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Chat Bubble UI')}</div>
+					<div class=" self-center text-xs">{$i18n.t('Chat Bubble UI')}</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
@@ -142,18 +222,42 @@
 				</div>
 			</div>
 
+			{#if !$settings.chatBubble}
+				<div>
+					<div class=" py-0.5 flex w-full justify-between">
+						<div class=" self-center text-xs">
+							{$i18n.t('Display the username instead of You in the Chat')}
+						</div>
+
+						<button
+							class="p-1 px-3 text-xs flex rounded transition"
+							on:click={() => {
+								toggleShowUsername();
+							}}
+							type="button"
+						>
+							{#if showUsername === true}
+								<span class="ml-2 self-center">{$i18n.t('On')}</span>
+							{:else}
+								<span class="ml-2 self-center">{$i18n.t('Off')}</span>
+							{/if}
+						</button>
+					</div>
+				</div>
+			{/if}
+
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
+					<div class=" self-center text-xs">{$i18n.t('Widescreen Mode')}</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							toggleTitleAutoGenerate();
+							togglewidescreenMode();
 						}}
 						type="button"
 					>
-						{#if titleAutoGenerate === true}
+						{#if widescreenMode === true}
 							<span class="ml-2 self-center">{$i18n.t('On')}</span>
 						{:else}
 							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -164,18 +268,36 @@
 
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">
-						{$i18n.t('Response AutoCopy to Clipboard')}
+					<div class=" self-center text-xs">{$i18n.t('Chat direction')}</div>
+
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						on:click={toggleChangeChatDirection}
+						type="button"
+					>
+						{#if chatDirection === 'LTR'}
+							<span class="ml-2 self-center">{$i18n.t('LTR')}</span>
+						{:else}
+							<span class="ml-2 self-center">{$i18n.t('RTL')}</span>
+						{/if}
+					</button>
+				</div>
+			</div>
+
+			<div>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs">
+						{$i18n.t('Fluidly stream large external response chunks')}
 					</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							toggleResponseAutoCopy();
+							toggleSplitLargeChunks();
 						}}
 						type="button"
 					>
-						{#if responseAutoCopy === true}
+						{#if splitLargeChunks === true}
 							<span class="ml-2 self-center">{$i18n.t('On')}</span>
 						{:else}
 							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -186,36 +308,45 @@
 
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Widescreen Mode')}</div>
+					<div class=" self-center text-xs">
+						{$i18n.t('Chat Background Image')}
+					</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							togglewidescreenMode();
+							if (backgroundImageUrl !== null) {
+								backgroundImageUrl = null;
+								saveSettings({ backgroundImageUrl });
+							} else {
+								filesInputElement.click();
+							}
 						}}
 						type="button"
 					>
-						{#if widescreenMode === true}
-							<span class="ml-2 self-center">{$i18n.t('On')}</span>
+						{#if backgroundImageUrl !== null}
+							<span class="ml-2 self-center">{$i18n.t('Reset')}</span>
 						{:else}
-							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
+							<span class="ml-2 self-center">{$i18n.t('Upload')}</span>
 						{/if}
 					</button>
 				</div>
 			</div>
 
+			<div class=" my-1.5 text-sm font-medium">{$i18n.t('Chat')}</div>
+
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Display Emoji in Call')}</div>
+					<div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							toggleEmojiInCall();
+							toggleTitleAutoGenerate();
 						}}
 						type="button"
 					>
-						{#if showEmojiInCall === true}
+						{#if titleAutoGenerate === true}
 							<span class="ml-2 self-center">{$i18n.t('On')}</span>
 						{:else}
 							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -224,44 +355,20 @@
 				</div>
 			</div>
 
-			{#if !$settings.chatBubble}
-				<div>
-					<div class=" py-0.5 flex w-full justify-between">
-						<div class=" self-center text-xs font-medium">
-							{$i18n.t('Display the username instead of You in the Chat')}
-						</div>
-
-						<button
-							class="p-1 px-3 text-xs flex rounded transition"
-							on:click={() => {
-								toggleShowUsername();
-							}}
-							type="button"
-						>
-							{#if showUsername === true}
-								<span class="ml-2 self-center">{$i18n.t('On')}</span>
-							{:else}
-								<span class="ml-2 self-center">{$i18n.t('Off')}</span>
-							{/if}
-						</button>
-					</div>
-				</div>
-			{/if}
-
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">
-						{$i18n.t('Fluidly stream large external response chunks')}
+					<div class=" self-center text-xs">
+						{$i18n.t('Response AutoCopy to Clipboard')}
 					</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							toggleSplitLargeChunks();
+							toggleResponseAutoCopy();
 						}}
 						type="button"
 					>
-						{#if splitLargeChunks === true}
+						{#if responseAutoCopy === true}
 							<span class="ml-2 self-center">{$i18n.t('On')}</span>
 						{:else}
 							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -269,46 +376,47 @@
 					</button>
 				</div>
 			</div>
-		</div>
 
-		<div>
-			<div class=" py-0.5 flex w-full justify-between">
-				<div class=" self-center text-xs font-medium">{$i18n.t('Chat direction')}</div>
+			<div>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
 
-				<button
-					class="p-1 px-3 text-xs flex rounded transition"
-					on:click={toggleChangeChatDirection}
-					type="button"
-				>
-					{#if chatDirection === 'LTR'}
-						<span class="ml-2 self-center">{$i18n.t('LTR')}</span>
-					{:else}
-						<span class="ml-2 self-center">{$i18n.t('RTL')}</span>
-					{/if}
-				</button>
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						on:click={() => {
+							toggleUserLocation();
+						}}
+						type="button"
+					>
+						{#if userLocation === true}
+							<span class="ml-2 self-center">{$i18n.t('On')}</span>
+						{:else}
+							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
+						{/if}
+					</button>
+				</div>
 			</div>
-		</div>
 
-		<hr class=" dark:border-gray-850" />
+			<div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
 
-		<div class=" space-y-1 mb-3">
-			<div class="mb-2">
-				<div class="flex justify-between items-center text-xs">
-					<div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
-				</div>
-			</div>
+			<div>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs">{$i18n.t('Display Emoji in Call')}</div>
 
-			<div class="flex-1 mr-2">
-				<select
-					class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
-					bind:value={defaultModelId}
-					placeholder="Select a model"
-				>
-					<option value="" disabled selected>{$i18n.t('Select a model')}</option>
-					{#each $models.filter((model) => model.id) as model}
-						<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
-					{/each}
-				</select>
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						on:click={() => {
+							toggleEmojiInCall();
+						}}
+						type="button"
+					>
+						{#if showEmojiInCall === true}
+							<span class="ml-2 self-center">{$i18n.t('On')}</span>
+						{:else}
+							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
+						{/if}
+					</button>
+				</div>
 			</div>
 		</div>
 	</div>

+ 29 - 29
src/lib/components/chat/SettingsModal.svelte

@@ -88,35 +88,6 @@
 					<div class=" self-center">{$i18n.t('General')}</div>
 				</button>
 
-				{#if $user.role === 'admin'}
-					<button
-						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
-						'admin'
-							? 'bg-gray-200 dark:bg-gray-700'
-							: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
-						on:click={async () => {
-							await goto('/admin/settings');
-							show = false;
-						}}
-					>
-						<div class=" self-center mr-2">
-							<svg
-								xmlns="http://www.w3.org/2000/svg"
-								viewBox="0 0 24 24"
-								fill="currentColor"
-								class="size-4"
-							>
-								<path
-									fill-rule="evenodd"
-									d="M4.5 3.75a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V6.75a3 3 0 0 0-3-3h-15Zm4.125 3a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Zm-3.873 8.703a4.126 4.126 0 0 1 7.746 0 .75.75 0 0 1-.351.92 7.47 7.47 0 0 1-3.522.877 7.47 7.47 0 0 1-3.522-.877.75.75 0 0 1-.351-.92ZM15 8.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15ZM14.25 12a.75.75 0 0 1 .75-.75h3.75a.75.75 0 0 1 0 1.5H15a.75.75 0 0 1-.75-.75Zm.75 2.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15Z"
-									clip-rule="evenodd"
-								/>
-							</svg>
-						</div>
-						<div class=" self-center">{$i18n.t('Admin Settings')}</div>
-					</button>
-				{/if}
-
 				<button
 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 					'interface'
@@ -237,6 +208,35 @@
 					<div class=" self-center">{$i18n.t('Account')}</div>
 				</button>
 
+				{#if $user.role === 'admin'}
+					<button
+						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
+						'admin'
+							? 'bg-gray-200 dark:bg-gray-700'
+							: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
+						on:click={async () => {
+							await goto('/admin/settings');
+							show = false;
+						}}
+					>
+						<div class=" self-center mr-2">
+							<svg
+								xmlns="http://www.w3.org/2000/svg"
+								viewBox="0 0 24 24"
+								fill="currentColor"
+								class="size-4"
+							>
+								<path
+									fill-rule="evenodd"
+									d="M4.5 3.75a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V6.75a3 3 0 0 0-3-3h-15Zm4.125 3a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Zm-3.873 8.703a4.126 4.126 0 0 1 7.746 0 .75.75 0 0 1-.351.92 7.47 7.47 0 0 1-3.522.877 7.47 7.47 0 0 1-3.522-.877.75.75 0 0 1-.351-.92ZM15 8.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15ZM14.25 12a.75.75 0 0 1 .75-.75h3.75a.75.75 0 0 1 0 1.5H15a.75.75 0 0 1-.75-.75Zm.75 2.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15Z"
+									clip-rule="evenodd"
+								/>
+							</svg>
+						</div>
+						<div class=" self-center">{$i18n.t('Admin Settings')}</div>
+					</button>
+				{/if}
+
 				<button
 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 					'about'

+ 6 - 5
src/lib/components/common/ConfirmDialog.svelte

@@ -1,16 +1,17 @@
 <script lang="ts">
-	import { onMount, createEventDispatcher } from 'svelte';
+	import { onMount, getContext, createEventDispatcher } from 'svelte';
 	import { fade } from 'svelte/transition';
+	const i18n = getContext('i18n');
 
 	import { flyAndScale } from '$lib/utils/transitions';
 
 	const dispatch = createEventDispatcher();
 
-	export let title = 'Confirm your action';
-	export let message = 'This action cannot be undone. Do you wish to continue?';
+	export let title = $i18n.t('Confirm your action');
+	export let message = $i18n.t('This action cannot be undone. Do you wish to continue?');
 
-	export let cancelLabel = 'Cancel';
-	export let confirmLabel = 'Confirm';
+	export let cancelLabel = $i18n.t('Cancel');
+	export let confirmLabel = $i18n.t('Confirm');
 
 	export let show = false;
 	let modalElement = null;

+ 8 - 8
src/lib/components/common/Modal.svelte

@@ -33,14 +33,14 @@
 		mounted = true;
 	});
 
-	$: if (mounted) {
-		if (show) {
-			window.addEventListener('keydown', handleKeyDown);
-			document.body.style.overflow = 'hidden';
-		} else {
-			window.removeEventListener('keydown', handleKeyDown);
-			document.body.style.overflow = 'unset';
-		}
+	$: if (show && modalElement) {
+		document.body.appendChild(modalElement);
+		window.addEventListener('keydown', handleKeyDown);
+		document.body.style.overflow = 'hidden';
+	} else if (modalElement) {
+		document.body.removeChild(modalElement);
+		window.removeEventListener('keydown', handleKeyDown);
+		document.body.style.overflow = 'unset';
 	}
 </script>
 

+ 1 - 1
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-10">
+<div class=" hidden lg:flex fixed bottom-0 right-0 px-2 py-2 z-20">
 	<button
 		id="show-shortcuts-button"
 		class="hidden"

+ 1 - 1
src/lib/components/layout/Navbar.svelte

@@ -38,7 +38,7 @@
 </script>
 
 <ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
-<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
+<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-10">
 	<div class=" flex max-w-full w-full mx-auto px-5 pt-0.5 md:px-[1rem]">
 		<div class="flex items-center w-full max-w-full">
 			<div

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

@@ -120,18 +120,31 @@
 			}
 		};
 
-		document.addEventListener('keydown', onKeyDown);
-		document.addEventListener('keyup', onKeyUp);
+		const onFocus = () => {};
+
+		const onBlur = () => {
+			shiftKey = false;
+			selectedChatId = null;
+		};
+
+		window.addEventListener('keydown', onKeyDown);
+		window.addEventListener('keyup', onKeyUp);
 
 		window.addEventListener('touchstart', onTouchStart);
 		window.addEventListener('touchend', onTouchEnd);
 
+		window.addEventListener('focus', onFocus);
+		window.addEventListener('blur', onBlur);
+
 		return () => {
 			window.removeEventListener('keydown', onKeyDown);
 			window.removeEventListener('keyup', onKeyUp);
 
 			window.removeEventListener('touchstart', onTouchStart);
 			window.removeEventListener('touchend', onTouchEnd);
+
+			window.removeEventListener('focus', onFocus);
+			window.removeEventListener('blur', onBlur);
 		};
 	});
 
@@ -182,13 +195,13 @@
 
 <DeleteConfirmDialog
 	bind:show={showDeleteConfirm}
-	title="Delete chat?"
+	title={$i18n.t('Delete chat?')}
 	on:confirm={() => {
 		deleteChatHandler(deleteChat.id);
 	}}
 >
 	<div class=" text-sm text-gray-500">
-		This will delete <span class="  font-semibold">{deleteChat.title}</span>.
+		{$i18n.t('This will delete')} <span class="  font-semibold">{deleteChat.title}</span>.
 	</div>
 </DeleteConfirmDialog>
 
@@ -464,9 +477,16 @@
 						on:select={() => {
 							selectedChatId = chat.id;
 						}}
-						on:delete={() => {
-							deleteChat = chat;
-							showDeleteConfirm = true;
+						on:unselect={() => {
+							selectedChatId = null;
+						}}
+						on:delete={(e) => {
+							if ((e?.detail ?? '') === 'shift') {
+								deleteChatHandler(chat.id);
+							} else {
+								deleteChat = chat;
+								showDeleteConfirm = true;
+							}
 						}}
 					/>
 				{/each}

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

@@ -126,7 +126,7 @@
 			: selected
 			? 'from-gray-100 dark:from-gray-950'
 			: 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
-            absolute right-[10px] top-[10px] pr-2 pl-5 bg-gradient-to-l from-80%
+            absolute right-[10px] top-[6px] py-1 pr-2 pl-5 bg-gradient-to-l from-80%
 
               to-transparent"
 		on:mouseenter={(e) => {
@@ -138,7 +138,7 @@
 	>
 		{#if confirmEdit}
 			<div class="flex self-center space-x-1.5 z-10">
-				<Tooltip content="Confirm">
+				<Tooltip content={$i18n.t('Confirm')}>
 					<button
 						class=" self-center dark:hover:text-white transition"
 						on:click={() => {
@@ -162,7 +162,7 @@
 					</button>
 				</Tooltip>
 
-				<Tooltip content="Cancel">
+				<Tooltip content={$i18n.t('Cancel')}>
 					<button
 						class=" self-center dark:hover:text-white transition"
 						on:click={() => {
@@ -185,7 +185,7 @@
 			</div>
 		{:else if shiftKey && mouseOver}
 			<div class=" flex items-center self-center space-x-1.5">
-				<Tooltip content="Archive" className="flex items-center">
+				<Tooltip content={$i18n.t('Archive')} className="flex items-center">
 					<button
 						class=" self-center dark:hover:text-white transition"
 						on:click={() => {
@@ -197,11 +197,11 @@
 					</button>
 				</Tooltip>
 
-				<Tooltip content="Delete">
+				<Tooltip content={$i18n.t('Delete')}>
 					<button
 						class=" self-center dark:hover:text-white transition"
 						on:click={() => {
-							deleteChat(chat.id);
+							dispatch('delete', 'shift');
 						}}
 						type="button"
 					>
@@ -231,7 +231,7 @@
 						dispatch('delete');
 					}}
 					onClose={() => {
-						selected = false;
+						dispatch('unselect');
 					}}
 				>
 					<button

+ 26 - 0
src/lib/components/layout/Sidebar/UserMenu.svelte

@@ -81,6 +81,32 @@
 			</button>
 
 			{#if role === 'admin'}
+				<button
+					class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
+					on:click={() => {
+						goto('/playground');
+						show = false;
+					}}
+				>
+					<div class=" self-center mr-3">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="1.5"
+							stroke="currentColor"
+							class="size-5"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="M14.25 9.75 16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z"
+							/>
+						</svg>
+					</div>
+					<div class=" self-center font-medium">{$i18n.t('Playground')}</div>
+				</button>
+
 				<button
 					class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
 					on:click={() => {

+ 377 - 0
src/lib/components/workspace/Functions.svelte

@@ -0,0 +1,377 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import fileSaver from 'file-saver';
+	const { saveAs } = fileSaver;
+
+	import { WEBUI_NAME, functions, models } from '$lib/stores';
+	import { onMount, getContext } from 'svelte';
+	import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
+
+	import { goto } from '$app/navigation';
+	import {
+		createNewFunction,
+		deleteFunctionById,
+		exportFunctions,
+		getFunctionById,
+		getFunctions
+	} from '$lib/apis/functions';
+
+	import ArrowDownTray from '../icons/ArrowDownTray.svelte';
+	import Tooltip from '../common/Tooltip.svelte';
+	import ConfirmDialog from '../common/ConfirmDialog.svelte';
+	import { getModels } from '$lib/apis';
+
+	const i18n = getContext('i18n');
+
+	let functionsImportInputElement: HTMLInputElement;
+	let importFiles;
+
+	let showConfirm = false;
+	let query = '';
+</script>
+
+<svelte:head>
+	<title>
+		{$i18n.t('Functions')} | {$WEBUI_NAME}
+	</title>
+</svelte:head>
+
+<div class="mb-3 flex justify-between items-center">
+	<div class=" text-lg font-semibold self-center">{$i18n.t('Functions')}</div>
+</div>
+
+<div class=" flex w-full space-x-2">
+	<div class="flex flex-1">
+		<div class=" self-center ml-1 mr-3">
+			<svg
+				xmlns="http://www.w3.org/2000/svg"
+				viewBox="0 0 20 20"
+				fill="currentColor"
+				class="w-4 h-4"
+			>
+				<path
+					fill-rule="evenodd"
+					d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
+					clip-rule="evenodd"
+				/>
+			</svg>
+		</div>
+		<input
+			class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
+			bind:value={query}
+			placeholder={$i18n.t('Search Functions')}
+		/>
+	</div>
+
+	<div>
+		<a
+			class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
+			href="/workspace/functions/create"
+		>
+			<svg
+				xmlns="http://www.w3.org/2000/svg"
+				viewBox="0 0 16 16"
+				fill="currentColor"
+				class="w-4 h-4"
+			>
+				<path
+					d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
+				/>
+			</svg>
+		</a>
+	</div>
+</div>
+<hr class=" dark:border-gray-850 my-2.5" />
+
+<div class="my-3 mb-5">
+	{#each $functions.filter((f) => query === '' || f.name
+				.toLowerCase()
+				.includes(query.toLowerCase()) || f.id.toLowerCase().includes(query.toLowerCase())) as func}
+		<button
+			class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
+			type="button"
+			on:click={() => {
+				goto(`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`);
+			}}
+		>
+			<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
+				<a
+					href={`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`}
+					class="flex items-center text-left"
+				>
+					<div class=" flex-1 self-center pl-1">
+						<div class=" font-semibold flex items-center gap-1.5">
+							<div
+								class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+							>
+								{func.type}
+							</div>
+
+							<div>
+								{func.name}
+							</div>
+						</div>
+
+						<div class="flex gap-1.5 px-1">
+							<div class=" text-gray-500 text-xs font-medium">{func.id}</div>
+
+							<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
+								{func.meta.description}
+							</div>
+						</div>
+					</div>
+				</a>
+			</div>
+			<div class="flex flex-row space-x-1 self-center">
+				<Tooltip content="Edit">
+					<a
+						class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+						type="button"
+						href={`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`}
+					>
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="1.5"
+							stroke="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
+							/>
+						</svg>
+					</a>
+				</Tooltip>
+
+				<Tooltip content="Clone">
+					<button
+						class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+						type="button"
+						on:click={async (e) => {
+							e.stopPropagation();
+
+							const _function = await getFunctionById(localStorage.token, func.id).catch(
+								(error) => {
+									toast.error(error);
+									return null;
+								}
+							);
+
+							if (_function) {
+								sessionStorage.function = JSON.stringify({
+									..._function,
+									id: `${_function.id}_clone`,
+									name: `${_function.name} (Clone)`
+								});
+								goto('/workspace/functions/create');
+							}
+						}}
+					>
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="1.5"
+							stroke="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
+							/>
+						</svg>
+					</button>
+				</Tooltip>
+
+				<Tooltip content="Export">
+					<button
+						class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+						type="button"
+						on:click={async (e) => {
+							e.stopPropagation();
+
+							const _function = await getFunctionById(localStorage.token, func.id).catch(
+								(error) => {
+									toast.error(error);
+									return null;
+								}
+							);
+
+							if (_function) {
+								let blob = new Blob([JSON.stringify([_function])], {
+									type: 'application/json'
+								});
+								saveAs(blob, `function-${_function.id}-export-${Date.now()}.json`);
+							}
+						}}
+					>
+						<ArrowDownTray />
+					</button>
+				</Tooltip>
+
+				<Tooltip content="Delete">
+					<button
+						class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+						type="button"
+						on:click={async (e) => {
+							e.stopPropagation();
+
+							const res = await deleteFunctionById(localStorage.token, func.id).catch((error) => {
+								toast.error(error);
+								return null;
+							});
+
+							if (res) {
+								toast.success('Function deleted successfully');
+
+								functions.set(await getFunctions(localStorage.token));
+								models.set(await getModels(localStorage.token));
+							}
+						}}
+					>
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="1.5"
+							stroke="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="M14.74 9l-.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 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
+							/>
+						</svg>
+					</button>
+				</Tooltip>
+			</div>
+		</button>
+	{/each}
+</div>
+
+<!-- <div class=" text-gray-500 text-xs mt-1 mb-2">
+	ⓘ {$i18n.t(
+		'Admins have access to all tools at all times; users need tools assigned per model in the workspace.'
+	)}
+</div> -->
+
+<div class=" flex justify-end w-full mb-2">
+	<div class="flex space-x-2">
+		<input
+			id="documents-import-input"
+			bind:this={functionsImportInputElement}
+			bind:files={importFiles}
+			type="file"
+			accept=".json"
+			hidden
+			on:change={() => {
+				console.log(importFiles);
+				showConfirm = true;
+			}}
+		/>
+
+		<button
+			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+			on:click={() => {
+				functionsImportInputElement.click();
+			}}
+		>
+			<div class=" self-center mr-2 font-medium">{$i18n.t('Import Functions')}</div>
+
+			<div class=" self-center">
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 16 16"
+					fill="currentColor"
+					class="w-4 h-4"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
+		</button>
+
+		<button
+			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+			on:click={async () => {
+				const _functions = await exportFunctions(localStorage.token).catch((error) => {
+					toast.error(error);
+					return null;
+				});
+
+				if (_functions) {
+					let blob = new Blob([JSON.stringify(_functions)], {
+						type: 'application/json'
+					});
+					saveAs(blob, `functions-export-${Date.now()}.json`);
+				}
+			}}
+		>
+			<div class=" self-center mr-2 font-medium">{$i18n.t('Export Functions')}</div>
+
+			<div class=" self-center">
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 16 16"
+					fill="currentColor"
+					class="w-4 h-4"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
+		</button>
+	</div>
+</div>
+
+<ConfirmDialog
+	bind:show={showConfirm}
+	on:confirm={() => {
+		const reader = new FileReader();
+		reader.onload = async (event) => {
+			const _functions = JSON.parse(event.target.result);
+			console.log(_functions);
+
+			for (const func of _functions) {
+				const res = await createNewFunction(localStorage.token, func).catch((error) => {
+					toast.error(error);
+					return null;
+				});
+			}
+
+			toast.success('Functions imported successfully');
+			functions.set(await getFunctions(localStorage.token));
+			models.set(await getModels(localStorage.token));
+		};
+
+		reader.readAsText(importFiles[0]);
+	}}
+>
+	<div class="text-sm text-gray-500">
+		<div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3">
+			<div>Please carefully review the following warnings:</div>
+
+			<ul class=" mt-1 list-disc pl-4 text-xs">
+				<li>Functions allow arbitrary code execution.</li>
+				<li>Do not install functions from sources you do not fully trust.</li>
+			</ul>
+		</div>
+
+		<div class="my-3">
+			I acknowledge that I have read and I understand the implications of my action. I am aware of
+			the risks associated with executing arbitrary code and I have verified the trustworthiness of
+			the source.
+		</div>
+	</div>
+</ConfirmDialog>

+ 381 - 0
src/lib/components/workspace/Functions/FunctionEditor.svelte

@@ -0,0 +1,381 @@
+<script>
+	import { getContext, createEventDispatcher, onMount } from 'svelte';
+	import { goto } from '$app/navigation';
+
+	const dispatch = createEventDispatcher();
+	const i18n = getContext('i18n');
+
+	import CodeEditor from '$lib/components/common/CodeEditor.svelte';
+	import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
+
+	let formElement = null;
+	let loading = false;
+	let showConfirm = false;
+
+	export let edit = false;
+	export let clone = false;
+
+	export let id = '';
+	export let name = '';
+	export let meta = {
+		description: ''
+	};
+	export let content = '';
+
+	$: if (name && !edit && !clone) {
+		id = name.replace(/\s+/g, '_').toLowerCase();
+	}
+
+	let codeEditor;
+	let boilerplate = `from pydantic import BaseModel
+from typing import Optional
+
+
+class Filter:
+    class Valves(BaseModel):
+        max_turns: int = 4
+        pass
+
+    def __init__(self):
+        # Indicates custom file handling logic. This flag helps disengage default routines in favor of custom
+        # implementations, informing the WebUI to defer file-related operations to designated methods within this class.
+        # Alternatively, you can remove the files directly from the body in from the inlet hook
+        self.file_handler = True
+
+        # Initialize 'valves' with specific configurations. Using 'Valves' instance helps encapsulate settings,
+        # which ensures settings are managed cohesively and not confused with operational flags like 'file_handler'.
+        self.valves = self.Valves(**{"max_turns": 2})
+        pass
+
+    def inlet(self, body: dict, user: Optional[dict] = None) -> dict:
+        # Modify the request body or validate it before processing by the chat completion API.
+        # This function is the pre-processor for the API where various checks on the input can be performed.
+        # It can also modify the request before sending it to the API.
+        print(f"inlet:{__name__}")
+        print(f"inlet:body:{body}")
+        print(f"inlet:user:{user}")
+
+        if user.get("role", "admin") in ["user", "admin"]:
+            messages = body.get("messages", [])
+            if len(messages) > self.valves.max_turns:
+                raise Exception(
+                    f"Conversation turn limit exceeded. Max turns: {self.valves.max_turns}"
+                )
+
+        return body
+
+    def outlet(self, body: dict, user: Optional[dict] = None) -> dict:
+        # Modify or analyze the response body after processing by the API.
+        # This function is the post-processor for the API, which can be used to modify the response
+        # or perform additional checks and analytics.
+        print(f"outlet:{__name__}")
+        print(f"outlet:body:{body}")
+        print(f"outlet:user:{user}")
+
+        messages = [
+            {
+                **message,
+                "content": f"{message['content']} - @@Modified from Filter Outlet",
+            }
+            for message in body.get("messages", [])
+        ]
+
+        return {"messages": messages}
+
+`;
+
+	const _boilerplate = `from pydantic import BaseModel
+from typing import Optional, Union, Generator, Iterator
+from utils.misc import get_last_user_message
+
+import os
+import requests
+
+
+# Filter Class: This class is designed to serve as a pre-processor and post-processor
+# for request and response modifications. It checks and transforms requests and responses
+# to ensure they meet specific criteria before further processing or returning to the user.
+class Filter:
+    class Valves(BaseModel):
+        max_turns: int = 4
+        pass
+
+    def __init__(self):
+        # Indicates custom file handling logic. This flag helps disengage default routines in favor of custom
+        # implementations, informing the WebUI to defer file-related operations to designated methods within this class.
+        # Alternatively, you can remove the files directly from the body in from the inlet hook
+        self.file_handler = True
+
+        # Initialize 'valves' with specific configurations. Using 'Valves' instance helps encapsulate settings,
+        # which ensures settings are managed cohesively and not confused with operational flags like 'file_handler'.
+        self.valves = self.Valves(**{"max_turns": 2})
+        pass
+
+    def inlet(self, body: dict, user: Optional[dict] = None) -> dict:
+        # Modify the request body or validate it before processing by the chat completion API.
+        # This function is the pre-processor for the API where various checks on the input can be performed.
+        # It can also modify the request before sending it to the API.
+        print(f"inlet:{__name__}")
+        print(f"inlet:body:{body}")
+        print(f"inlet:user:{user}")
+
+        if user.get("role", "admin") in ["user", "admin"]:
+            messages = body.get("messages", [])
+            if len(messages) > self.valves.max_turns:
+                raise Exception(
+                    f"Conversation turn limit exceeded. Max turns: {self.valves.max_turns}"
+                )
+
+        return body
+
+    def outlet(self, body: dict, user: Optional[dict] = None) -> dict:
+        # Modify or analyze the response body after processing by the API.
+        # This function is the post-processor for the API, which can be used to modify the response
+        # or perform additional checks and analytics.
+        print(f"outlet:{__name__}")
+        print(f"outlet:body:{body}")
+        print(f"outlet:user:{user}")
+
+        messages = [
+            {
+                **message,
+                "content": f"{message['content']} - @@Modified from Filter Outlet",
+            }
+            for message in body.get("messages", [])
+        ]
+
+        return {"messages": messages}
+
+
+
+# Pipe Class: This class functions as a customizable pipeline.
+# It can be adapted to work with any external or internal models,
+# making it versatile for various use cases outside of just OpenAI models.
+class Pipe:
+    class Valves(BaseModel):
+        OPENAI_API_BASE_URL: str = "https://api.openai.com/v1"
+        OPENAI_API_KEY: str = "your-key"
+        pass
+
+    def __init__(self):
+        self.type = "manifold"
+        self.valves = self.Valves()
+        self.pipes = self.get_openai_models()
+        pass
+
+    def get_openai_models(self):
+        if self.valves.OPENAI_API_KEY:
+            try:
+                headers = {}
+                headers["Authorization"] = f"Bearer {self.valves.OPENAI_API_KEY}"
+                headers["Content-Type"] = "application/json"
+
+                r = requests.get(
+                    f"{self.valves.OPENAI_API_BASE_URL}/models", headers=headers
+                )
+
+                models = r.json()
+                return [
+                    {
+                        "id": model["id"],
+                        "name": model["name"] if "name" in model else model["id"],
+                    }
+                    for model in models["data"]
+                    if "gpt" in model["id"]
+                ]
+
+            except Exception as e:
+
+                print(f"Error: {e}")
+                return [
+                    {
+                        "id": "error",
+                        "name": "Could not fetch models from OpenAI, please update the API Key in the valves.",
+                    },
+                ]
+        else:
+            return []
+
+    def pipe(self, body: dict) -> Union[str, Generator, Iterator]:
+        # This is where you can add your custom pipelines like RAG.
+        print(f"pipe:{__name__}")
+
+        if "user" in body:
+            print(body["user"])
+            del body["user"]
+
+        headers = {}
+        headers["Authorization"] = f"Bearer {self.valves.OPENAI_API_KEY}"
+        headers["Content-Type"] = "application/json"
+
+        model_id = body["model"][body["model"].find(".") + 1 :]
+        payload = {**body, "model": model_id}
+        print(payload)
+
+        try:
+            r = requests.post(
+                url=f"{self.valves.OPENAI_API_BASE_URL}/chat/completions",
+                json=payload,
+                headers=headers,
+                stream=True,
+            )
+
+            r.raise_for_status()
+
+            if body["stream"]:
+                return r.iter_lines()
+            else:
+                return r.json()
+        except Exception as e:
+            return f"Error: {e}"
+`;
+
+	const saveHandler = async () => {
+		loading = true;
+		dispatch('save', {
+			id,
+			name,
+			meta,
+			content
+		});
+	};
+
+	const submitHandler = async () => {
+		if (codeEditor) {
+			const res = await codeEditor.formatPythonCodeHandler();
+
+			if (res) {
+				console.log('Code formatted successfully');
+				saveHandler();
+			}
+		}
+	};
+</script>
+
+<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
+	<div class="mx-auto w-full md:px-0 h-full">
+		<form
+			bind:this={formElement}
+			class=" flex flex-col max-h-[100dvh] h-full"
+			on:submit|preventDefault={() => {
+				if (edit) {
+					submitHandler();
+				} else {
+					showConfirm = true;
+				}
+			}}
+		>
+			<div class="mb-2.5">
+				<button
+					class="flex space-x-1"
+					on:click={() => {
+						goto('/workspace/functions');
+					}}
+					type="button"
+				>
+					<div class=" self-center">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							viewBox="0 0 20 20"
+							fill="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								fill-rule="evenodd"
+								d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
+								clip-rule="evenodd"
+							/>
+						</svg>
+					</div>
+					<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
+				</button>
+			</div>
+
+			<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
+				<div class="w-full mb-2 flex flex-col gap-1.5">
+					<div class="flex gap-2 w-full">
+						<input
+							class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
+							type="text"
+							placeholder="Function Name (e.g. My Filter)"
+							bind:value={name}
+							required
+						/>
+
+						<input
+							class="w-full px-3 py-2 text-sm font-medium disabled:text-gray-300 dark:disabled:text-gray-700 bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
+							type="text"
+							placeholder="Function ID (e.g. my_filter)"
+							bind:value={id}
+							required
+							disabled={edit}
+						/>
+					</div>
+					<input
+						class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
+						type="text"
+						placeholder="Function Description (e.g. A filter to remove profanity from text)"
+						bind:value={meta.description}
+						required
+					/>
+				</div>
+
+				<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
+					<CodeEditor
+						bind:value={content}
+						bind:this={codeEditor}
+						{boilerplate}
+						on:save={() => {
+							if (formElement) {
+								formElement.requestSubmit();
+							}
+						}}
+					/>
+				</div>
+
+				<div class="pb-3 flex justify-between">
+					<div class="flex-1 pr-3">
+						<div class="text-xs text-gray-500 line-clamp-2">
+							<span class=" font-semibold dark:text-gray-200">Warning:</span> Functions allow
+							arbitrary code execution <br />—
+							<span class=" font-medium dark:text-gray-400"
+								>don't install random functions from sources you don't trust.</span
+							>
+						</div>
+					</div>
+
+					<button
+						class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg"
+						type="submit"
+					>
+						{$i18n.t('Save')}
+					</button>
+				</div>
+			</div>
+		</form>
+	</div>
+</div>
+
+<ConfirmDialog
+	bind:show={showConfirm}
+	on:confirm={() => {
+		submitHandler();
+	}}
+>
+	<div class="text-sm text-gray-500">
+		<div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3">
+			<div>Please carefully review the following warnings:</div>
+
+			<ul class=" mt-1 list-disc pl-4 text-xs">
+				<li>Functions allow arbitrary code execution.</li>
+				<li>Do not install functions from sources you do not fully trust.</li>
+			</ul>
+		</div>
+
+		<div class="my-3">
+			I acknowledge that I have read and I understand the implications of my action. I am aware of
+			the risks associated with executing arbitrary code and I have verified the trustworthiness of
+			the source.
+		</div>
+	</div>
+</ConfirmDialog>

+ 60 - 0
src/lib/components/workspace/Models/FiltersSelector.svelte

@@ -0,0 +1,60 @@
+<script lang="ts">
+	import { getContext, onMount } from 'svelte';
+	import Checkbox from '$lib/components/common/Checkbox.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+
+	const i18n = getContext('i18n');
+
+	export let filters = [];
+	export let selectedFilterIds = [];
+
+	let _filters = {};
+
+	onMount(() => {
+		_filters = filters.reduce((acc, filter) => {
+			acc[filter.id] = {
+				...filter,
+				selected: selectedFilterIds.includes(filter.id)
+			};
+
+			return acc;
+		}, {});
+	});
+</script>
+
+<div>
+	<div class="flex w-full justify-between mb-1">
+		<div class=" self-center text-sm font-semibold">{$i18n.t('Filters')}</div>
+	</div>
+
+	<div class=" text-xs dark:text-gray-500">
+		{$i18n.t('To select filters here, add them to the "Functions" workspace first.')}
+	</div>
+
+	<!-- TODO: Filer order matters -->
+	<div class="flex flex-col">
+		{#if filters.length > 0}
+			<div class=" flex items-center mt-2 flex-wrap">
+				{#each Object.keys(_filters) as filter, filterIdx}
+					<div class=" flex items-center gap-2 mr-3">
+						<div class="self-center flex items-center">
+							<Checkbox
+								state={_filters[filter].selected ? 'checked' : 'unchecked'}
+								on:change={(e) => {
+									_filters[filter].selected = e.detail === 'checked';
+									selectedFilterIds = Object.keys(_filters).filter((t) => _filters[t].selected);
+								}}
+							/>
+						</div>
+
+						<div class=" py-0.5 text-sm w-full capitalize font-medium">
+							<Tooltip content={_filters[filter].meta.description}>
+								{_filters[filter].name}
+							</Tooltip>
+						</div>
+					</div>
+				{/each}
+			</div>
+		{/if}
+	</div>
+</div>

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

@@ -124,7 +124,11 @@
 				{/if}
 
 				<div class="flex items-center">
-					{$i18n.t(model?.info?.meta?.hidden ?? false ? 'Show Model' : 'Hide Model')}
+					{#if model?.info?.meta?.hidden ?? false}
+						{$i18n.t('Show Model')}
+					{:else}
+						{$i18n.t('Hide Model')}
+					{/if}
 				</div>
 			</DropdownMenu.Item>
 

+ 2 - 2
src/lib/components/workspace/Models/ToolsSelector.svelte

@@ -33,9 +33,9 @@
 
 	<div class="flex flex-col">
 		{#if tools.length > 0}
-			<div class=" flex items-center gap-2 mt-2">
+			<div class=" flex items-center mt-2 flex-wrap">
 				{#each Object.keys(_tools) as tool, toolIdx}
-					<div class=" flex items-center gap-2">
+					<div class=" flex items-center gap-2 mr-3">
 						<div class="self-center flex items-center">
 							<Checkbox
 								state={_tools[tool].selected ? 'checked' : 'unchecked'}

+ 14 - 4
src/lib/components/workspace/Tools.svelte

@@ -97,15 +97,25 @@
 					href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
 					class="flex items-center text-left"
 				>
-					<div class=" flex-1 self-center pl-5">
+					<div class=" flex-1 self-center pl-1">
 						<div class=" font-semibold flex items-center gap-1.5">
+							<div
+								class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+							>
+								TOOL
+							</div>
+
 							<div>
 								{tool.name}
 							</div>
-							<div class=" text-gray-500 text-xs font-medium">{tool.id}</div>
 						</div>
-						<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
-							{tool.meta.description}
+
+						<div class="flex gap-1.5 px-1">
+							<div class=" text-gray-500 text-xs font-medium">{tool.id}</div>
+
+							<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
+								{tool.meta.description}
+							</div>
 						</div>
 					</div>
 				</a>

+ 0 - 127
src/lib/components/workspace/Tools/CodeEditor.svelte

@@ -1,127 +0,0 @@
-<script lang="ts">
-	import CodeEditor from '$lib/components/common/CodeEditor.svelte';
-	import { createEventDispatcher } from 'svelte';
-
-	const dispatch = createEventDispatcher();
-
-	export let value = '';
-
-	let codeEditor;
-	let boilerplate = `import os
-import requests
-from datetime import datetime
-
-
-class Tools:
-    def __init__(self):
-        pass
-
-    # Add your custom tools using pure Python code here, make sure to add type hints
-    # Use Sphinx-style docstrings to document your tools, they will be used for generating tools specifications
-    # Please refer to function_calling_filter_pipeline.py file from pipelines project for an example
-
-    def get_user_name_and_email_and_id(self, __user__: dict = {}) -> str:
-        """
-        Get the user name, Email and ID from the user object.
-        """
-
-        # Do not include :param for __user__ in the docstring as it should not be shown in the tool's specification
-        # The session user object will be passed as a parameter when the function is called
-
-        print(__user__)
-        result = ""
-
-        if "name" in __user__:
-            result += f"User: {__user__['name']}"
-        if "id" in __user__:
-            result += f" (ID: {__user__['id']})"
-        if "email" in __user__:
-            result += f" (Email: {__user__['email']})"
-
-        if result == "":
-            result = "User: Unknown"
-
-        return result
-
-    def get_current_time(self) -> str:
-        """
-        Get the current time in a more human-readable format.
-        :return: The current time.
-        """
-
-        now = datetime.now()
-        current_time = now.strftime("%I:%M:%S %p")  # Using 12-hour format with AM/PM
-        current_date = now.strftime(
-            "%A, %B %d, %Y"
-        )  # Full weekday, month name, day, and year
-
-        return f"Current Date and Time = {current_date}, {current_time}"
-
-    def calculator(self, equation: str) -> str:
-        """
-        Calculate the result of an equation.
-        :param equation: The equation to calculate.
-        """
-
-        # Avoid using eval in production code
-        # https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
-        try:
-            result = eval(equation)
-            return f"{equation} = {result}"
-        except Exception as e:
-            print(e)
-            return "Invalid equation"
-
-    def get_current_weather(self, city: str) -> str:
-        """
-        Get the current weather for a given city.
-        :param city: The name of the city to get the weather for.
-        :return: The current weather information or an error message.
-        """
-        api_key = os.getenv("OPENWEATHER_API_KEY")
-        if not api_key:
-            return (
-                "API key is not set in the environment variable 'OPENWEATHER_API_KEY'."
-            )
-
-        base_url = "http://api.openweathermap.org/data/2.5/weather"
-        params = {
-            "q": city,
-            "appid": api_key,
-            "units": "metric",  # Optional: Use 'imperial' for Fahrenheit
-        }
-
-        try:
-            response = requests.get(base_url, params=params)
-            response.raise_for_status()  # Raise HTTPError for bad responses (4xx and 5xx)
-            data = response.json()
-
-            if data.get("cod") != 200:
-                return f"Error fetching weather data: {data.get('message')}"
-
-            weather_description = data["weather"][0]["description"]
-            temperature = data["main"]["temp"]
-            humidity = data["main"]["humidity"]
-            wind_speed = data["wind"]["speed"]
-
-            return f"Weather in {city}: {temperature}°C"
-        except requests.RequestException as e:
-            return f"Error fetching weather data: {str(e)}"
-`;
-
-	export const formatHandler = async () => {
-		if (codeEditor) {
-			return await codeEditor.formatPythonCodeHandler();
-		}
-		return false;
-	};
-</script>
-
-<CodeEditor
-	bind:value
-	{boilerplate}
-	bind:this={codeEditor}
-	on:save={() => {
-		dispatch('save');
-	}}
-/>

+ 104 - 2
src/lib/components/workspace/Tools/ToolkitEditor.svelte

@@ -3,7 +3,7 @@
 
 	const i18n = getContext('i18n');
 
-	import CodeEditor from './CodeEditor.svelte';
+	import CodeEditor from '$lib/components/common/CodeEditor.svelte';
 	import { goto } from '$app/navigation';
 	import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 
@@ -28,6 +28,107 @@
 	}
 
 	let codeEditor;
+	let boilerplate = `import os
+import requests
+from datetime import datetime
+
+
+class Tools:
+    def __init__(self):
+        pass
+
+    # Add your custom tools using pure Python code here, make sure to add type hints
+    # Use Sphinx-style docstrings to document your tools, they will be used for generating tools specifications
+    # Please refer to function_calling_filter_pipeline.py file from pipelines project for an example
+
+    def get_user_name_and_email_and_id(self, __user__: dict = {}) -> str:
+        """
+        Get the user name, Email and ID from the user object.
+        """
+
+        # Do not include :param for __user__ in the docstring as it should not be shown in the tool's specification
+        # The session user object will be passed as a parameter when the function is called
+
+        print(__user__)
+        result = ""
+
+        if "name" in __user__:
+            result += f"User: {__user__['name']}"
+        if "id" in __user__:
+            result += f" (ID: {__user__['id']})"
+        if "email" in __user__:
+            result += f" (Email: {__user__['email']})"
+
+        if result == "":
+            result = "User: Unknown"
+
+        return result
+
+    def get_current_time(self) -> str:
+        """
+        Get the current time in a more human-readable format.
+        :return: The current time.
+        """
+
+        now = datetime.now()
+        current_time = now.strftime("%I:%M:%S %p")  # Using 12-hour format with AM/PM
+        current_date = now.strftime(
+            "%A, %B %d, %Y"
+        )  # Full weekday, month name, day, and year
+
+        return f"Current Date and Time = {current_date}, {current_time}"
+
+    def calculator(self, equation: str) -> str:
+        """
+        Calculate the result of an equation.
+        :param equation: The equation to calculate.
+        """
+
+        # Avoid using eval in production code
+        # https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
+        try:
+            result = eval(equation)
+            return f"{equation} = {result}"
+        except Exception as e:
+            print(e)
+            return "Invalid equation"
+
+    def get_current_weather(self, city: str) -> str:
+        """
+        Get the current weather for a given city.
+        :param city: The name of the city to get the weather for.
+        :return: The current weather information or an error message.
+        """
+        api_key = os.getenv("OPENWEATHER_API_KEY")
+        if not api_key:
+            return (
+                "API key is not set in the environment variable 'OPENWEATHER_API_KEY'."
+            )
+
+        base_url = "http://api.openweathermap.org/data/2.5/weather"
+        params = {
+            "q": city,
+            "appid": api_key,
+            "units": "metric",  # Optional: Use 'imperial' for Fahrenheit
+        }
+
+        try:
+            response = requests.get(base_url, params=params)
+            response.raise_for_status()  # Raise HTTPError for bad responses (4xx and 5xx)
+            data = response.json()
+
+            if data.get("cod") != 200:
+                return f"Error fetching weather data: {data.get('message')}"
+
+            weather_description = data["weather"][0]["description"]
+            temperature = data["main"]["temp"]
+            humidity = data["main"]["humidity"]
+            wind_speed = data["wind"]["speed"]
+
+            return f"Weather in {city}: {temperature}°C"
+        except requests.RequestException as e:
+            return f"Error fetching weather data: {str(e)}"
+`;
 
 	const saveHandler = async () => {
 		loading = true;
@@ -41,7 +142,7 @@
 
 	const submitHandler = async () => {
 		if (codeEditor) {
-			const res = await codeEditor.formatHandler();
+			const res = await codeEditor.formatPythonCodeHandler();
 
 			if (res) {
 				console.log('Code formatted successfully');
@@ -123,6 +224,7 @@
 					<CodeEditor
 						bind:value={content}
 						bind:this={codeEditor}
+						{boilerplate}
 						on:save={() => {
 							if (formElement) {
 								formElement.requestSubmit();

+ 28 - 2
src/lib/i18n/locales/ar-BH/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "يسمح",
 	"Allow Chat Deletion": "يستطيع حذف المحادثات",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "الأحرف الأبجدية الرقمية والواصلات",
 	"Already have an account?": "هل تملك حساب ؟",
 	"an assistant": "مساعد",
@@ -81,6 +82,7 @@
 	"Capabilities": "قدرات",
 	"Change Password": "تغير الباسورد",
 	"Chat": "المحادثة",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "UI الدردشة",
 	"Chat direction": "اتجاه المحادثة",
 	"Chat History": "تاريخ المحادثة",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "أضغط هنا للمساعدة",
 	"Click here to": "أضغط هنا الانتقال",
+	"Click here to download user import template file.": "",
 	"Click here to select": "أضغط هنا للاختيار",
 	"Click here to select a csv file.": "أضغط هنا للاختيار ملف csv",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "ComfyUI الرابط مطلوب",
 	"Command": "الأوامر",
 	"Concurrent Requests": "الطلبات المتزامنة",
+	"Confirm": "",
 	"Confirm Password": "تأكيد كلمة المرور",
+	"Confirm your action": "",
 	"Connections": "اتصالات",
 	"Contact Admin for WebUI Access": "",
 	"Content": "الاتصال",
@@ -130,6 +135,8 @@
 	"Create new secret key": "عمل سر جديد",
 	"Created at": "أنشئت في",
 	"Created At": "أنشئت من",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "الموديل المختار",
 	"Current Password": "كلمة السر الحالية",
 	"Custom": "مخصص",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "حذف جميع الدردشات",
 	"Delete chat": "حذف المحادثه",
 	"Delete Chat": "حذف المحادثه.",
+	"Delete chat?": "",
 	"delete this link": "أحذف هذا الرابط",
 	"Delete User": "حذف المستخدم",
 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} حذف",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "تصدير جميع الدردشات",
 	"Export Documents Mapping": "تصدير وثائق الخرائط",
+	"Export Functions": "",
 	"Export Models": "نماذج التصدير",
 	"Export Prompts": "مطالبات التصدير",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "فبراير",
 	"Feel free to add specific details": "لا تتردد في إضافة تفاصيل محددة",
+	"File": "",
 	"File Mode": "وضع الملف",
 	"File not found.": "لم يتم العثور على الملف.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "تم اكتشاف انتحال بصمة الإصبع: غير قادر على استخدام الأحرف الأولى كصورة رمزية. الافتراضي لصورة الملف الشخصي الافتراضية.",
 	"Fluidly stream large external response chunks": "دفق قطع الاستجابة الخارجية الكبيرة بسلاسة",
 	"Focus chat input": "التركيز على إدخال الدردشة",
 	"Followed instructions perfectly": "اتبعت التعليمات على أكمل وجه",
+	"Form": "",
 	"Format your variables using square brackets like this:": "قم بتنسيق المتغيرات الخاصة بك باستخدام الأقواس المربعة مثل هذا:",
 	"Frequency Penalty": "عقوبة التردد",
+	"Functions": "",
 	"General": "عام",
 	"General Settings": "الاعدادات العامة",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": " {{name}} مرحبا",
 	"Help": "مساعدة",
 	"Hide": "أخفاء",
+	"Hide Model": "",
 	"How can I help you today?": "كيف استطيع مساعدتك اليوم؟",
 	"Hybrid Search": "البحث الهجين",
 	"Image Generation (Experimental)": "توليد الصور (تجريبي)",
@@ -262,6 +276,7 @@
 	"Images": "الصور",
 	"Import Chats": "استيراد الدردشات",
 	"Import Documents Mapping": "استيراد خرائط المستندات",
+	"Import Functions": "",
 	"Import Models": "استيراد النماذج",
 	"Import Prompts": "مطالبات الاستيراد",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "يُسمح فقط بالأحرف الأبجدية الرقمية والواصلات في سلسلة الأمر.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "خطاء! تمسك بقوة! ملفاتك لا تزال في فرن المعالجة. نحن نطبخهم إلى حد الكمال. يرجى التحلي بالصبر وسنخبرك عندما يصبحون جاهزين.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "خطاء! يبدو أن عنوان URL غير صالح. يرجى التحقق مرة أخرى والمحاولة مرة أخرى.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "خطاء! أنت تستخدم طريقة غير مدعومة (الواجهة الأمامية فقط). يرجى تقديم واجهة WebUI من الواجهة الخلفية.",
 	"Open": "فتح",
 	"Open AI": "AI فتح",
@@ -406,6 +422,7 @@
 	"Reranking Model": "إعادة تقييم النموذج",
 	"Reranking model disabled": "تم تعطيل نموذج إعادة الترتيب",
 	"Reranking model set to \"{{reranking_model}}\"": "تم ضبط نموذج إعادة الترتيب على \"{{reranking_model}}\"",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "إعادة تعيين تخزين المتجهات",
 	"Response AutoCopy to Clipboard": "النسخ التلقائي للاستجابة إلى الحافظة",
@@ -425,6 +442,7 @@
 	"Search a model": "البحث عن موديل",
 	"Search Chats": "البحث في الدردشات",
 	"Search Documents": "البحث المستندات",
+	"Search Functions": "",
 	"Search Models": "نماذج البحث",
 	"Search Prompts": "أبحث حث",
 	"Search Query Generation Prompt": "",
@@ -478,6 +496,7 @@
 	"short-summary": "ملخص قصير",
 	"Show": "عرض",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "إظهار الاختصارات",
 	"Showcased creativity": "أظهر الإبداع",
 	"sidebar": "الشريط الجانبي",
@@ -499,6 +518,7 @@
 	"System": "النظام",
 	"System Prompt": "محادثة النظام",
 	"Tags": "الوسوم",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "أخبرنا المزيد:",
 	"Temperature": "درجة حرارة",
@@ -510,9 +530,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "يجب أن تكون النتيجة قيمة تتراوح بين 0.0 (0%) و1.0 (100%).",
 	"Theme": "الثيم",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "وهذا يضمن حفظ محادثاتك القيمة بشكل آمن في قاعدة بياناتك الخلفية. شكرًا لك!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "لا تتم مزامنة هذا الإعداد عبر المتصفحات أو الأجهزة.",
+	"This will delete": "",
 	"Thorough explanation": "شرح شامل",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "ملاحضة: قم بتحديث عدة فتحات متغيرة على التوالي عن طريق الضغط على مفتاح tab في مدخلات الدردشة بعد كل استبدال.",
 	"Title": "العنوان",
@@ -526,6 +548,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "الى كتابة المحادثه",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "اليوم",
 	"Toggle settings": "فتح وأغلاق الاعدادات",
@@ -541,10 +564,13 @@
 	"Type": "نوع",
 	"Type Hugging Face Resolve (Download) URL": "اكتب عنوان URL لحل مشكلة الوجه (تنزيل).",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "{{provider}}خطاء أوه! حدثت مشكلة في الاتصال بـ ",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "نوع ملف غير معروف '{{file_type}}', ولكن القبول والتعامل كنص عادي ",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "تحديث ونسخ الرابط",
 	"Update password": "تحديث كلمة المرور",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "GGUF رفع موديل نوع",
 	"Upload Files": "تحميل الملفات",
 	"Upload Pipeline": "",
@@ -563,6 +589,7 @@
 	"variable": "المتغير",
 	"variable to have them replaced with clipboard content.": "متغير لاستبدالها بمحتوى الحافظة.",
 	"Version": "إصدار",
+	"Voice": "",
 	"Warning": "تحذير",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "تحذير: إذا قمت بتحديث أو تغيير نموذج التضمين الخاص بك، فستحتاج إلى إعادة استيراد كافة المستندات.",
 	"Web": "Web",
@@ -572,7 +599,6 @@
 	"Web Search": "بحث الويب",
 	"Web Search Engine": "محرك بحث الويب",
 	"Webhook URL": "Webhook الرابط",
-	"WebUI Add-ons": "WebUI الأضافات",
 	"WebUI Settings": "WebUI اعدادات",
 	"WebUI will make requests to": "سوف يقوم WebUI بتقديم طلبات ل",
 	"What’s New in": "ما هو الجديد",

+ 28 - 2
src/lib/i18n/locales/bg-BG/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "Позволи",
 	"Allow Chat Deletion": "Позволи Изтриване на Чат",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "алфанумерични знаци и тире",
 	"Already have an account?": "Вече имате акаунт? ",
 	"an assistant": "асистент",
@@ -81,6 +82,7 @@
 	"Capabilities": "Възможности",
 	"Change Password": "Промяна на Парола",
 	"Chat": "Чат",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "UI за чат бублон",
 	"Chat direction": "Направление на чата",
 	"Chat History": "Чат История",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "Натиснете тук за помощ.",
 	"Click here to": "Натиснете тук за",
+	"Click here to download user import template file.": "",
 	"Click here to select": "Натиснете тук, за да изберете",
 	"Click here to select a csv file.": "Натиснете тук, за да изберете csv файл.",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "ComfyUI Base URL е задължително.",
 	"Command": "Команда",
 	"Concurrent Requests": "Едновременни искания",
+	"Confirm": "",
 	"Confirm Password": "Потвърди Парола",
+	"Confirm your action": "",
 	"Connections": "Връзки",
 	"Contact Admin for WebUI Access": "",
 	"Content": "Съдържание",
@@ -130,6 +135,8 @@
 	"Create new secret key": "Създаване на нов секретен ключ",
 	"Created at": "Създадено на",
 	"Created At": "Създадено на",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "Текущ модел",
 	"Current Password": "Текуща Парола",
 	"Custom": "Персонализиран",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "Изтриване на всички чатове",
 	"Delete chat": "Изтриване на чат",
 	"Delete Chat": "Изтриване на Чат",
+	"Delete chat?": "",
 	"delete this link": "Изтриване на този линк",
 	"Delete User": "Изтриване на потребител",
 	"Deleted {{deleteModelTag}}": "Изтрито {{deleteModelTag}}",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "Експортване на чатове",
 	"Export Documents Mapping": "Експортване на документен мапинг",
+	"Export Functions": "",
 	"Export Models": "Експортиране на модели",
 	"Export Prompts": "Експортване на промптове",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "Февруари",
 	"Feel free to add specific details": "Feel free to add specific details",
+	"File": "",
 	"File Mode": "Файл Мод",
 	"File not found.": "Файл не е намерен.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Потвърждаване на отпечатък: Не може да се използва инициализационна буква като аватар. Потребителят се връща към стандартна аватарка.",
 	"Fluidly stream large external response chunks": "Плавно предаване на големи части от външен отговор",
 	"Focus chat input": "Фокусиране на чат вход",
 	"Followed instructions perfectly": "Следвайте инструкциите перфектно",
+	"Form": "",
 	"Format your variables using square brackets like this:": "Форматирайте вашите променливи, като използвате квадратни скоби, както следва:",
 	"Frequency Penalty": "Наказание за честота",
+	"Functions": "",
 	"General": "Основни",
 	"General Settings": "Основни Настройки",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "Здравей, {{name}}",
 	"Help": "Помощ",
 	"Hide": "Скрий",
+	"Hide Model": "",
 	"How can I help you today?": "Как мога да ви помогна днес?",
 	"Hybrid Search": "Hybrid Search",
 	"Image Generation (Experimental)": "Генерация на изображения (Експериментално)",
@@ -262,6 +276,7 @@
 	"Images": "Изображения",
 	"Import Chats": "Импортване на чатове",
 	"Import Documents Mapping": "Импортване на документен мапинг",
+	"Import Functions": "",
 	"Import Models": "Импортиране на модели",
 	"Import Prompts": "Импортване на промптове",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "Само алфанумерични знаци и тире са разрешени в командния низ.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Упс! Задръжте! Файловете ви все още са в пещта за обработка. Готвим ги до съвършенство. Моля, бъдете търпеливи и ще ви уведомим, когато са готови.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Изглежда URL адресът е невалиден. Моля, проверете отново и опитайте пак.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Упс! Използвате неподдържан метод (само фронтенд). Моля, сервирайте WebUI от бекенда.",
 	"Open": "Отвори",
 	"Open AI": "Open AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "Reranking Model",
 	"Reranking model disabled": "Reranking model disabled",
 	"Reranking model set to \"{{reranking_model}}\"": "Reranking model set to \"{{reranking_model}}\"",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "Ресет Vector Storage",
 	"Response AutoCopy to Clipboard": "Аувтоматично копиране на отговор в клипборда",
@@ -425,6 +442,7 @@
 	"Search a model": "Търси модел",
 	"Search Chats": "Търсене на чатове",
 	"Search Documents": "Търси Документи",
+	"Search Functions": "",
 	"Search Models": "Търсене на модели",
 	"Search Prompts": "Търси Промптове",
 	"Search Query Generation Prompt": "",
@@ -474,6 +492,7 @@
 	"short-summary": "short-summary",
 	"Show": "Покажи",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "Покажи",
 	"Showcased creativity": "Показана креативност",
 	"sidebar": "sidebar",
@@ -495,6 +514,7 @@
 	"System": "Система",
 	"System Prompt": "Системен Промпт",
 	"Tags": "Тагове",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "Повече информация:",
 	"Temperature": "Температура",
@@ -506,9 +526,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "The score should be a value between 0.0 (0%) and 1.0 (100%).",
 	"Theme": "Тема",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Това гарантира, че ценните ви разговори се запазват сигурно във вашата бекенд база данни. Благодарим ви!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "Тази настройка не се синхронизира между браузъри или устройства.",
+	"This will delete": "",
 	"Thorough explanation": "Това е подробно описание.",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Съвет: Актуализирайте няколко слота за променливи последователно, като натискате клавиша Tab в чат входа след всяка подмяна.",
 	"Title": "Заглавие",
@@ -522,6 +544,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "към чат входа.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "днес",
 	"Toggle settings": "Toggle settings",
@@ -537,10 +560,13 @@
 	"Type": "Вид",
 	"Type Hugging Face Resolve (Download) URL": "Въведете Hugging Face Resolve (Download) URL",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "О, не! Възникна проблем при свързването с {{provider}}.",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Непознат файлов тип '{{file_type}}', но се приема и обработва като текст",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "Обнови и копирай връзка",
 	"Update password": "Обновяване на парола",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "Качване на GGUF модел",
 	"Upload Files": "Качване на файлове",
 	"Upload Pipeline": "",
@@ -559,6 +585,7 @@
 	"variable": "променлива",
 	"variable to have them replaced with clipboard content.": "променливи да се заменят съдържанието от клипборд.",
 	"Version": "Версия",
+	"Voice": "",
 	"Warning": "Предупреждение",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Предупреждение: Ако актуализирате или промените вашия модел за вграждане, трябва да повторите импортирането на всички документи.",
 	"Web": "Уеб",
@@ -568,7 +595,6 @@
 	"Web Search": "Търсене в уеб",
 	"Web Search Engine": "Уеб търсачка",
 	"Webhook URL": "Уебхук URL",
-	"WebUI Add-ons": "WebUI Добавки",
 	"WebUI Settings": "WebUI Настройки",
 	"WebUI will make requests to": "WebUI ще направи заявки към",
 	"What’s New in": "Какво е новото в",

+ 28 - 2
src/lib/i18n/locales/bn-BD/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "অনুমোদন",
 	"Allow Chat Deletion": "চ্যাট ডিলিট করতে দিন",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "ইংরেজি অক্ষর, সংখ্যা এবং হাইফেন",
 	"Already have an account?": "আগে থেকেই একাউন্ট আছে?",
 	"an assistant": "একটা এসিস্ট্যান্ট",
@@ -81,6 +82,7 @@
 	"Capabilities": "সক্ষমতা",
 	"Change Password": "পাসওয়ার্ড পরিবর্তন করুন",
 	"Chat": "চ্যাট",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "চ্যাট বাবল UI",
 	"Chat direction": "চ্যাট দিকনির্দেশ",
 	"Chat History": "চ্যাট হিস্টোরি",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "সাহায্যের জন্য এখানে ক্লিক করুন",
 	"Click here to": "এখানে ক্লিক করুন",
+	"Click here to download user import template file.": "",
 	"Click here to select": "নির্বাচন করার জন্য এখানে ক্লিক করুন",
 	"Click here to select a csv file.": "একটি csv ফাইল নির্বাচন করার জন্য এখানে ক্লিক করুন",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "ComfyUI Base URL আবশ্যক।",
 	"Command": "কমান্ড",
 	"Concurrent Requests": "সমকালীন অনুরোধ",
+	"Confirm": "",
 	"Confirm Password": "পাসওয়ার্ড নিশ্চিত করুন",
+	"Confirm your action": "",
 	"Connections": "কানেকশনগুলো",
 	"Contact Admin for WebUI Access": "",
 	"Content": "বিষয়বস্তু",
@@ -130,6 +135,8 @@
 	"Create new secret key": "একটি নতুন সিক্রেট কী তৈরি করুন",
 	"Created at": "নির্মানকাল",
 	"Created At": "নির্মানকাল",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "বর্তমান মডেল",
 	"Current Password": "বর্তমান পাসওয়ার্ড",
 	"Custom": "কাস্টম",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "সব চ্যাট মুছে ফেলুন",
 	"Delete chat": "চ্যাট মুছে ফেলুন",
 	"Delete Chat": "চ্যাট মুছে ফেলুন",
+	"Delete chat?": "",
 	"delete this link": "এই লিংক মুছে ফেলুন",
 	"Delete User": "ইউজার মুছে ফেলুন",
 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} মুছে ফেলা হয়েছে",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "চ্যাটগুলো এক্সপোর্ট করুন",
 	"Export Documents Mapping": "ডকুমেন্টসমূহ ম্যাপিং এক্সপোর্ট করুন",
+	"Export Functions": "",
 	"Export Models": "রপ্তানি মডেল",
 	"Export Prompts": "প্রম্পটগুলো একপোর্ট করুন",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "ফেব্রুয়ারি",
 	"Feel free to add specific details": "নির্দিষ্ট বিবরণ যোগ করতে বিনা দ্বিধায়",
+	"File": "",
 	"File Mode": "ফাইল মোড",
 	"File not found.": "ফাইল পাওয়া যায়নি",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "ফিঙ্গারপ্রিন্ট স্পুফিং ধরা পড়েছে: অ্যাভাটার হিসেবে নামের আদ্যক্ষর ব্যবহার করা যাচ্ছে না। ডিফল্ট প্রোফাইল পিকচারে ফিরিয়ে নেয়া হচ্ছে।",
 	"Fluidly stream large external response chunks": "বড় এক্সটার্নাল রেসপন্স চাঙ্কগুলো মসৃণভাবে প্রবাহিত করুন",
 	"Focus chat input": "চ্যাট ইনপুট ফোকাস করুন",
 	"Followed instructions perfectly": "নির্দেশাবলী নিখুঁতভাবে অনুসরণ করা হয়েছে",
+	"Form": "",
 	"Format your variables using square brackets like this:": "আপনার ভেরিয়বলগুলো এভাবে স্কয়ার ব্রাকেটের মাধ্যমে সাজান",
 	"Frequency Penalty": "ফ্রিকোয়েন্সি পেনাল্টি",
+	"Functions": "",
 	"General": "সাধারণ",
 	"General Settings": "সাধারণ সেটিংসমূহ",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "হ্যালো, {{name}}",
 	"Help": "সহায়তা",
 	"Hide": "লুকান",
+	"Hide Model": "",
 	"How can I help you today?": "আপনাকে আজ কিভাবে সাহায্য করতে পারি?",
 	"Hybrid Search": "হাইব্রিড অনুসন্ধান",
 	"Image Generation (Experimental)": "ইমেজ জেনারেশন (পরিক্ষামূলক)",
@@ -262,6 +276,7 @@
 	"Images": "ছবিসমূহ",
 	"Import Chats": "চ্যাটগুলি ইমপোর্ট করুন",
 	"Import Documents Mapping": "ডকুমেন্টসমূহ ম্যাপিং ইমপোর্ট করুন",
+	"Import Functions": "",
 	"Import Models": "মডেল আমদানি করুন",
 	"Import Prompts": "প্রম্পটগুলো ইমপোর্ট করুন",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "কমান্ড স্ট্রিং-এ শুধুমাত্র ইংরেজি অক্ষর, সংখ্যা এবং হাইফেন ব্যবহার করা যাবে।",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "আহা! আরেকটু ধৈর্য্য ধরুন! আপনার ফাইলগুলো এখনো প্রোসেস চলছে, আমরা ওগুলোকে সেরা প্রক্রিয়াজাত করছি। তৈরি হয়ে গেলে আপনাকে জানিয়ে দেয়া হবে।",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "ওহ, মনে হচ্ছে ইউআরএলটা ইনভ্যালিড। দয়া করে আর চেক করে চেষ্টা করুন।",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "আপনি একটা আনসাপোর্টেড পদ্ধতি (শুধু ফ্রন্টএন্ড) ব্যবহার করছেন। দয়া করে WebUI ব্যাকএন্ড থেকে চালনা করুন।",
 	"Open": "খোলা",
 	"Open AI": "Open AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "রির্যাক্টিং মডেল",
 	"Reranking model disabled": "রির্যাক্টিং মডেল নিষ্ক্রিয় করা",
 	"Reranking model set to \"{{reranking_model}}\"": "রির ্যাঙ্কিং মডেল \"{{reranking_model}}\" -এ সেট করা আছে",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "ভেক্টর স্টোরেজ রিসেট করুন",
 	"Response AutoCopy to Clipboard": "রেসপন্সগুলো স্বয়ংক্রিভাবে ক্লিপবোর্ডে কপি হবে",
@@ -425,6 +442,7 @@
 	"Search a model": "মডেল অনুসন্ধান করুন",
 	"Search Chats": "চ্যাট অনুসন্ধান করুন",
 	"Search Documents": "ডকুমেন্টসমূহ অনুসন্ধান করুন",
+	"Search Functions": "",
 	"Search Models": "অনুসন্ধান মডেল",
 	"Search Prompts": "প্রম্পটসমূহ অনুসন্ধান করুন",
 	"Search Query Generation Prompt": "",
@@ -474,6 +492,7 @@
 	"short-summary": "সংক্ষিপ্ত বিবরণ",
 	"Show": "দেখান",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "শর্টকাটগুলো দেখান",
 	"Showcased creativity": "সৃজনশীলতা প্রদর্শন",
 	"sidebar": "সাইডবার",
@@ -495,6 +514,7 @@
 	"System": "সিস্টেম",
 	"System Prompt": "সিস্টেম প্রম্পট",
 	"Tags": "ট্যাগসমূহ",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "আরও বলুন:",
 	"Temperature": "তাপমাত্রা",
@@ -506,9 +526,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "স্কোর একটি 0.0 (0%) এবং 1.0 (100%) এর মধ্যে একটি মান হওয়া উচিত।",
 	"Theme": "থিম",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "এটা নিশ্চিত করে যে, আপনার গুরুত্বপূর্ণ আলোচনা নিরাপদে আপনার ব্যাকএন্ড ডেটাবেজে সংরক্ষিত আছে। ধন্যবাদ!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "এই সেটিং অন্যন্য ব্রাউজার বা ডিভাইসের সাথে সিঙ্ক্রোনাইজ নয় না।",
+	"This will delete": "",
 	"Thorough explanation": "পুঙ্খানুপুঙ্খ ব্যাখ্যা",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "পরামর্শ: একাধিক ভেরিয়েবল স্লট একের পর এক রিপ্লেস করার জন্য চ্যাট ইনপুটে কিবোর্ডের Tab বাটন ব্যবহার করুন।",
 	"Title": "শিরোনাম",
@@ -522,6 +544,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "চ্যাট ইনপুটে",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "আজ",
 	"Toggle settings": "সেটিংস টোগল",
@@ -537,10 +560,13 @@
 	"Type": "টাইপ",
 	"Type Hugging Face Resolve (Download) URL": "Hugging Face থেকে ডাউনলোড করার ইউআরএল টাইপ করুন",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "ওহ-হো! {{provider}} এর সাথে কানেকশনে সমস্যা হয়েছে।",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "অপরিচিত ফাইল ফরম্যাট '{{file_type}}', তবে প্লেইন টেক্সট হিসেবে গ্রহণ করা হলো",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "আপডেট এবং লিংক কপি করুন",
 	"Update password": "পাসওয়ার্ড আপডেট করুন",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "একটি GGUF মডেল আপলোড করুন",
 	"Upload Files": "ফাইল আপলোড করুন",
 	"Upload Pipeline": "",
@@ -559,6 +585,7 @@
 	"variable": "ভেরিয়েবল",
 	"variable to have them replaced with clipboard content.": "ক্লিপবোর্ডের কন্টেন্ট দিয়ে যেই ভেরিয়েবল রিপ্লেস করা যাবে।",
 	"Version": "ভার্সন",
+	"Voice": "",
 	"Warning": "সতর্কীকরণ",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "সতর্কীকরণ: আপনি যদি আপনার এম্বেডিং মডেল আপডেট বা পরিবর্তন করেন, তাহলে আপনাকে সমস্ত নথি পুনরায় আমদানি করতে হবে।.",
 	"Web": "ওয়েব",
@@ -568,7 +595,6 @@
 	"Web Search": "ওয়েব অনুসন্ধান",
 	"Web Search Engine": "ওয়েব সার্চ ইঞ্জিন",
 	"Webhook URL": "ওয়েবহুক URL",
-	"WebUI Add-ons": "WebUI এড-অনসমূহ",
 	"WebUI Settings": "WebUI সেটিংসমূহ",
 	"WebUI will make requests to": "WebUI যেখানে রিকোয়েস্ট পাঠাবে",
 	"What’s New in": "এতে নতুন কী",

+ 28 - 2
src/lib/i18n/locales/ca-ES/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "Permet",
 	"Allow Chat Deletion": "Permet la Supressió del Xat",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "caràcters alfanumèrics i guions",
 	"Already have an account?": "Ja tens un compte?",
 	"an assistant": "un assistent",
@@ -81,6 +82,7 @@
 	"Capabilities": "Capacitats",
 	"Change Password": "Canvia la Contrasenya",
 	"Chat": "Xat",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "Chat Bubble UI",
 	"Chat direction": "Direcció del Xat",
 	"Chat History": "Històric del Xat",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "Fes clic aquí per ajuda.",
 	"Click here to": "Fes clic aquí per",
+	"Click here to download user import template file.": "",
 	"Click here to select": "Fes clic aquí per seleccionar",
 	"Click here to select a csv file.": "Fes clic aquí per seleccionar un fitxer csv.",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "URL base de ComfyUI és obligatòria.",
 	"Command": "Comanda",
 	"Concurrent Requests": "Sol·licituds simultànies",
+	"Confirm": "",
 	"Confirm Password": "Confirma la Contrasenya",
+	"Confirm your action": "",
 	"Connections": "Connexions",
 	"Contact Admin for WebUI Access": "",
 	"Content": "Contingut",
@@ -130,6 +135,8 @@
 	"Create new secret key": "Crea una nova clau secreta",
 	"Created at": "Creat el",
 	"Created At": "Creat el",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "Model Actual",
 	"Current Password": "Contrasenya Actual",
 	"Custom": "Personalitzat",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "Suprimir tots els xats",
 	"Delete chat": "Esborra xat",
 	"Delete Chat": "Esborra Xat",
+	"Delete chat?": "",
 	"delete this link": "Esborra aquest enllaç",
 	"Delete User": "Esborra Usuari",
 	"Deleted {{deleteModelTag}}": "Esborrat {{deleteModelTag}}",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "Exporta Xats",
 	"Export Documents Mapping": "Exporta el Mapatge de Documents",
+	"Export Functions": "",
 	"Export Models": "Models d'exportació",
 	"Export Prompts": "Exporta Prompts",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "Febrer",
 	"Feel free to add specific details": "Siusplau, afegeix detalls específics",
+	"File": "",
 	"File Mode": "Mode Arxiu",
 	"File not found.": "Arxiu no trobat.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "S'ha detectat la suplantació d'identitat d'empremtes digitals: no es poden utilitzar les inicials com a avatar. Per defecte a la imatge de perfil predeterminada.",
 	"Fluidly stream large external response chunks": "Transmita con fluidez grandes fragmentos de respuesta externa",
 	"Focus chat input": "Enfoca l'entrada del xat",
 	"Followed instructions perfectly": "Siguiu les instruccions perfeicte",
+	"Form": "",
 	"Format your variables using square brackets like this:": "Formata les teves variables utilitzant claudàtors així:",
 	"Frequency Penalty": "Pena de freqüència",
+	"Functions": "",
 	"General": "General",
 	"General Settings": "Configuració General",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "Hola, {{name}}",
 	"Help": "Ajuda",
 	"Hide": "Amaga",
+	"Hide Model": "",
 	"How can I help you today?": "Com et puc ajudar avui?",
 	"Hybrid Search": "Cerca Hibrida",
 	"Image Generation (Experimental)": "Generació d'Imatges (Experimental)",
@@ -262,6 +276,7 @@
 	"Images": "Imatges",
 	"Import Chats": "Importa Xats",
 	"Import Documents Mapping": "Importa el Mapa de Documents",
+	"Import Functions": "",
 	"Import Models": "Models d'importació",
 	"Import Prompts": "Importa Prompts",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "Només es permeten caràcters alfanumèrics i guions en la cadena de comandes.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Ui! Aguanta! Els teus fitxers encara estan en el forn de processament. Els estem cuinant a la perfecció. Si us plau, tingues paciència i t'avisarem quan estiguin llestos.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Ui! Sembla que l'URL és invàlida. Si us plau, revisa-ho i prova de nou.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ui! Estàs utilitzant un mètode no suportat (només frontend). Si us plau, serveix la WebUI des del backend.",
 	"Open": "Obre",
 	"Open AI": "Open AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "Model de Reranking desactivat",
 	"Reranking model disabled": "Model de Reranking desactivat",
 	"Reranking model set to \"{{reranking_model}}\"": "Model de Reranking establert a \"{{reranking_model}}\"",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "Reinicia l'Emmagatzematge de Vectors",
 	"Response AutoCopy to Clipboard": "Resposta AutoCopiar al Portapapers",
@@ -425,6 +442,7 @@
 	"Search a model": "Cerca un model",
 	"Search Chats": "Cercar xats",
 	"Search Documents": "Cerca Documents",
+	"Search Functions": "",
 	"Search Models": "Models de cerca",
 	"Search Prompts": "Cerca Prompts",
 	"Search Query Generation Prompt": "",
@@ -475,6 +493,7 @@
 	"short-summary": "resum curt",
 	"Show": "Mostra",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "Mostra dreceres",
 	"Showcased creativity": "Mostra la creativitat",
 	"sidebar": "barra lateral",
@@ -496,6 +515,7 @@
 	"System": "Sistema",
 	"System Prompt": "Prompt del Sistema",
 	"Tags": "Etiquetes",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "Dóna'ns més informació:",
 	"Temperature": "Temperatura",
@@ -507,9 +527,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "El puntuatge ha de ser un valor entre 0.0 (0%) i 1.0 (100%).",
 	"Theme": "Tema",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Això assegura que les teves converses valuoses queden segurament guardades a la teva base de dades backend. Gràcies!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "Aquesta configuració no es sincronitza entre navegadors ni dispositius.",
+	"This will delete": "",
 	"Thorough explanation": "Explacació exhaustiva",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Consell: Actualitza diversos espais de variables consecutivament prement la tecla de tabulació en l'entrada del xat després de cada reemplaçament.",
 	"Title": "Títol",
@@ -523,6 +545,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "a l'entrada del xat.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "Avui",
 	"Toggle settings": "Commuta configuracions",
@@ -538,10 +561,13 @@
 	"Type": "Tipus",
 	"Type Hugging Face Resolve (Download) URL": "Escriu URL de Resolució (Descàrrega) de Hugging Face",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "Uf! Hi va haver un problema connectant-se a {{provider}}.",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tipus d'Arxiu Desconegut '{{file_type}}', però acceptant i tractant com a text pla",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "Actualitza i Copia enllaç",
 	"Update password": "Actualitza contrasenya",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "Puja un model GGUF",
 	"Upload Files": "Pujar fitxers",
 	"Upload Pipeline": "",
@@ -560,6 +586,7 @@
 	"variable": "variable",
 	"variable to have them replaced with clipboard content.": "variable per tenir-les reemplaçades amb el contingut del porta-retalls.",
 	"Version": "Versió",
+	"Voice": "",
 	"Warning": "Advertiment",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Avís: Si actualitzeu o canvieu el model d'incrustació, haureu de tornar a importar tots els documents.",
 	"Web": "Web",
@@ -569,7 +596,6 @@
 	"Web Search": "Cercador web",
 	"Web Search Engine": "Cercador web",
 	"Webhook URL": "URL del webhook",
-	"WebUI Add-ons": "Complements de WebUI",
 	"WebUI Settings": "Configuració de WebUI",
 	"WebUI will make requests to": "WebUI farà peticions a",
 	"What’s New in": "Què hi ha de Nou en",

+ 28 - 2
src/lib/i18n/locales/ceb-PH/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "Sa pagtugot",
 	"Allow Chat Deletion": "Tugoti nga mapapas ang mga chat",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "alphanumeric nga mga karakter ug hyphen",
 	"Already have an account?": "Naa na kay account ?",
 	"an assistant": "usa ka katabang",
@@ -81,6 +82,7 @@
 	"Capabilities": "",
 	"Change Password": "Usba ang password",
 	"Chat": "Panaghisgot",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "",
 	"Chat direction": "",
 	"Chat History": "Kasaysayan sa chat",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "I-klik dinhi alang sa tabang.",
 	"Click here to": "",
+	"Click here to download user import template file.": "",
 	"Click here to select": "I-klik dinhi aron makapili",
 	"Click here to select a csv file.": "",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "",
 	"Command": "Pag-order",
 	"Concurrent Requests": "",
+	"Confirm": "",
 	"Confirm Password": "Kumpirma ang password",
+	"Confirm your action": "",
 	"Connections": "Mga koneksyon",
 	"Contact Admin for WebUI Access": "",
 	"Content": "Kontento",
@@ -130,6 +135,8 @@
 	"Create new secret key": "",
 	"Created at": "Gihimo ang",
 	"Created At": "",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "Kasamtangang modelo",
 	"Current Password": "Kasamtangang Password",
 	"Custom": "Custom",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "",
 	"Delete chat": "Pagtangtang sa panaghisgot",
 	"Delete Chat": "",
+	"Delete chat?": "",
 	"delete this link": "",
 	"Delete User": "",
 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} gipapas",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "I-export ang mga chat",
 	"Export Documents Mapping": "I-export ang pagmapa sa dokumento",
+	"Export Functions": "",
 	"Export Models": "",
 	"Export Prompts": "Export prompts",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "",
 	"Feel free to add specific details": "",
+	"File": "",
 	"File Mode": "File mode",
 	"File not found.": "Wala makit-an ang file.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
 	"Fluidly stream large external response chunks": "Hapsay nga paghatud sa daghang mga tipik sa eksternal nga mga tubag",
 	"Focus chat input": "Pag-focus sa entry sa diskusyon",
 	"Followed instructions perfectly": "",
+	"Form": "",
 	"Format your variables using square brackets like this:": "I-format ang imong mga variable gamit ang square brackets sama niini:",
 	"Frequency Penalty": "",
+	"Functions": "",
 	"General": "Heneral",
 	"General Settings": "kinatibuk-ang mga setting",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "Maayong buntag, {{name}}",
 	"Help": "",
 	"Hide": "Tagoa",
+	"Hide Model": "",
 	"How can I help you today?": "Unsaon nako pagtabang kanimo karon?",
 	"Hybrid Search": "",
 	"Image Generation (Experimental)": "Pagmugna og hulagway (Eksperimento)",
@@ -262,6 +276,7 @@
 	"Images": "Mga hulagway",
 	"Import Chats": "Import nga mga chat",
 	"Import Documents Mapping": "Import nga pagmapa sa dokumento",
+	"Import Functions": "",
 	"Import Models": "",
 	"Import Prompts": "Import prompt",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "Ang alphanumeric nga mga karakter ug hyphen lang ang gitugotan sa command string.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Oops! ",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! ",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! ",
 	"Open": "Bukas",
 	"Open AI": "Buksan ang AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "",
 	"Reranking model disabled": "",
 	"Reranking model set to \"{{reranking_model}}\"": "",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "I-reset ang pagtipig sa vector",
 	"Response AutoCopy to Clipboard": "Awtomatikong kopya sa tubag sa clipboard",
@@ -425,6 +442,7 @@
 	"Search a model": "",
 	"Search Chats": "",
 	"Search Documents": "Pangitaa ang mga dokumento",
+	"Search Functions": "",
 	"Search Models": "",
 	"Search Prompts": "Pangitaa ang mga prompt",
 	"Search Query Generation Prompt": "",
@@ -474,6 +492,7 @@
 	"short-summary": "mubo nga summary",
 	"Show": "Pagpakita",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "Ipakita ang mga shortcut",
 	"Showcased creativity": "",
 	"sidebar": "lateral bar",
@@ -495,6 +514,7 @@
 	"System": "Sistema",
 	"System Prompt": "Madasig nga Sistema",
 	"Tags": "Mga tag",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "",
 	"Temperature": "Temperatura",
@@ -506,9 +526,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "",
 	"Theme": "Tema",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Kini nagsiguro nga ang imong bililhon nga mga panag-istoryahanay luwas nga natipig sa imong backend database. ",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "Kini nga setting wala mag-sync tali sa mga browser o device.",
+	"This will delete": "",
 	"Thorough explanation": "",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Sugyot: Pag-update sa daghang variable nga lokasyon nga sunud-sunod pinaagi sa pagpindot sa tab key sa chat entry pagkahuman sa matag puli.",
 	"Title": "Titulo",
@@ -522,6 +544,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "sa entrada sa iring.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "",
 	"Toggle settings": "I-toggle ang mga setting",
@@ -537,10 +560,13 @@
 	"Type": "",
 	"Type Hugging Face Resolve (Download) URL": "Pagsulod sa resolusyon (pag-download) URL Hugging Face",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh!  {{provider}}.",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Wala mailhi nga tipo sa file '{{file_type}}', apan gidawat ug gitratar ingon yano nga teksto",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "",
 	"Update password": "I-update ang password",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "Pag-upload ug modelo sa GGUF",
 	"Upload Files": "",
 	"Upload Pipeline": "",
@@ -559,6 +585,7 @@
 	"variable": "variable",
 	"variable to have them replaced with clipboard content.": "variable aron pulihan kini sa mga sulud sa clipboard.",
 	"Version": "Bersyon",
+	"Voice": "",
 	"Warning": "",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "",
 	"Web": "Web",
@@ -568,7 +595,6 @@
 	"Web Search": "",
 	"Web Search Engine": "",
 	"Webhook URL": "",
-	"WebUI Add-ons": "Mga add-on sa WebUI",
 	"WebUI Settings": "Mga Setting sa WebUI",
 	"WebUI will make requests to": "Ang WebUI maghimo mga hangyo sa",
 	"What’s New in": "Unsay bag-o sa",

+ 28 - 2
src/lib/i18n/locales/de-DE/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "Erlauben",
 	"Allow Chat Deletion": "Chat Löschung erlauben",
 	"Allow non-local voices": "Nicht-lokale Stimmen erlauben",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "alphanumerische Zeichen und Bindestriche",
 	"Already have an account?": "Hast du vielleicht schon ein Account?",
 	"an assistant": "ein Assistent",
@@ -81,6 +82,7 @@
 	"Capabilities": "Fähigkeiten",
 	"Change Password": "Passwort ändern",
 	"Chat": "Chat",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "Chat Bubble UI",
 	"Chat direction": "Chat Richtung",
 	"Chat History": "Chat Verlauf",
@@ -97,6 +99,7 @@
 	"Clear memory": "Memory löschen",
 	"Click here for help.": "Klicke hier für Hilfe.",
 	"Click here to": "Klicke hier, um",
+	"Click here to download user import template file.": "",
 	"Click here to select": "Klicke hier um auszuwählen",
 	"Click here to select a csv file.": "Klicke hier um eine CSV-Datei auszuwählen.",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "ComfyUI Base URL wird benötigt.",
 	"Command": "Befehl",
 	"Concurrent Requests": "Gleichzeitige Anforderungen",
+	"Confirm": "",
 	"Confirm Password": "Passwort bestätigen",
+	"Confirm your action": "",
 	"Connections": "Verbindungen",
 	"Contact Admin for WebUI Access": "",
 	"Content": "Info",
@@ -130,6 +135,8 @@
 	"Create new secret key": "Neuen API Schlüssel erstellen",
 	"Created at": "Erstellt am",
 	"Created At": "Erstellt am",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "Aktuelles Modell",
 	"Current Password": "Aktuelles Passwort",
 	"Custom": "Benutzerdefiniert",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "Alle Chats löschen",
 	"Delete chat": "Chat löschen",
 	"Delete Chat": "Chat löschen",
+	"Delete chat?": "",
 	"delete this link": "diesen Link zu löschen",
 	"Delete User": "Benutzer löschen",
 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} gelöscht",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "Chat exportieren (.json)",
 	"Export Chats": "Chats exportieren",
 	"Export Documents Mapping": "Dokumentenmapping exportieren",
+	"Export Functions": "",
 	"Export Models": "Modelle exportieren",
 	"Export Prompts": "Prompts exportieren",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "Fehler beim Aktualisieren der Einstellungen",
 	"February": "Februar",
 	"Feel free to add specific details": "Ergänze Details.",
+	"File": "",
 	"File Mode": "File Modus",
 	"File not found.": "Datei nicht gefunden.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Fingerprint spoofing erkannt: Initialen können nicht als Avatar verwendet werden. Es wird auf das Standardprofilbild zurückgegriffen.",
 	"Fluidly stream large external response chunks": "Große externe Antwortblöcke flüssig streamen",
 	"Focus chat input": "Chat-Eingabe fokussieren",
 	"Followed instructions perfectly": "Anweisungen perfekt befolgt",
+	"Form": "",
 	"Format your variables using square brackets like this:": "Formatiere deine Variablen mit eckigen Klammern wie folgt:",
 	"Frequency Penalty": "Frequenz-Strafe",
+	"Functions": "",
 	"General": "Allgemein",
 	"General Settings": "Allgemeine Einstellungen",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "Hallo, {{name}}",
 	"Help": "Hilfe",
 	"Hide": "Verbergen",
+	"Hide Model": "",
 	"How can I help you today?": "Wie kann ich dir heute helfen?",
 	"Hybrid Search": "Hybride Suche",
 	"Image Generation (Experimental)": "Bildgenerierung (experimentell)",
@@ -262,6 +276,7 @@
 	"Images": "Bilder",
 	"Import Chats": "Chats importieren",
 	"Import Documents Mapping": "Dokumentenmapping importieren",
+	"Import Functions": "",
 	"Import Models": "Modelle importieren",
 	"Import Prompts": "Prompts importieren",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "Nur alphanumerische Zeichen und Bindestriche sind im Befehlsstring erlaubt.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Hoppla! Warte noch einen Moment! Die Dateien sind noch im der Verarbeitung. Bitte habe etwas Geduld und wir informieren Dich, sobald sie bereit sind.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Hoppla! Es sieht so aus, als wäre die URL ungültig. Bitte überprüfe sie und versuche es nochmal.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Hoppla! du verwendest eine nicht unterstützte Methode (nur Frontend). Bitte stelle die WebUI vom Backend aus bereit.",
 	"Open": "Öffne",
 	"Open AI": "Open AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "Reranking Modell",
 	"Reranking model disabled": "Rranking Modell deaktiviert",
 	"Reranking model set to \"{{reranking_model}}\"": "Reranking Modell auf \"{{reranking_model}}\" gesetzt",
+	"Reset": "",
 	"Reset Upload Directory": "Uploadverzeichnis löschen",
 	"Reset Vector Storage": "Vektorspeicher zurücksetzen",
 	"Response AutoCopy to Clipboard": "Antwort automatisch in die Zwischenablage kopieren",
@@ -425,6 +442,7 @@
 	"Search a model": "Nach einem Modell suchen",
 	"Search Chats": "Chats durchsuchen",
 	"Search Documents": "Dokumente suchen",
+	"Search Functions": "",
 	"Search Models": "Modelle suchen",
 	"Search Prompts": "Prompts suchen",
 	"Search Query Generation Prompt": "",
@@ -474,6 +492,7 @@
 	"short-summary": "kurze-zusammenfassung",
 	"Show": "Anzeigen",
 	"Show Admin Details in Account Pending Overlay": "Admin-Details im Account-Pending-Overlay anzeigen",
+	"Show Model": "",
 	"Show shortcuts": "Verknüpfungen anzeigen",
 	"Showcased creativity": "Kreativität zur Schau gestellt",
 	"sidebar": "Seitenleiste",
@@ -495,6 +514,7 @@
 	"System": "System",
 	"System Prompt": "System-Prompt",
 	"Tags": "Tags",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "Erzähl uns mehr",
 	"Temperature": "Temperatur",
@@ -506,9 +526,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "Der Score sollte ein Wert zwischen 0,0 (0 %) und 1,0 (100 %) sein.",
 	"Theme": "Design",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Dadurch werden deine wertvollen Unterhaltungen sicher in der Backend-Datenbank gespeichert. Vielen Dank!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "Diese Einstellung wird nicht zwischen Browsern oder Geräten synchronisiert.",
+	"This will delete": "",
 	"Thorough explanation": "Genaue Erklärung",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Tipp: Aktualisiere mehrere Variablen nacheinander, indem du nach jeder Aktualisierung die Tabulatortaste im Chat-Eingabefeld drückst.",
 	"Title": "Titel",
@@ -522,6 +544,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "to chat input.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "Heute",
 	"Toggle settings": "Einstellungen umschalten",
@@ -537,10 +560,13 @@
 	"Type": "Art",
 	"Type Hugging Face Resolve (Download) URL": "Gib die Hugging Face Resolve (Download) URL ein",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "Ups! Es gab ein Problem bei der Verbindung mit {{provider}}.",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Unbekannter Dateityp '{{file_type}}', wird jedoch akzeptiert und als einfacher Text behandelt.",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "Erneuern und kopieren",
 	"Update password": "Passwort aktualisieren",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "GGUF Model hochladen",
 	"Upload Files": "Dateien hochladen",
 	"Upload Pipeline": "",
@@ -559,6 +585,7 @@
 	"variable": "Variable",
 	"variable to have them replaced with clipboard content.": "Variable, um den Inhalt der Zwischenablage beim Nutzen des Prompts zu ersetzen.",
 	"Version": "Version",
+	"Voice": "",
 	"Warning": "Warnung",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Warnung: Wenn du dein Einbettungsmodell aktualisierst oder änderst, musst du alle Dokumente erneut importieren.",
 	"Web": "Web",
@@ -568,7 +595,6 @@
 	"Web Search": "Websuche",
 	"Web Search Engine": "Web-Suchmaschine",
 	"Webhook URL": "Webhook URL",
-	"WebUI Add-ons": "WebUI-Add-Ons",
 	"WebUI Settings": "WebUI-Einstellungen",
 	"WebUI will make requests to": "Wenn aktiviert sendet WebUI externe Anfragen an",
 	"What’s New in": "Was gibt's Neues in",

+ 30 - 2
src/lib/i18n/locales/dg-DG/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "Allow",
 	"Allow Chat Deletion": "Allow Delete Chats",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "so alpha, many hyphen",
 	"Already have an account?": "Such account exists?",
 	"an assistant": "such assistant",
@@ -81,6 +82,7 @@
 	"Capabilities": "",
 	"Change Password": "Change Password",
 	"Chat": "Chat",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "",
 	"Chat direction": "",
 	"Chat History": "Chat History",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "Click for help. Much assist.",
 	"Click here to": "",
+	"Click here to download user import template file.": "",
 	"Click here to select": "Click to select",
 	"Click here to select a csv file.": "",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "",
 	"Command": "Command",
 	"Concurrent Requests": "",
+	"Confirm": "",
 	"Confirm Password": "Confirm Password",
+	"Confirm your action": "",
 	"Connections": "Connections",
 	"Contact Admin for WebUI Access": "",
 	"Content": "Content",
@@ -130,6 +135,8 @@
 	"Create new secret key": "",
 	"Created at": "Created at",
 	"Created At": "",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "Current Model",
 	"Current Password": "Current Password",
 	"Custom": "Custom",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "",
 	"Delete chat": "Delete chat",
 	"Delete Chat": "",
+	"Delete chat?": "",
 	"delete this link": "",
 	"Delete User": "",
 	"Deleted {{deleteModelTag}}": "Deleted {{deleteModelTag}}",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "Export Barks",
 	"Export Documents Mapping": "Export Mappings of Dogos",
+	"Export Functions": "",
 	"Export Models": "",
 	"Export Prompts": "Export Promptos",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "",
 	"Feel free to add specific details": "",
+	"File": "",
 	"File Mode": "Bark Mode",
 	"File not found.": "Bark not found.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Fingerprint dogeing: Unable to use initials as avatar. Defaulting to default doge image.",
 	"Fluidly stream large external response chunks": "Fluidly wow big chunks",
 	"Focus chat input": "Focus chat bork",
 	"Followed instructions perfectly": "",
+	"Form": "",
 	"Format your variables using square brackets like this:": "Format variables using square brackets like wow:",
 	"Frequency Penalty": "",
+	"Functions": "",
 	"General": "Woweral",
 	"General Settings": "General Doge Settings",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "Much helo, {{name}}",
 	"Help": "",
 	"Hide": "Hide",
+	"Hide Model": "",
 	"How can I help you today?": "How can I halp u today?",
 	"Hybrid Search": "",
 	"Image Generation (Experimental)": "Image Wow (Much Experiment)",
@@ -262,6 +276,7 @@
 	"Images": "Wowmages",
 	"Import Chats": "Import Barks",
 	"Import Documents Mapping": "Import Doge Mapping",
+	"Import Functions": "",
 	"Import Models": "",
 	"Import Prompts": "Import Promptos",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "Only wow characters and hyphens are allowed in the bork string.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! Looks like the URL is invalid. Please double-check and try again.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.",
 	"Open": "Open",
 	"Open AI": "Open AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "",
 	"Reranking model disabled": "",
 	"Reranking model set to \"{{reranking_model}}\"": "",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "Reset Vector Storage",
 	"Response AutoCopy to Clipboard": "Copy Bark Auto Bark",
@@ -425,6 +442,7 @@
 	"Search a model": "",
 	"Search Chats": "",
 	"Search Documents": "Search Documents much find",
+	"Search Functions": "",
 	"Search Models": "",
 	"Search Prompts": "Search Prompts much wow",
 	"Search Query Generation Prompt": "",
@@ -432,6 +450,8 @@
 	"Search Result Count": "",
 	"Search Tools": "",
 	"Searched {{count}} sites_one": "",
+	"Searched {{count}} sites_few": "",
+	"Searched {{count}} sites_many": "",
 	"Searched {{count}} sites_other": "",
 	"Searching \"{{searchQuery}}\"": "",
 	"Searxng Query URL": "",
@@ -474,6 +494,7 @@
 	"short-summary": "short-summary so short",
 	"Show": "Show much show",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "Show shortcuts much shortcut",
 	"Showcased creativity": "",
 	"sidebar": "sidebar much side",
@@ -495,6 +516,7 @@
 	"System": "System very system",
 	"System Prompt": "System Prompt much prompt",
 	"Tags": "Tags very tags",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "",
 	"Temperature": "Temperature very temp",
@@ -506,9 +528,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "",
 	"Theme": "Theme much theme",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "This ensures that your valuable conversations are securely saved to your backend database. Thank you! Much secure!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "This setting does not sync across browsers or devices. Very not sync.",
+	"This will delete": "",
 	"Thorough explanation": "",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement. Much tip!",
 	"Title": "Title very title",
@@ -522,6 +546,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "to chat input. Very chat.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "",
 	"Toggle settings": "Toggle settings much toggle",
@@ -537,10 +562,13 @@
 	"Type": "",
 	"Type Hugging Face Resolve (Download) URL": "Type Hugging Face Resolve (Download) URL much download",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! There was an issue connecting to {{provider}}. Much uh-oh!",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Unknown File Type '{{file_type}}', but accepting and treating as plain text very unknown",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "",
 	"Update password": "Update password much change",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "Upload a GGUF model very upload",
 	"Upload Files": "",
 	"Upload Pipeline": "",
@@ -559,6 +587,7 @@
 	"variable": "variable very variable",
 	"variable to have them replaced with clipboard content.": "variable to have them replaced with clipboard content. Very replace.",
 	"Version": "Version much version",
+	"Voice": "",
 	"Warning": "",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "",
 	"Web": "Web very web",
@@ -568,7 +597,6 @@
 	"Web Search": "",
 	"Web Search Engine": "",
 	"Webhook URL": "",
-	"WebUI Add-ons": "WebUI Add-ons very add-ons",
 	"WebUI Settings": "WebUI Settings much settings",
 	"WebUI will make requests to": "WebUI will make requests to much request",
 	"What’s New in": "What’s New in much new",

+ 28 - 2
src/lib/i18n/locales/en-GB/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "",
 	"Allow Chat Deletion": "",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "",
 	"Already have an account?": "",
 	"an assistant": "",
@@ -81,6 +82,7 @@
 	"Capabilities": "",
 	"Change Password": "",
 	"Chat": "",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "",
 	"Chat direction": "",
 	"Chat History": "",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "",
 	"Click here to": "",
+	"Click here to download user import template file.": "",
 	"Click here to select": "",
 	"Click here to select a csv file.": "",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "",
 	"Command": "",
 	"Concurrent Requests": "",
+	"Confirm": "",
 	"Confirm Password": "",
+	"Confirm your action": "",
 	"Connections": "",
 	"Contact Admin for WebUI Access": "",
 	"Content": "",
@@ -130,6 +135,8 @@
 	"Create new secret key": "",
 	"Created at": "",
 	"Created At": "",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "",
 	"Current Password": "",
 	"Custom": "",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "",
 	"Delete chat": "",
 	"Delete Chat": "",
+	"Delete chat?": "",
 	"delete this link": "",
 	"Delete User": "",
 	"Deleted {{deleteModelTag}}": "",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "",
 	"Export Documents Mapping": "",
+	"Export Functions": "",
 	"Export Models": "",
 	"Export Prompts": "",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "",
 	"Feel free to add specific details": "",
+	"File": "",
 	"File Mode": "",
 	"File not found.": "",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
 	"Fluidly stream large external response chunks": "",
 	"Focus chat input": "",
 	"Followed instructions perfectly": "",
+	"Form": "",
 	"Format your variables using square brackets like this:": "",
 	"Frequency Penalty": "",
+	"Functions": "",
 	"General": "",
 	"General Settings": "",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "",
 	"Help": "",
 	"Hide": "",
+	"Hide Model": "",
 	"How can I help you today?": "",
 	"Hybrid Search": "",
 	"Image Generation (Experimental)": "",
@@ -262,6 +276,7 @@
 	"Images": "",
 	"Import Chats": "",
 	"Import Documents Mapping": "",
+	"Import Functions": "",
 	"Import Models": "",
 	"Import Prompts": "",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "",
 	"Open": "",
 	"Open AI": "",
@@ -406,6 +422,7 @@
 	"Reranking Model": "",
 	"Reranking model disabled": "",
 	"Reranking model set to \"{{reranking_model}}\"": "",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "",
 	"Response AutoCopy to Clipboard": "",
@@ -425,6 +442,7 @@
 	"Search a model": "",
 	"Search Chats": "",
 	"Search Documents": "",
+	"Search Functions": "",
 	"Search Models": "",
 	"Search Prompts": "",
 	"Search Query Generation Prompt": "",
@@ -474,6 +492,7 @@
 	"short-summary": "",
 	"Show": "",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "",
 	"Showcased creativity": "",
 	"sidebar": "",
@@ -495,6 +514,7 @@
 	"System": "",
 	"System Prompt": "",
 	"Tags": "",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "",
 	"Temperature": "",
@@ -506,9 +526,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "",
 	"Theme": "",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "",
+	"This will delete": "",
 	"Thorough explanation": "",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "",
 	"Title": "",
@@ -522,6 +544,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "",
 	"Toggle settings": "",
@@ -537,10 +560,13 @@
 	"Type": "",
 	"Type Hugging Face Resolve (Download) URL": "",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "",
 	"Update password": "",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "",
 	"Upload Files": "",
 	"Upload Pipeline": "",
@@ -559,6 +585,7 @@
 	"variable": "",
 	"variable to have them replaced with clipboard content.": "",
 	"Version": "",
+	"Voice": "",
 	"Warning": "",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "",
 	"Web": "",
@@ -568,7 +595,6 @@
 	"Web Search": "",
 	"Web Search Engine": "",
 	"Webhook URL": "",
-	"WebUI Add-ons": "",
 	"WebUI Settings": "",
 	"WebUI will make requests to": "",
 	"What’s New in": "",

+ 28 - 2
src/lib/i18n/locales/en-US/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "",
 	"Allow Chat Deletion": "",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "",
 	"Already have an account?": "",
 	"an assistant": "",
@@ -81,6 +82,7 @@
 	"Capabilities": "",
 	"Change Password": "",
 	"Chat": "",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "",
 	"Chat direction": "",
 	"Chat History": "",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "",
 	"Click here to": "",
+	"Click here to download user import template file.": "",
 	"Click here to select": "",
 	"Click here to select a csv file.": "",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "",
 	"Command": "",
 	"Concurrent Requests": "",
+	"Confirm": "",
 	"Confirm Password": "",
+	"Confirm your action": "",
 	"Connections": "",
 	"Contact Admin for WebUI Access": "",
 	"Content": "",
@@ -130,6 +135,8 @@
 	"Create new secret key": "",
 	"Created at": "",
 	"Created At": "",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "",
 	"Current Password": "",
 	"Custom": "",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "",
 	"Delete chat": "",
 	"Delete Chat": "",
+	"Delete chat?": "",
 	"delete this link": "",
 	"Delete User": "",
 	"Deleted {{deleteModelTag}}": "",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "",
 	"Export Documents Mapping": "",
+	"Export Functions": "",
 	"Export Models": "",
 	"Export Prompts": "",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "",
 	"Feel free to add specific details": "",
+	"File": "",
 	"File Mode": "",
 	"File not found.": "",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
 	"Fluidly stream large external response chunks": "",
 	"Focus chat input": "",
 	"Followed instructions perfectly": "",
+	"Form": "",
 	"Format your variables using square brackets like this:": "",
 	"Frequency Penalty": "",
+	"Functions": "",
 	"General": "",
 	"General Settings": "",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "",
 	"Help": "",
 	"Hide": "",
+	"Hide Model": "",
 	"How can I help you today?": "",
 	"Hybrid Search": "",
 	"Image Generation (Experimental)": "",
@@ -262,6 +276,7 @@
 	"Images": "",
 	"Import Chats": "",
 	"Import Documents Mapping": "",
+	"Import Functions": "",
 	"Import Models": "",
 	"Import Prompts": "",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "",
 	"Open": "",
 	"Open AI": "",
@@ -406,6 +422,7 @@
 	"Reranking Model": "",
 	"Reranking model disabled": "",
 	"Reranking model set to \"{{reranking_model}}\"": "",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "",
 	"Response AutoCopy to Clipboard": "",
@@ -425,6 +442,7 @@
 	"Search a model": "",
 	"Search Chats": "",
 	"Search Documents": "",
+	"Search Functions": "",
 	"Search Models": "",
 	"Search Prompts": "",
 	"Search Query Generation Prompt": "",
@@ -474,6 +492,7 @@
 	"short-summary": "",
 	"Show": "",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "",
 	"Showcased creativity": "",
 	"sidebar": "",
@@ -495,6 +514,7 @@
 	"System": "",
 	"System Prompt": "",
 	"Tags": "",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "",
 	"Temperature": "",
@@ -506,9 +526,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "",
 	"Theme": "",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "",
+	"This will delete": "",
 	"Thorough explanation": "",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "",
 	"Title": "",
@@ -522,6 +544,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "",
 	"Toggle settings": "",
@@ -537,10 +560,13 @@
 	"Type": "",
 	"Type Hugging Face Resolve (Download) URL": "",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "",
 	"Update password": "",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "",
 	"Upload Files": "",
 	"Upload Pipeline": "",
@@ -559,6 +585,7 @@
 	"variable": "",
 	"variable to have them replaced with clipboard content.": "",
 	"Version": "",
+	"Voice": "",
 	"Warning": "",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "",
 	"Web": "",
@@ -568,7 +595,6 @@
 	"Web Search": "",
 	"Web Search Engine": "",
 	"Webhook URL": "",
-	"WebUI Add-ons": "",
 	"WebUI Settings": "",
 	"WebUI will make requests to": "",
 	"What’s New in": "",

+ 28 - 2
src/lib/i18n/locales/es-ES/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "Permitir",
 	"Allow Chat Deletion": "Permitir Borrar Chats",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "caracteres alfanuméricos y guiones",
 	"Already have an account?": "¿Ya tienes una cuenta?",
 	"an assistant": "un asistente",
@@ -81,6 +82,7 @@
 	"Capabilities": "Capacidades",
 	"Change Password": "Cambia la Contraseña",
 	"Chat": "Chat",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "Burbuja de chat UI",
 	"Chat direction": "Dirección del Chat",
 	"Chat History": "Historial del Chat",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "Presiona aquí para obtener ayuda.",
 	"Click here to": "Presiona aquí para",
+	"Click here to download user import template file.": "",
 	"Click here to select": "Presiona aquí para seleccionar",
 	"Click here to select a csv file.": "Presiona aquí para seleccionar un archivo csv.",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "ComfyUI Base URL es requerido.",
 	"Command": "Comando",
 	"Concurrent Requests": "Solicitudes simultáneas",
+	"Confirm": "",
 	"Confirm Password": "Confirmar Contraseña",
+	"Confirm your action": "",
 	"Connections": "Conexiones",
 	"Contact Admin for WebUI Access": "",
 	"Content": "Contenido",
@@ -130,6 +135,8 @@
 	"Create new secret key": "Crear una nueva clave secreta",
 	"Created at": "Creado en",
 	"Created At": "Creado en",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "Modelo Actual",
 	"Current Password": "Contraseña Actual",
 	"Custom": "Personalizado",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "Eliminar todos los chats",
 	"Delete chat": "Borrar chat",
 	"Delete Chat": "Borrar Chat",
+	"Delete chat?": "",
 	"delete this link": "Borrar este enlace",
 	"Delete User": "Borrar Usuario",
 	"Deleted {{deleteModelTag}}": "Se borró {{deleteModelTag}}",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "Exportar Chats",
 	"Export Documents Mapping": "Exportar el mapeo de documentos",
+	"Export Functions": "",
 	"Export Models": "Modelos de exportación",
 	"Export Prompts": "Exportar Prompts",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "Febrero",
 	"Feel free to add specific details": "Libre de agregar detalles específicos",
+	"File": "",
 	"File Mode": "Modo de archivo",
 	"File not found.": "Archivo no encontrado.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Se detectó suplantación de huellas: No se pueden usar las iniciales como avatar. Por defecto se utiliza la imagen de perfil predeterminada.",
 	"Fluidly stream large external response chunks": "Transmita con fluidez grandes fragmentos de respuesta externa",
 	"Focus chat input": "Enfoca la entrada del chat",
 	"Followed instructions perfectly": "Siguió las instrucciones perfectamente",
+	"Form": "",
 	"Format your variables using square brackets like this:": "Formatea tus variables usando corchetes de la siguiente manera:",
 	"Frequency Penalty": "Penalización de frecuencia",
+	"Functions": "",
 	"General": "General",
 	"General Settings": "Opciones Generales",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "Hola, {{name}}",
 	"Help": "Ayuda",
 	"Hide": "Esconder",
+	"Hide Model": "",
 	"How can I help you today?": "¿Cómo puedo ayudarte hoy?",
 	"Hybrid Search": "Búsqueda Híbrida",
 	"Image Generation (Experimental)": "Generación de imágenes (experimental)",
@@ -262,6 +276,7 @@
 	"Images": "Imágenes",
 	"Import Chats": "Importar chats",
 	"Import Documents Mapping": "Importar Mapeo de Documentos",
+	"Import Functions": "",
 	"Import Models": "Importar modelos",
 	"Import Prompts": "Importar Prompts",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "Sólo se permiten caracteres alfanuméricos y guiones en la cadena de comando.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "¡Ups! ¡Agárrate fuerte! Tus archivos todavía están en el horno de procesamiento. Los estamos cocinando a la perfección. Tenga paciencia y le avisaremos una vez que estén listos.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "¡Ups! Parece que la URL no es válida. Vuelva a verificar e inténtelo nuevamente.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "¡Ups! Estás utilizando un método no compatible (solo frontend). Por favor ejecute la WebUI desde el backend.",
 	"Open": "Abrir",
 	"Open AI": "Abrir AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "Modelo de reranking",
 	"Reranking model disabled": "Modelo de reranking deshabilitado",
 	"Reranking model set to \"{{reranking_model}}\"": "Modelo de reranking establecido en \"{{reranking_model}}\"",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "Restablecer almacenamiento vectorial",
 	"Response AutoCopy to Clipboard": "Copiar respuesta automáticamente al portapapeles",
@@ -425,6 +442,7 @@
 	"Search a model": "Buscar un modelo",
 	"Search Chats": "Chats de búsqueda",
 	"Search Documents": "Buscar Documentos",
+	"Search Functions": "",
 	"Search Models": "Modelos de búsqueda",
 	"Search Prompts": "Buscar Prompts",
 	"Search Query Generation Prompt": "",
@@ -475,6 +493,7 @@
 	"short-summary": "resumen-corto",
 	"Show": "Mostrar",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "Mostrar atajos",
 	"Showcased creativity": "Mostrar creatividad",
 	"sidebar": "barra lateral",
@@ -496,6 +515,7 @@
 	"System": "Sistema",
 	"System Prompt": "Prompt del sistema",
 	"Tags": "Etiquetas",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "Dinos más:",
 	"Temperature": "Temperatura",
@@ -507,9 +527,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "El puntaje debe ser un valor entre 0.0 (0%) y 1.0 (100%).",
 	"Theme": "Tema",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Esto garantiza que sus valiosas conversaciones se guarden de forma segura en su base de datos en el backend. ¡Gracias!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "Esta configuración no se sincroniza entre navegadores o dispositivos.",
+	"This will delete": "",
 	"Thorough explanation": "Explicación exhaustiva",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Consejo: Actualice múltiples variables consecutivamente presionando la tecla tab en la entrada del chat después de cada reemplazo.",
 	"Title": "Título",
@@ -523,6 +545,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "a la entrada del chat.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "Hoy",
 	"Toggle settings": "Alternar configuración",
@@ -538,10 +561,13 @@
 	"Type": "Tipo",
 	"Type Hugging Face Resolve (Download) URL": "Escriba la URL (Descarga) de Hugging Face Resolve",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "¡Uh oh! Hubo un problema al conectarse a {{provider}}.",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tipo de archivo desconocido '{{file_type}}', pero se acepta y se trata como texto sin formato",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "Actualizar y copiar enlace",
 	"Update password": "Actualizar contraseña",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "Subir un modelo GGUF",
 	"Upload Files": "Subir archivos",
 	"Upload Pipeline": "",
@@ -560,6 +586,7 @@
 	"variable": "variable",
 	"variable to have them replaced with clipboard content.": "variable para reemplazarlos con el contenido del portapapeles.",
 	"Version": "Versión",
+	"Voice": "",
 	"Warning": "Advertencia",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Advertencia: Si actualiza o cambia su modelo de inserción, necesitará volver a importar todos los documentos.",
 	"Web": "Web",
@@ -569,7 +596,6 @@
 	"Web Search": "Búsqueda en la Web",
 	"Web Search Engine": "Motor de búsqueda web",
 	"Webhook URL": "Webhook URL",
-	"WebUI Add-ons": "WebUI Add-ons",
 	"WebUI Settings": "Configuración del WebUI",
 	"WebUI will make requests to": "WebUI realizará solicitudes a",
 	"What’s New in": "Novedades en",

+ 28 - 2
src/lib/i18n/locales/fa-IR/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "اجازه دادن",
 	"Allow Chat Deletion": "اجازه حذف گپ",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "حروف الفبایی و خط فاصله",
 	"Already have an account?": "از قبل حساب کاربری دارید؟",
 	"an assistant": "یک دستیار",
@@ -81,6 +82,7 @@
 	"Capabilities": "قابلیت",
 	"Change Password": "تغییر رمز عبور",
 	"Chat": "گپ",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "UI\u200cی\u200c گفتگو\u200c",
 	"Chat direction": "جهت\u200cگفتگو",
 	"Chat History": "تاریخچه\u200cی گفتگو",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "برای کمک اینجا را کلیک کنید.",
 	"Click here to": "برای کمک اینجا را کلیک کنید.",
+	"Click here to download user import template file.": "",
 	"Click here to select": "برای انتخاب اینجا کلیک کنید",
 	"Click here to select a csv file.": "برای انتخاب یک فایل csv اینجا را کلیک کنید.",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "URL پایه کومیوآی الزامی است.",
 	"Command": "دستور",
 	"Concurrent Requests": "درخواست های همزمان",
+	"Confirm": "",
 	"Confirm Password": "تایید رمز عبور",
+	"Confirm your action": "",
 	"Connections": "ارتباطات",
 	"Contact Admin for WebUI Access": "",
 	"Content": "محتوا",
@@ -130,6 +135,8 @@
 	"Create new secret key": "ساخت کلید gehez جدید",
 	"Created at": "ایجاد شده در",
 	"Created At": "ایجاد شده در",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "مدل فعلی",
 	"Current Password": "رمز عبور فعلی",
 	"Custom": "دلخواه",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "حذف همه گفتگوها",
 	"Delete chat": "حذف گپ",
 	"Delete Chat": "حذف گپ",
+	"Delete chat?": "",
 	"delete this link": "حذف این لینک",
 	"Delete User": "حذف کاربر",
 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} پاک شد",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "اکسپورت از گپ\u200cها",
 	"Export Documents Mapping": "اکسپورت از نگاشت اسناد",
+	"Export Functions": "",
 	"Export Models": "مدل های صادرات",
 	"Export Prompts": "اکسپورت از پرامپت\u200cها",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "فوری",
 	"Feel free to add specific details": "اگر به دلخواه، معلومات خاصی اضافه کنید",
+	"File": "",
 	"File Mode": "حالت فایل",
 	"File not found.": "فایل یافت نشد.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "فانگ سرفیس شناسایی شد: نمی توان از نمایه شما به عنوان آواتار استفاده کرد. پیش فرض به عکس پروفایل پیش فرض برگشت داده شد.",
 	"Fluidly stream large external response chunks": "تکه های پاسخ خارجی بزرگ را به صورت سیال پخش کنید",
 	"Focus chat input": "فوکوس کردن ورودی گپ",
 	"Followed instructions perfectly": "دستورالعمل ها را کاملا دنبال کرد",
+	"Form": "",
 	"Format your variables using square brackets like this:": "متغیرهای خود را با استفاده از براکت مربع به شکل زیر قالب بندی کنید:",
 	"Frequency Penalty": "مجازات فرکانس",
+	"Functions": "",
 	"General": "عمومی",
 	"General Settings": "تنظیمات عمومی",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "سلام، {{name}}",
 	"Help": "کمک",
 	"Hide": "پنهان",
+	"Hide Model": "",
 	"How can I help you today?": "امروز چطور می توانم کمک تان کنم؟",
 	"Hybrid Search": "جستجوی همزمان",
 	"Image Generation (Experimental)": "تولید تصویر (آزمایشی)",
@@ -262,6 +276,7 @@
 	"Images": "تصاویر",
 	"Import Chats": "ایمپورت گپ\u200cها",
 	"Import Documents Mapping": "ایمپورت نگاشت اسناد",
+	"Import Functions": "",
 	"Import Models": "واردات مدلها",
 	"Import Prompts": "ایمپورت پرامپت\u200cها",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "فقط کاراکترهای الفبایی و خط فاصله در رشته فرمان مجاز هستند.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "اوه! فایل های شما هنوز در فر پردازش هستند. ما آنها را کامل می پزیم. لطفا صبور باشید، به محض آماده شدن به شما اطلاع خواهیم داد.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "اوه! به نظر می رسد URL نامعتبر است. لطفاً دوباره بررسی کنید و دوباره امتحان کنید.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "اوه! شما از یک روش پشتیبانی نشده (فقط frontend) استفاده می کنید. لطفاً WebUI را از بکند اجرا کنید.",
 	"Open": "باز",
 	"Open AI": "Open AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "مدل ری\u200cشناسی مجدد غیرفعال است",
 	"Reranking model disabled": "مدل ری\u200cشناسی مجدد غیرفعال است",
 	"Reranking model set to \"{{reranking_model}}\"": "مدل ری\u200cشناسی مجدد به \"{{reranking_model}}\" تنظیم شده است",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "بازنشانی ذخیره سازی برداری",
 	"Response AutoCopy to Clipboard": "کپی خودکار پاسخ به کلیپ بورد",
@@ -425,6 +442,7 @@
 	"Search a model": "جستجوی مدل",
 	"Search Chats": "جستجو گپ ها",
 	"Search Documents": "جستجوی اسناد",
+	"Search Functions": "",
 	"Search Models": "مدل های جستجو",
 	"Search Prompts": "جستجوی پرامپت\u200cها",
 	"Search Query Generation Prompt": "",
@@ -474,6 +492,7 @@
 	"short-summary": "خلاصه کوتاه",
 	"Show": "نمایش",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "نمایش میانبرها",
 	"Showcased creativity": "ایده\u200cآفرینی",
 	"sidebar": "نوار کناری",
@@ -495,6 +514,7 @@
 	"System": "سیستم",
 	"System Prompt": "پرامپت سیستم",
 	"Tags": "تگ\u200cها",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "بیشتر بگویید:",
 	"Temperature": "دما",
@@ -506,9 +526,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "امتیاز باید یک مقدار بین 0.0 (0%) و 1.0 (100%) باشد.",
 	"Theme": "قالب",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "این تضمین می کند که مکالمات ارزشمند شما به طور ایمن در پایگاه داده بکند ذخیره می شود. تشکر!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "این تنظیم در مرورگرها یا دستگاه\u200cها همگام\u200cسازی نمی\u200cشود.",
+	"This will delete": "",
 	"Thorough explanation": "توضیح کامل",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "با فشردن کلید Tab در ورودی چت پس از هر بار تعویض، چندین متغیر را به صورت متوالی به روزرسانی کنید.",
 	"Title": "عنوان",
@@ -522,6 +544,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "در ورودی گپ.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "امروز",
 	"Toggle settings": "نمایش/عدم نمایش تنظیمات",
@@ -537,10 +560,13 @@
 	"Type": "نوع",
 	"Type Hugging Face Resolve (Download) URL": "مقدار URL دانلود (Resolve) Hugging Face را وارد کنید",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "اوه اوه! مشکلی در اتصال به {{provider}} وجود داشت.",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "نوع فایل '{{file_type}}' ناشناخته است، به عنوان یک فایل متنی ساده با آن برخورد می شود.",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "به روزرسانی و کپی لینک",
 	"Update password": "به روزرسانی رمزعبور",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "آپلود یک مدل GGUF",
 	"Upload Files": "بارگذاری پروندهها",
 	"Upload Pipeline": "",
@@ -559,6 +585,7 @@
 	"variable": "متغیر",
 	"variable to have them replaced with clipboard content.": "متغیر برای جایگزینی آنها با محتوای کلیپ بورد.",
 	"Version": "نسخه",
+	"Voice": "",
 	"Warning": "هشدار",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "هشدار: اگر شما به روز کنید یا تغییر دهید مدل شما، باید تمام سند ها را مجددا وارد کنید.",
 	"Web": "وب",
@@ -568,7 +595,6 @@
 	"Web Search": "جستجوی وب",
 	"Web Search Engine": "موتور جستجوی وب",
 	"Webhook URL": "URL وبهوک",
-	"WebUI Add-ons": "WebUI افزونه\u200cهای",
 	"WebUI Settings": "تنظیمات WebUI",
 	"WebUI will make requests to": "WebUI درخواست\u200cها را ارسال خواهد کرد به",
 	"What’s New in": "موارد جدید در",

+ 28 - 2
src/lib/i18n/locales/fi-FI/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "Salli",
 	"Allow Chat Deletion": "Salli keskustelujen poisto",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "kirjaimia, numeroita ja väliviivoja",
 	"Already have an account?": "Onko sinulla jo tili?",
 	"an assistant": "avustaja",
@@ -81,6 +82,7 @@
 	"Capabilities": "Ominaisuuksia",
 	"Change Password": "Vaihda salasana",
 	"Chat": "Keskustelu",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "Keskustelu-pallojen käyttöliittymä",
 	"Chat direction": "Keskustelun suunta",
 	"Chat History": "Keskusteluhistoria",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "Klikkaa tästä saadaksesi apua.",
 	"Click here to": "Klikkaa tästä",
+	"Click here to download user import template file.": "",
 	"Click here to select": "Klikkaa tästä valitaksesi",
 	"Click here to select a csv file.": "Klikkaa tästä valitaksesi CSV-tiedosto.",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "ComfyUI-perus-URL vaaditaan.",
 	"Command": "Komento",
 	"Concurrent Requests": "Samanaikaiset pyynnöt",
+	"Confirm": "",
 	"Confirm Password": "Vahvista salasana",
+	"Confirm your action": "",
 	"Connections": "Yhteydet",
 	"Contact Admin for WebUI Access": "",
 	"Content": "Sisältö",
@@ -130,6 +135,8 @@
 	"Create new secret key": "Luo uusi salainen avain",
 	"Created at": "Luotu",
 	"Created At": "Luotu",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "Nykyinen malli",
 	"Current Password": "Nykyinen salasana",
 	"Custom": "Mukautettu",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "Poista kaikki keskustelut",
 	"Delete chat": "Poista keskustelu",
 	"Delete Chat": "Poista keskustelu",
+	"Delete chat?": "",
 	"delete this link": "poista tämä linkki",
 	"Delete User": "Poista käyttäjä",
 	"Deleted {{deleteModelTag}}": "Poistettu {{deleteModelTag}}",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "Vie keskustelut",
 	"Export Documents Mapping": "Vie asiakirjakartoitus",
+	"Export Functions": "",
 	"Export Models": "Vie malleja",
 	"Export Prompts": "Vie kehotteet",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "helmikuu",
 	"Feel free to add specific details": "Voit lisätä tarkempia tietoja",
+	"File": "",
 	"File Mode": "Tiedostotila",
 	"File not found.": "Tiedostoa ei löytynyt.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Sormenjäljen väärentäminen havaittu: Ei voi käyttää alkukirjaimia avatarina. Käytetään oletusprofiilikuvaa.",
 	"Fluidly stream large external response chunks": "Virtaa suuria ulkoisia vastausosia joustavasti",
 	"Focus chat input": "Fokusoi syöttökenttään",
 	"Followed instructions perfectly": "Noudatti ohjeita täydellisesti",
+	"Form": "",
 	"Format your variables using square brackets like this:": "Muotoile muuttujat hakasulkeilla näin:",
 	"Frequency Penalty": "Taajuussakko",
+	"Functions": "",
 	"General": "Yleinen",
 	"General Settings": "Yleisasetukset",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "Terve, {{name}}",
 	"Help": "Apua",
 	"Hide": "Piilota",
+	"Hide Model": "",
 	"How can I help you today?": "Kuinka voin auttaa tänään?",
 	"Hybrid Search": "Hybridihaku",
 	"Image Generation (Experimental)": "Kuvagenerointi (kokeellinen)",
@@ -262,6 +276,7 @@
 	"Images": "Kuvat",
 	"Import Chats": "Tuo keskustelut",
 	"Import Documents Mapping": "Tuo asiakirjakartoitus",
+	"Import Functions": "",
 	"Import Models": "Mallien tuominen",
 	"Import Prompts": "Tuo kehotteita",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "Vain kirjaimet, numerot ja väliviivat ovat sallittuja komentosarjassa.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Hetki pieni, tiedostosi ovat yhä leivinuunissa. Odota kärsivällisesti, ja ilmoitamme, kun ne ovat valmiita.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Hups! Näyttää siltä, että URL on virheellinen. Tarkista se ja yritä uudelleen.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Hupsista! Käytät ei-tuettua menetelmää. WebUI pitää palvella backendista.",
 	"Open": "Avaa",
 	"Open AI": "Open AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "Uudelleenpisteytysmalli",
 	"Reranking model disabled": "Uudelleenpisteytysmalli poistettu käytöstä",
 	"Reranking model set to \"{{reranking_model}}\"": "\"{{reranking_model}}\" valittu uudelleenpisteytysmalliksi",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "Tyhjennä vektorivarasto",
 	"Response AutoCopy to Clipboard": "Vastauksen automaattikopiointi leikepöydälle",
@@ -425,6 +442,7 @@
 	"Search a model": "Hae mallia",
 	"Search Chats": "Etsi chatteja",
 	"Search Documents": "Hae asiakirjoja",
+	"Search Functions": "",
 	"Search Models": "Hae malleja",
 	"Search Prompts": "Hae kehotteita",
 	"Search Query Generation Prompt": "",
@@ -474,6 +492,7 @@
 	"short-summary": "lyhyt-yhteenveto",
 	"Show": "Näytä",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "Näytä pikanäppäimet",
 	"Showcased creativity": "Näytti luovuutta",
 	"sidebar": "sivupalkki",
@@ -495,6 +514,7 @@
 	"System": "Järjestelmä",
 	"System Prompt": "Järjestelmäkehote",
 	"Tags": "Tagit",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "Kerro lisää:",
 	"Temperature": "Lämpötila",
@@ -506,9 +526,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "Pisteytyksen tulee olla arvo välillä 0.0 (0%) ja 1.0 (100%).",
 	"Theme": "Teema",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Tämä varmistaa, että arvokkaat keskustelusi tallennetaan turvallisesti backend-tietokantaasi. Kiitos!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "Tämä asetus ei synkronoidu selainten tai laitteiden välillä.",
+	"This will delete": "",
 	"Thorough explanation": "Perusteellinen selitys",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Vinkki: Päivitä useita muuttujapaikkoja peräkkäin painamalla tabulaattoria keskustelusyötteessä jokaisen korvauksen jälkeen.",
 	"Title": "Otsikko",
@@ -522,6 +544,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "keskustelusyötteeseen.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "Tänään",
 	"Toggle settings": "Kytke asetukset",
@@ -537,10 +560,13 @@
 	"Type": "Tyyppi",
 	"Type Hugging Face Resolve (Download) URL": "Kirjoita Hugging Face -resolve-osoite",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "Voi ei! Yhteysongelma {{provider}}:n kanssa.",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tuntematon tiedostotyyppi '{{file_type}}', mutta hyväksytään ja käsitellään pelkkänä tekstinä",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "Päivitä ja kopioi linkki",
 	"Update password": "Päivitä salasana",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "Lataa GGUF-malli",
 	"Upload Files": "Lataa tiedostoja",
 	"Upload Pipeline": "",
@@ -559,6 +585,7 @@
 	"variable": "muuttuja",
 	"variable to have them replaced with clipboard content.": "muuttuja korvataan leikepöydän sisällöllä.",
 	"Version": "Versio",
+	"Voice": "",
 	"Warning": "Varoitus",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Varoitus: Jos päivität tai vaihdat upotusmallia, sinun on tuotava kaikki asiakirjat uudelleen.",
 	"Web": "Web",
@@ -568,7 +595,6 @@
 	"Web Search": "Web-haku",
 	"Web Search Engine": "Web-hakukone",
 	"Webhook URL": "Webhook-URL",
-	"WebUI Add-ons": "WebUI-lisäosat",
 	"WebUI Settings": "WebUI-asetukset",
 	"WebUI will make requests to": "WebUI tekee pyyntöjä",
 	"What’s New in": "Mitä uutta",

+ 28 - 2
src/lib/i18n/locales/fr-CA/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "Autoriser",
 	"Allow Chat Deletion": "Autoriser la suppression des discussions",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "caractères alphanumériques et tirets",
 	"Already have an account?": "Vous avez déjà un compte ?",
 	"an assistant": "un assistant",
@@ -81,6 +82,7 @@
 	"Capabilities": "Capacités",
 	"Change Password": "Changer le mot de passe",
 	"Chat": "Discussion",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "Bubble UI de discussion",
 	"Chat direction": "Direction de discussion",
 	"Chat History": "Historique des discussions",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "Cliquez ici pour de l'aide.",
 	"Click here to": "Cliquez ici pour",
+	"Click here to download user import template file.": "",
 	"Click here to select": "Cliquez ici pour sélectionner",
 	"Click here to select a csv file.": "Cliquez ici pour sélectionner un fichier csv.",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "ComfyUI Base URL est requis.",
 	"Command": "Commande",
 	"Concurrent Requests": "Demandes simultanées",
+	"Confirm": "",
 	"Confirm Password": "Confirmer le mot de passe",
+	"Confirm your action": "",
 	"Connections": "Connexions",
 	"Contact Admin for WebUI Access": "",
 	"Content": "Contenu",
@@ -130,6 +135,8 @@
 	"Create new secret key": "Créer une nouvelle clé secrète",
 	"Created at": "Créé le",
 	"Created At": "Créé le",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "Modèle actuel",
 	"Current Password": "Mot de passe actuel",
 	"Custom": "Personnalisé",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "Supprimer tous les chats",
 	"Delete chat": "Supprimer la discussion",
 	"Delete Chat": "Supprimer la discussion",
+	"Delete chat?": "",
 	"delete this link": "supprimer ce lien",
 	"Delete User": "Supprimer l'utilisateur",
 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "Exporter les discussions",
 	"Export Documents Mapping": "Exporter le mappage des documents",
+	"Export Functions": "",
 	"Export Models": "Modèles d’exportation",
 	"Export Prompts": "Exporter les prompts",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "Février",
 	"Feel free to add specific details": "Vous pouvez ajouter des détails spécifiques",
+	"File": "",
 	"File Mode": "Mode fichier",
 	"File not found.": "Fichier introuvable.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Détection de falsification de empreinte digitale\u00a0: impossible d'utiliser les initiales comme avatar. Par défaut, l'image de profil par défaut est utilisée.",
 	"Fluidly stream large external response chunks": "Diffusez de manière fluide de gros morceaux de réponses externes",
 	"Focus chat input": "Se concentrer sur l'entrée de la discussion",
 	"Followed instructions perfectly": "Suivi des instructions parfaitement",
+	"Form": "",
 	"Format your variables using square brackets like this:": "Formatez vos variables en utilisant des crochets comme ceci :",
 	"Frequency Penalty": "Pénalité de fréquence",
+	"Functions": "",
 	"General": "Général",
 	"General Settings": "Paramètres généraux",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "Bonjour, {{name}}",
 	"Help": "Aide",
 	"Hide": "Cacher",
+	"Hide Model": "",
 	"How can I help you today?": "Comment puis-je vous aider aujourd'hui ?",
 	"Hybrid Search": "Recherche hybride",
 	"Image Generation (Experimental)": "Génération d'image (Expérimental)",
@@ -262,6 +276,7 @@
 	"Images": "Images",
 	"Import Chats": "Importer les discussions",
 	"Import Documents Mapping": "Importer le mappage des documents",
+	"Import Functions": "",
 	"Import Models": "Importer des modèles",
 	"Import Prompts": "Importer les prompts",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "Seuls les caractères alphanumériques et les tirets sont autorisés dans la chaîne de commande.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Oups ! Tenez bon ! Vos fichiers sont encore dans le four de traitement. Nous les préparons jusqu'à la perfection. Soyez patient et nous vous informerons dès qu'ils seront prêts.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Oups ! Il semble que l'URL soit invalide. Merci de vérifier et réessayer.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oups ! Vous utilisez une méthode non prise en charge (frontal uniquement). Veuillez servir WebUI depuis le backend.",
 	"Open": "Ouvrir",
 	"Open AI": "Open AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "Modèle de reranking",
 	"Reranking model disabled": "Modèle de reranking désactivé",
 	"Reranking model set to \"{{reranking_model}}\"": "Modèle de reranking défini sur \"{{reranking_model}}\"",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "Réinitialiser le stockage vectoriel",
 	"Response AutoCopy to Clipboard": "Copie automatique de la réponse vers le presse-papiers",
@@ -425,6 +442,7 @@
 	"Search a model": "Rechercher un modèle",
 	"Search Chats": "Rechercher des chats",
 	"Search Documents": "Rechercher des documents",
+	"Search Functions": "",
 	"Search Models": "Modèles de recherche",
 	"Search Prompts": "Rechercher des prompts",
 	"Search Query Generation Prompt": "",
@@ -475,6 +493,7 @@
 	"short-summary": "résumé court",
 	"Show": "Afficher",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "Afficher les raccourcis",
 	"Showcased creativity": "Créativité affichée",
 	"sidebar": "barre latérale",
@@ -496,6 +515,7 @@
 	"System": "Système",
 	"System Prompt": "Prompt Système",
 	"Tags": "Tags",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "Donnez-nous plus:",
 	"Temperature": "Température",
@@ -507,9 +527,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "Le score doit être une valeur entre 0.0 (0%) et 1.0 (100%).",
 	"Theme": "Thème",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Cela garantit que vos précieuses conversations sont enregistrées en toute sécurité dans votre base de données backend. Merci !",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "Ce réglage ne se synchronise pas entre les navigateurs ou les appareils.",
+	"This will delete": "",
 	"Thorough explanation": "Explication approfondie",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Astuce : Mettez à jour plusieurs emplacements de variables consécutivement en appuyant sur la touche tabulation dans l'entrée de chat après chaque remplacement.",
 	"Title": "Titre",
@@ -523,6 +545,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "à l'entrée du chat.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "Aujourd'hui",
 	"Toggle settings": "Basculer les paramètres",
@@ -538,10 +561,13 @@
 	"Type": "Type",
 	"Type Hugging Face Resolve (Download) URL": "Entrez l'URL de résolution (téléchargement) Hugging Face",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh ! Il y a eu un problème de connexion à {{provider}}.",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Type de fichier inconnu '{{file_type}}', mais accepté et traité comme du texte brut",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "Mettre à jour et copier le lien",
 	"Update password": "Mettre à jour le mot de passe",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "Téléverser un modèle GGUF",
 	"Upload Files": "Téléverser des fichiers",
 	"Upload Pipeline": "",
@@ -560,6 +586,7 @@
 	"variable": "variable",
 	"variable to have them replaced with clipboard content.": "variable pour les remplacer par le contenu du presse-papiers.",
 	"Version": "Version",
+	"Voice": "",
 	"Warning": "Avertissement",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Attention : Si vous mettez à jour ou changez votre modèle d'intégration, vous devrez réimporter tous les documents.",
 	"Web": "Web",
@@ -569,7 +596,6 @@
 	"Web Search": "Recherche sur le Web",
 	"Web Search Engine": "Moteur de recherche Web",
 	"Webhook URL": "URL Webhook",
-	"WebUI Add-ons": "Add-ons WebUI",
 	"WebUI Settings": "Paramètres WebUI",
 	"WebUI will make requests to": "WebUI effectuera des demandes à",
 	"What’s New in": "Quoi de neuf dans",

+ 28 - 2
src/lib/i18n/locales/fr-FR/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "Autoriser",
 	"Allow Chat Deletion": "Autoriser la suppression du chat",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "caractères alphanumériques et tirets",
 	"Already have an account?": "Vous avez déjà un compte ?",
 	"an assistant": "un assistant",
@@ -81,6 +82,7 @@
 	"Capabilities": "Capacités",
 	"Change Password": "Changer le mot de passe",
 	"Chat": "Chat",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "UI Bulles de Chat",
 	"Chat direction": "Direction du chat",
 	"Chat History": "Historique du chat",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "Cliquez ici pour de l'aide.",
 	"Click here to": "Cliquez ici pour",
+	"Click here to download user import template file.": "",
 	"Click here to select": "Cliquez ici pour sélectionner",
 	"Click here to select a csv file.": "Cliquez ici pour sélectionner un fichier csv.",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "L'URL de base ComfyUI est requise.",
 	"Command": "Commande",
 	"Concurrent Requests": "Demandes simultanées",
+	"Confirm": "",
 	"Confirm Password": "Confirmer le mot de passe",
+	"Confirm your action": "",
 	"Connections": "Connexions",
 	"Contact Admin for WebUI Access": "",
 	"Content": "Contenu",
@@ -130,6 +135,8 @@
 	"Create new secret key": "Créer une nouvelle clé secrète",
 	"Created at": "Créé le",
 	"Created At": "Crée Le",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "Modèle actuel",
 	"Current Password": "Mot de passe actuel",
 	"Custom": "Personnalisé",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "Supprimer toutes les discussions",
 	"Delete chat": "Supprimer le chat",
 	"Delete Chat": "Supprimer le Chat",
+	"Delete chat?": "",
 	"delete this link": "supprimer ce lien",
 	"Delete User": "Supprimer l'Utilisateur",
 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "Exporter les Chats",
 	"Export Documents Mapping": "Exporter la Correspondance des Documents",
+	"Export Functions": "",
 	"Export Models": "Exporter les Modèles",
 	"Export Prompts": "Exporter les Prompts",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "Février",
 	"Feel free to add specific details": "N'hésitez pas à ajouter des détails spécifiques",
+	"File": "",
 	"File Mode": "Mode Fichier",
 	"File not found.": "Fichier non trouvé.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Usurpation d'empreinte digitale détectée : Impossible d'utiliser les initiales comme avatar. L'image de profil par défaut sera utilisée.",
 	"Fluidly stream large external response chunks": "Diffusez de manière fluide de gros morceaux de réponses externes",
 	"Focus chat input": "Concentrer sur l'entrée du chat",
 	"Followed instructions perfectly": "A suivi les instructions parfaitement",
+	"Form": "",
 	"Format your variables using square brackets like this:": "Formatez vos variables en utilisant des crochets comme ceci :",
 	"Frequency Penalty": "Pénalité de fréquence",
+	"Functions": "",
 	"General": "Général",
 	"General Settings": "Paramètres Généraux",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "Bonjour, {{name}}",
 	"Help": "Aide",
 	"Hide": "Cacher",
+	"Hide Model": "",
 	"How can I help you today?": "Comment puis-je vous aider aujourd'hui ?",
 	"Hybrid Search": "Recherche Hybride",
 	"Image Generation (Experimental)": "Génération d'Image (Expérimental)",
@@ -262,6 +276,7 @@
 	"Images": "Images",
 	"Import Chats": "Importer les Chats",
 	"Import Documents Mapping": "Importer la Correspondance des Documents",
+	"Import Functions": "",
 	"Import Models": "Importer des Modèles",
 	"Import Prompts": "Importer des Prompts",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "Seuls les caractères alphanumériques et les tirets sont autorisés dans la chaîne de commande.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Oups ! Tenez bon ! Vos fichiers sont encore dans le four. Nous les cuisinons à la perfection. Soyez patient et nous vous informerons dès qu'ils seront prêts.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Oups ! On dirait que l'URL est invalide. Vérifiez et réessayez.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oups ! Vous utilisez une méthode non-supportée (frontend uniquement). Veuillez également servir WebUI depuis le backend.",
 	"Open": "Ouvrir",
 	"Open AI": "Open AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "Modèle de Reclassement",
 	"Reranking model disabled": "Modèle de Reclassement Désactivé",
 	"Reranking model set to \"{{reranking_model}}\"": "Modèle de reclassement défini sur \"{{reranking_model}}\"",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "Réinitialiser le Stockage de Vecteur",
 	"Response AutoCopy to Clipboard": "Copie Automatique de la Réponse dans le Presse-papiers",
@@ -425,6 +442,7 @@
 	"Search a model": "Rechercher un modèle",
 	"Search Chats": "Rechercher des chats",
 	"Search Documents": "Rechercher des Documents",
+	"Search Functions": "",
 	"Search Models": "Rechercher des modèles",
 	"Search Prompts": "Rechercher des Prompts",
 	"Search Query Generation Prompt": "",
@@ -475,6 +493,7 @@
 	"short-summary": "résumé court",
 	"Show": "Montrer",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "Afficher les raccourcis",
 	"Showcased creativity": "Créativité affichée",
 	"sidebar": "barre latérale",
@@ -496,6 +515,7 @@
 	"System": "Système",
 	"System Prompt": "Prompt du Système",
 	"Tags": "Tags",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "Dites-nous en plus :",
 	"Temperature": "Température",
@@ -507,9 +527,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "Le score devrait avoir une valeur entre 0.0 (0%) et 1.0 (100%).",
 	"Theme": "Thème",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Cela garantit que vos précieuses conversations sont en sécurité dans votre base de données. Merci !",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "Ce paramètre ne se synchronise pas entre les navigateurs ou les appareils.",
+	"This will delete": "",
 	"Thorough explanation": "Explication détaillée",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Conseil : Mettez à jour plusieurs emplacements de variables consécutivement en appuyant sur la touche tab dans l'entrée de chat après chaque remplacement",
 	"Title": "Titre",
@@ -523,6 +545,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "à l'entrée du chat.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "Aujourd'hui",
 	"Toggle settings": "Basculer les paramètres",
@@ -538,10 +561,13 @@
 	"Type": "Type",
 	"Type Hugging Face Resolve (Download) URL": "Entrez l'URL de Résolution (Téléchargement) Hugging Face",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh ! Il y a eu un problème de connexion à {{provider}}.",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Type de Fichier Inconnu '{{file_type}}', mais accepté et traité comme du texte brut",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "Mettre à Jour et Copier le Lien",
 	"Update password": "Mettre à Jour le Mot de Passe",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "Téléverser un modèle GGUF",
 	"Upload Files": "Téléverser des fichiers",
 	"Upload Pipeline": "",
@@ -560,6 +586,7 @@
 	"variable": "variable",
 	"variable to have them replaced with clipboard content.": "variable pour les remplacer par le contenu du presse-papiers.",
 	"Version": "Version",
+	"Voice": "",
 	"Warning": "Avertissement",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Avertissement : Si vous mettez à jour ou modifier votre modèle d'embedding, vous devrez réimporter tous les documents.",
 	"Web": "Web",
@@ -569,7 +596,6 @@
 	"Web Search": "Recherche sur le Web",
 	"Web Search Engine": "Moteur de recherche Web",
 	"Webhook URL": "URL du Webhook",
-	"WebUI Add-ons": "Add-ons WebUI",
 	"WebUI Settings": "Paramètres WebUI",
 	"WebUI will make requests to": "WebUI effectuera des demandes à",
 	"What’s New in": "Quoi de neuf dans",

+ 28 - 2
src/lib/i18n/locales/he-IL/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "אפשר",
 	"Allow Chat Deletion": "אפשר מחיקת צ'אט",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "תווים אלפאנומריים ומקפים",
 	"Already have an account?": "כבר יש לך חשבון?",
 	"an assistant": "עוזר",
@@ -81,6 +82,7 @@
 	"Capabilities": "יכולות",
 	"Change Password": "שנה סיסמה",
 	"Chat": "צ'אט",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "UI של תיבת הדיבור",
 	"Chat direction": "כיוון צ'אט",
 	"Chat History": "היסטוריית צ'אט",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "לחץ כאן לעזרה.",
 	"Click here to": "לחץ כאן כדי",
+	"Click here to download user import template file.": "",
 	"Click here to select": "לחץ כאן לבחירה",
 	"Click here to select a csv file.": "לחץ כאן לבחירת קובץ csv.",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "נדרשת כתובת URL בסיסית של ComfyUI",
 	"Command": "פקודה",
 	"Concurrent Requests": "בקשות בו-זמניות",
+	"Confirm": "",
 	"Confirm Password": "אשר סיסמה",
+	"Confirm your action": "",
 	"Connections": "חיבורים",
 	"Contact Admin for WebUI Access": "",
 	"Content": "תוכן",
@@ -130,6 +135,8 @@
 	"Create new secret key": "צור מפתח סודי חדש",
 	"Created at": "נוצר ב",
 	"Created At": "נוצר ב",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "המודל הנוכחי",
 	"Current Password": "הסיסמה הנוכחית",
 	"Custom": "מותאם אישית",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "מחק את כל הצ'אטים",
 	"Delete chat": "מחק צ'אט",
 	"Delete Chat": "מחק צ'אט",
+	"Delete chat?": "",
 	"delete this link": "מחק את הקישור הזה",
 	"Delete User": "מחק משתמש",
 	"Deleted {{deleteModelTag}}": "נמחק {{deleteModelTag}}",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "ייצוא צ'אטים",
 	"Export Documents Mapping": "ייצוא מיפוי מסמכים",
+	"Export Functions": "",
 	"Export Models": "ייצוא מודלים",
 	"Export Prompts": "ייצוא פקודות",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "פברואר",
 	"Feel free to add specific details": "נא להוסיף פרטים ספציפיים לפי רצון",
+	"File": "",
 	"File Mode": "מצב קובץ",
 	"File not found.": "הקובץ לא נמצא.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "התגלתה הזיית טביעת אצבע: לא ניתן להשתמש בראשי תיבות כאווטאר. משתמש בתמונת פרופיל ברירת מחדל.",
 	"Fluidly stream large external response chunks": "שידור נתונים חיצוניים בקצב רציף",
 	"Focus chat input": "מיקוד הקלט לצ'אט",
 	"Followed instructions perfectly": "עקב אחר ההוראות במושלמות",
+	"Form": "",
 	"Format your variables using square brackets like this:": "עצב את המשתנים שלך באמצעות סוגריים מרובעים כך:",
 	"Frequency Penalty": "עונש תדירות",
+	"Functions": "",
 	"General": "כללי",
 	"General Settings": "הגדרות כלליות",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "שלום, {{name}}",
 	"Help": "עזרה",
 	"Hide": "הסתר",
+	"Hide Model": "",
 	"How can I help you today?": "כיצד אוכל לעזור לך היום?",
 	"Hybrid Search": "חיפוש היברידי",
 	"Image Generation (Experimental)": "יצירת תמונות (ניסיוני)",
@@ -262,6 +276,7 @@
 	"Images": "תמונות",
 	"Import Chats": "יבוא צ'אטים",
 	"Import Documents Mapping": "יבוא מיפוי מסמכים",
+	"Import Functions": "",
 	"Import Models": "ייבוא דגמים",
 	"Import Prompts": "יבוא פקודות",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "רק תווים אלפאנומריים ומקפים מותרים במחרוזת הפקודה.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "אופס! תחזיק מעמד! הקבצים שלך עדיין בתהליך העיבוד. אנו מבשלים אותם לשלמות. נא להתאזר בסבלנות ונודיע לך ברגע שיהיו מוכנים.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "אופס! נראה שהכתובת URL אינה תקינה. אנא בדוק שוב ונסה שנית.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "אופס! אתה משתמש בשיטה לא נתמכת (רק חזית). אנא שרת את ממשק המשתמש האינטרנטי מהשרת האחורי.",
 	"Open": "פתח",
 	"Open AI": "Open AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "מודל דירוג מחדש",
 	"Reranking model disabled": "מודל דירוג מחדש מושבת",
 	"Reranking model set to \"{{reranking_model}}\"": "מודל דירוג מחדש הוגדר ל-\"{{reranking_model}}\"",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "איפוס אחסון וקטורים",
 	"Response AutoCopy to Clipboard": "העתקה אוטומטית של תגובה ללוח",
@@ -425,6 +442,7 @@
 	"Search a model": "חפש מודל",
 	"Search Chats": "חיפוש צ'אטים",
 	"Search Documents": "חפש מסמכים",
+	"Search Functions": "",
 	"Search Models": "חיפוש מודלים",
 	"Search Prompts": "חפש פקודות",
 	"Search Query Generation Prompt": "",
@@ -475,6 +493,7 @@
 	"short-summary": "סיכום קצר",
 	"Show": "הצג",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "הצג קיצורי דרך",
 	"Showcased creativity": "הצגת יצירתיות",
 	"sidebar": "סרגל צד",
@@ -496,6 +515,7 @@
 	"System": "מערכת",
 	"System Prompt": "תגובת מערכת",
 	"Tags": "תגיות",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "תרשמו יותר:",
 	"Temperature": "טמפרטורה",
@@ -507,9 +527,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "ציון צריך להיות ערך בין 0.0 (0%) ל-1.0 (100%)",
 	"Theme": "נושא",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "פעולה זו מבטיחה שהשיחות בעלות הערך שלך יישמרו באופן מאובטח במסד הנתונים העורפי שלך. תודה!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "הגדרה זו אינה מסתנכרנת בין דפדפנים או מכשירים.",
+	"This will delete": "",
 	"Thorough explanation": "תיאור מפורט",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "טיפ: עדכן חריצים משתנים מרובים ברציפות על-ידי לחיצה על מקש Tab בקלט הצ'אט לאחר כל החלפה.",
 	"Title": "שם",
@@ -523,6 +545,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "לקלטת שיחה.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "היום",
 	"Toggle settings": "החלפת מצב של הגדרות",
@@ -538,10 +561,13 @@
 	"Type": "סוג",
 	"Type Hugging Face Resolve (Download) URL": "הקלד כתובת URL של פתרון פנים מחבק (הורד)",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "או-הו! אירעה בעיה בהתחברות ל- {{provider}}.",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "סוג קובץ לא ידוע '{{file_type}}', אך מקבל ומתייחס אליו כטקסט רגיל",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "עדכן ושכפל קישור",
 	"Update password": "עדכן סיסמה",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "העלה מודל GGUF",
 	"Upload Files": "העלאת קבצים",
 	"Upload Pipeline": "",
@@ -560,6 +586,7 @@
 	"variable": "משתנה",
 	"variable to have them replaced with clipboard content.": "משתנה להחליפו ב- clipboard תוכן.",
 	"Version": "גרסה",
+	"Voice": "",
 	"Warning": "אזהרה",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "אזהרה: אם תעדכן או תשנה את מודל ההטבעה שלך, יהיה עליך לייבא מחדש את כל המסמכים.",
 	"Web": "רשת",
@@ -569,7 +596,6 @@
 	"Web Search": "חיפוש באינטרנט",
 	"Web Search Engine": "מנוע חיפוש באינטרנט",
 	"Webhook URL": "URL Webhook",
-	"WebUI Add-ons": "נסיונות WebUI",
 	"WebUI Settings": "הגדרות WebUI",
 	"WebUI will make requests to": "WebUI יבקש לבקש",
 	"What’s New in": "מה חדש ב",

+ 28 - 2
src/lib/i18n/locales/hi-IN/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "अनुमति दें",
 	"Allow Chat Deletion": "चैट हटाने की अनुमति दें",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "अल्फ़ान्यूमेरिक वर्ण और हाइफ़न",
 	"Already have an account?": "क्या आपके पास पहले से एक खाता मौजूद है?",
 	"an assistant": "एक सहायक",
@@ -81,6 +82,7 @@
 	"Capabilities": "क्षमताओं",
 	"Change Password": "पासवर्ड बदलें",
 	"Chat": "चैट करें",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "चैट बॉली",
 	"Chat direction": "चैट दिशा",
 	"Chat History": "चैट का इतिहास",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "सहायता के लिए यहां क्लिक करें।",
 	"Click here to": "यहां क्लिक करें",
+	"Click here to download user import template file.": "",
 	"Click here to select": "चयन करने के लिए यहां क्लिक करें।",
 	"Click here to select a csv file.": "सीएसवी फ़ाइल का चयन करने के लिए यहां क्लिक करें।",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "ComfyUI का बेस यूआरएल आवश्यक है",
 	"Command": "कमांड",
 	"Concurrent Requests": "समवर्ती अनुरोध",
+	"Confirm": "",
 	"Confirm Password": "पासवर्ड की पुष्टि कीजिये",
+	"Confirm your action": "",
 	"Connections": "सम्बन्ध",
 	"Contact Admin for WebUI Access": "",
 	"Content": "सामग्री",
@@ -130,6 +135,8 @@
 	"Create new secret key": "नया क्रिप्टोग्राफिक क्षेत्र बनाएं",
 	"Created at": "किस समय बनाया गया",
 	"Created At": "किस समय बनाया गया",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "वर्तमान मॉडल",
 	"Current Password": "वर्तमान पासवर्ड",
 	"Custom": "कस्टम संस्करण",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "सभी चैट हटाएं",
 	"Delete chat": "चैट हटाएं",
 	"Delete Chat": "चैट हटाएं",
+	"Delete chat?": "",
 	"delete this link": "इस लिंक को हटाएं",
 	"Delete User": "उपभोक्ता मिटायें",
 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} हटा दिया गया",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "चैट निर्यात करें",
 	"Export Documents Mapping": "निर्यात दस्तावेज़ मैपिंग",
+	"Export Functions": "",
 	"Export Models": "निर्यात मॉडल",
 	"Export Prompts": "प्रॉम्प्ट निर्यात करें",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "फरवरी",
 	"Feel free to add specific details": "विशिष्ट विवरण जोड़ने के लिए स्वतंत्र महसूस करें",
+	"File": "",
 	"File Mode": "फ़ाइल मोड",
 	"File not found.": "फ़ाइल प्राप्त नहीं हुई।",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "फ़िंगरप्रिंट स्पूफ़िंग का पता चला: प्रारंभिक अक्षरों को अवतार के रूप में उपयोग करने में असमर्थ। प्रोफ़ाइल छवि को डिफ़ॉल्ट पर डिफ़ॉल्ट किया जा रहा है.",
 	"Fluidly stream large external response chunks": "बड़े बाह्य प्रतिक्रिया खंडों को तरल रूप से प्रवाहित करें",
 	"Focus chat input": "चैट इनपुट पर फ़ोकस करें",
 	"Followed instructions perfectly": "निर्देशों का पूर्णतः पालन किया",
+	"Form": "",
 	"Format your variables using square brackets like this:": "वर्गाकार कोष्ठकों का उपयोग करके अपने चरों को इस प्रकार प्रारूपित करें :",
 	"Frequency Penalty": "फ्रीक्वेंसी पेनल्टी",
+	"Functions": "",
 	"General": "सामान्य",
 	"General Settings": "सामान्य सेटिंग्स",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "नमस्ते, {{name}}",
 	"Help": "मदद",
 	"Hide": "छुपाएं",
+	"Hide Model": "",
 	"How can I help you today?": "आज मैं आपकी कैसे मदद कर सकता हूँ?",
 	"Hybrid Search": "हाइब्रिड खोज",
 	"Image Generation (Experimental)": "छवि निर्माण (प्रायोगिक)",
@@ -262,6 +276,7 @@
 	"Images": "इमेजिस",
 	"Import Chats": "चैट आयात करें",
 	"Import Documents Mapping": "दस्तावेज़ मैपिंग आयात करें",
+	"Import Functions": "",
 	"Import Models": "आयात मॉडल",
 	"Import Prompts": "प्रॉम्प्ट आयात करें",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "कमांड स्ट्रिंग में केवल अल्फ़ान्यूमेरिक वर्ण और हाइफ़न की अनुमति है।",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "उफ़! कृपया प्रतीक्षा करें, आपकी फ़ाइलें अभी भी प्रसंस्करण ओवन में हैं। हम उन्हें पूर्णता से पका रहे हैं। कृपया धैर्य रखें और जब वे तैयार हो जाएंगे तो हम आपको बता देंगे।",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "उफ़! ऐसा लगता है कि यूआरएल अमान्य है. कृपया दोबारा जांचें और पुनः प्रयास करें।",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "उफ़! आप एक असमर्थित विधि (केवल फ्रंटएंड) का उपयोग कर रहे हैं। कृपया बैकएंड से WebUI सर्वे करें।",
 	"Open": "खोलें",
 	"Open AI": "Open AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "रीरैकिंग मोड",
 	"Reranking model disabled": "पुनर्रैंकिंग मॉडल अक्षम किया गया",
 	"Reranking model set to \"{{reranking_model}}\"": "रीरैंकिंग मॉडल को \"{{reranking_model}}\" पर \u200b\u200bसेट किया गया",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "वेक्टर संग्रहण रीसेट करें",
 	"Response AutoCopy to Clipboard": "क्लिपबोर्ड पर प्रतिक्रिया ऑटोकॉपी",
@@ -425,6 +442,7 @@
 	"Search a model": "एक मॉडल खोजें",
 	"Search Chats": "चैट खोजें",
 	"Search Documents": "दस्तावेज़ खोजें",
+	"Search Functions": "",
 	"Search Models": "मॉडल खोजें",
 	"Search Prompts": "प्रॉम्प्ट खोजें",
 	"Search Query Generation Prompt": "",
@@ -474,6 +492,7 @@
 	"short-summary": "संक्षिप्त सारांश",
 	"Show": "दिखाओ",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "शॉर्टकट दिखाएँ",
 	"Showcased creativity": "रचनात्मकता का प्रदर्शन किया",
 	"sidebar": "साइड बार",
@@ -495,6 +514,7 @@
 	"System": "सिस्टम",
 	"System Prompt": "सिस्टम प्रॉम्प्ट",
 	"Tags": "टैग",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "हमें और अधिक बताएँ:",
 	"Temperature": "टेंपेरेचर",
@@ -506,9 +526,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "स्कोर का मान 0.0 (0%) और 1.0 (100%) के बीच होना चाहिए।",
 	"Theme": "थीम",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "यह सुनिश्चित करता है कि आपकी मूल्यवान बातचीत आपके बैकएंड डेटाबेस में सुरक्षित रूप से सहेजी गई है। धन्यवाद!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "यह सेटिंग सभी ब्राउज़रों या डिवाइसों में समन्वयित नहीं होती है",
+	"This will delete": "",
 	"Thorough explanation": "विस्तृत व्याख्या",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "टिप: प्रत्येक प्रतिस्थापन के बाद चैट इनपुट में टैब कुंजी दबाकर लगातार कई वैरिएबल स्लॉट अपडेट करें।",
 	"Title": "शीर्षक",
@@ -522,6 +544,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "इनपुट चैट करने के लिए.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "आज",
 	"Toggle settings": "सेटिंग्स टॉगल करें",
@@ -537,10 +560,13 @@
 	"Type": "प्रकार",
 	"Type Hugging Face Resolve (Download) URL": "हगिंग फेस रिज़ॉल्व (डाउनलोड) यूआरएल टाइप करें",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "उह ओह! {{provider}} से कनेक्ट करने में एक समस्या थी।",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "अज्ञात फ़ाइल प्रकार '{{file_type}}', लेकिन स्वीकार करना और सादे पाठ के रूप में व्यवहार करना",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "अपडेट करें और लिंक कॉपी करें",
 	"Update password": "पासवर्ड अपडेट करें",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "GGUF मॉडल अपलोड करें",
 	"Upload Files": "फ़ाइलें अपलोड करें",
 	"Upload Pipeline": "",
@@ -559,6 +585,7 @@
 	"variable": "वेरिएबल",
 	"variable to have them replaced with clipboard content.": "उन्हें क्लिपबोर्ड सामग्री से बदलने के लिए वेरिएबल।",
 	"Version": "संस्करण",
+	"Voice": "",
 	"Warning": "चेतावनी",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "चेतावनी: यदि आप अपने एम्बेडिंग मॉडल को अपडेट या बदलते हैं, तो आपको सभी दस्तावेज़ों को फिर से आयात करने की आवश्यकता होगी।",
 	"Web": "वेब",
@@ -568,7 +595,6 @@
 	"Web Search": "वेब खोज",
 	"Web Search Engine": "वेब खोज इंजन",
 	"Webhook URL": "वेबहुक URL",
-	"WebUI Add-ons": "वेबयू ऐड-ons",
 	"WebUI Settings": "WebUI सेटिंग्स",
 	"WebUI will make requests to": "WebUI अनुरोध करेगा",
 	"What’s New in": "इसमें नया क्या है",

+ 28 - 2
src/lib/i18n/locales/hr-HR/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "Dopusti",
 	"Allow Chat Deletion": "Dopusti brisanje razgovora",
 	"Allow non-local voices": "Dopusti nelokalne glasove",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "alfanumerički znakovi i crtice",
 	"Already have an account?": "Već imate račun?",
 	"an assistant": "asistent",
@@ -81,6 +82,7 @@
 	"Capabilities": "Mogućnosti",
 	"Change Password": "Promijeni lozinku",
 	"Chat": "Razgovor",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "Razgovor - Bubble UI",
 	"Chat direction": "Razgovor - smijer",
 	"Chat History": "Povijest razgovora",
@@ -97,6 +99,7 @@
 	"Clear memory": "Očisti memoriju",
 	"Click here for help.": "Kliknite ovdje za pomoć.",
 	"Click here to": "Kliknite ovdje za",
+	"Click here to download user import template file.": "",
 	"Click here to select": "Kliknite ovdje za odabir",
 	"Click here to select a csv file.": "Kliknite ovdje da odaberete csv datoteku.",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "Potreban je ComfyUI osnovni URL.",
 	"Command": "Naredba",
 	"Concurrent Requests": "Istodobni zahtjevi",
+	"Confirm": "",
 	"Confirm Password": "Potvrdite lozinku",
+	"Confirm your action": "",
 	"Connections": "Povezivanja",
 	"Contact Admin for WebUI Access": "Kontaktirajte admina za WebUI pristup",
 	"Content": "Sadržaj",
@@ -130,6 +135,8 @@
 	"Create new secret key": "Stvori novi tajni ključ",
 	"Created at": "Stvoreno",
 	"Created At": "Stvoreno",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "Trenutni model",
 	"Current Password": "Trenutna lozinka",
 	"Custom": "Prilagođeno",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "Izbriši sve razgovore",
 	"Delete chat": "Izbriši razgovor",
 	"Delete Chat": "Izbriši razgovor",
+	"Delete chat?": "",
 	"delete this link": "izbriši ovu vezu",
 	"Delete User": "Izbriši korisnika",
 	"Deleted {{deleteModelTag}}": "Izbrisan {{deleteModelTag}}",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "Izvoz četa (.json)",
 	"Export Chats": "Izvoz razgovora",
 	"Export Documents Mapping": "Izvoz mapiranja dokumenata",
+	"Export Functions": "",
 	"Export Models": "Izvoz modela",
 	"Export Prompts": "Izvoz prompta",
 	"Export Tools": "Izvoz alata",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "Greška kod ažuriranja postavki",
 	"February": "Veljača",
 	"Feel free to add specific details": "Slobodno dodajte specifične detalje",
+	"File": "",
 	"File Mode": "Način datoteke",
 	"File not found.": "Datoteka nije pronađena.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Otkriveno krivotvorenje otisaka prstiju: Nemoguće je koristiti inicijale kao avatar. Postavljanje na zadanu profilnu sliku.",
 	"Fluidly stream large external response chunks": "Glavno strujanje velikih vanjskih dijelova odgovora",
 	"Focus chat input": "Fokusiraj unos razgovora",
 	"Followed instructions perfectly": "Savršeno slijedio upute",
+	"Form": "",
 	"Format your variables using square brackets like this:": "Formatirajte svoje varijable pomoću uglatih zagrada ovako:",
 	"Frequency Penalty": "Kazna za učestalost",
+	"Functions": "",
 	"General": "Općenito",
 	"General Settings": "Opće postavke",
 	"Generate Image": "Gneriraj sliku",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "Bok, {{name}}",
 	"Help": "Pomoć",
 	"Hide": "Sakrij",
+	"Hide Model": "",
 	"How can I help you today?": "Kako vam mogu pomoći danas?",
 	"Hybrid Search": "Hibridna pretraga",
 	"Image Generation (Experimental)": "Generiranje slika (eksperimentalno)",
@@ -262,6 +276,7 @@
 	"Images": "Slike",
 	"Import Chats": "Uvoz razgovora",
 	"Import Documents Mapping": "Uvoz mapiranja dokumenata",
+	"Import Functions": "",
 	"Import Models": "Uvoz modela",
 	"Import Prompts": "Uvoz prompta",
 	"Import Tools": "Uvoz alata",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "Samo alfanumerički znakovi i crtice su dopušteni u naredbenom nizu.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Ups! Držite se! Vaše datoteke su još uvijek u procesu obrade. Pečemo ih do savršenstva. Molimo vas da budete strpljivi i obavijestit ćemo vas kada budu spremne.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Ups! Izgleda da je URL nevažeći. Molimo provjerite ponovno i pokušajte ponovo.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ups! Koristite nepodržanu metodu (samo frontend). Molimo poslužite WebUI s backend-a.",
 	"Open": "Otvoreno",
 	"Open AI": "Open AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "Model za ponovno rangiranje",
 	"Reranking model disabled": "Model za ponovno rangiranje onemogućen",
 	"Reranking model set to \"{{reranking_model}}\"": "Model za ponovno rangiranje postavljen na \"{{reranking_model}}\"",
+	"Reset": "",
 	"Reset Upload Directory": "Poništi upload direktorij",
 	"Reset Vector Storage": "Resetiraj pohranu vektora",
 	"Response AutoCopy to Clipboard": "Automatsko kopiranje odgovora u međuspremnik",
@@ -425,6 +442,7 @@
 	"Search a model": "Pretraži model",
 	"Search Chats": "Pretraži razgovore",
 	"Search Documents": "Pretraga dokumenata",
+	"Search Functions": "",
 	"Search Models": "Pretražite modele",
 	"Search Prompts": "Pretraga prompta",
 	"Search Query Generation Prompt": "Upit za generiranje upita za pretraživanje",
@@ -475,6 +493,7 @@
 	"short-summary": "kratki sažetak",
 	"Show": "Pokaži",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "Pokaži prečace",
 	"Showcased creativity": "Prikazana kreativnost",
 	"sidebar": "bočna traka",
@@ -496,6 +515,7 @@
 	"System": "Sustav",
 	"System Prompt": "Sistemski prompt",
 	"Tags": "Oznake",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "Recite nam više:",
 	"Temperature": "Temperatura",
@@ -507,9 +527,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "Ocjena treba biti vrijednost između 0,0 (0%) i 1,0 (100%).",
 	"Theme": "Tema",
 	"Thinking...": "Razmišljam",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Ovo osigurava da su vaši vrijedni razgovori sigurno spremljeni u bazu podataka. Hvala vam!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "Ovo je eksperimentalna značajka, možda neće funkcionirati prema očekivanjima i podložna je promjenama u bilo kojem trenutku.",
 	"This setting does not sync across browsers or devices.": "Ova postavka se ne sinkronizira između preglednika ili uređaja.",
+	"This will delete": "",
 	"Thorough explanation": "Detaljno objašnjenje",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Savjet: Ažurirajte više mjesta za varijable uzastopno pritiskom na tipku tab u unosu razgovora nakon svake zamjene.",
 	"Title": "Naslov",
@@ -523,6 +545,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Za pristup WebUI-u obratite se administratoru. Administratori mogu upravljati statusima korisnika s Admin panela.",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "Da biste ovdje dodali dokumente, prvo ih prenesite u radni prostor \"Dokumenti\".",
 	"to chat input.": "u unos razgovora.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "Danas",
 	"Toggle settings": "Prebaci postavke",
@@ -538,10 +561,13 @@
 	"Type": "Tip",
 	"Type Hugging Face Resolve (Download) URL": "Upišite Hugging Face Resolve (Download) URL",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Pojavio se problem s povezivanjem na {{provider}}.",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Nepoznata vrsta datoteke '{{file_type}}', ali prihvaćena i obrađuje se kao običan tekst",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "Ažuriraj i kopiraj vezu",
 	"Update password": "Ažuriraj lozinku",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "Učitaj GGUF model",
 	"Upload Files": "Prijenos datoteka",
 	"Upload Pipeline": "Prijenos kanala",
@@ -560,6 +586,7 @@
 	"variable": "varijabla",
 	"variable to have them replaced with clipboard content.": "varijabla za zamjenu sadržajem međuspremnika.",
 	"Version": "Verzija",
+	"Voice": "",
 	"Warning": "Upozorenje",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Upozorenje: Ako ažurirate ili promijenite svoj model za umetanje, morat ćete ponovno uvesti sve dokumente.",
 	"Web": "Web",
@@ -569,7 +596,6 @@
 	"Web Search": "Internet pretraga",
 	"Web Search Engine": "Web tražilica",
 	"Webhook URL": "URL webkuke",
-	"WebUI Add-ons": "Dodaci za WebUI",
 	"WebUI Settings": "WebUI postavke",
 	"WebUI will make requests to": "WebUI će slati zahtjeve na",
 	"What’s New in": "Što je novo u",

+ 28 - 2
src/lib/i18n/locales/it-IT/translation.json

@@ -42,6 +42,7 @@
 	"Allow": "Consenti",
 	"Allow Chat Deletion": "Consenti l'eliminazione della chat",
 	"Allow non-local voices": "",
+	"Allow User Location": "",
 	"alphanumeric characters and hyphens": "caratteri alfanumerici e trattini",
 	"Already have an account?": "Hai già un account?",
 	"an assistant": "un assistente",
@@ -81,6 +82,7 @@
 	"Capabilities": "Funzionalità",
 	"Change Password": "Cambia password",
 	"Chat": "Chat",
+	"Chat Background Image": "",
 	"Chat Bubble UI": "UI bolle chat",
 	"Chat direction": "Direzione chat",
 	"Chat History": "Cronologia chat",
@@ -97,6 +99,7 @@
 	"Clear memory": "",
 	"Click here for help.": "Clicca qui per aiuto.",
 	"Click here to": "Clicca qui per",
+	"Click here to download user import template file.": "",
 	"Click here to select": "Clicca qui per selezionare",
 	"Click here to select a csv file.": "Clicca qui per selezionare un file csv.",
 	"Click here to select a py file.": "",
@@ -111,7 +114,9 @@
 	"ComfyUI Base URL is required.": "L'URL base ComfyUI è obbligatorio.",
 	"Command": "Comando",
 	"Concurrent Requests": "Richieste simultanee",
+	"Confirm": "",
 	"Confirm Password": "Conferma password",
+	"Confirm your action": "",
 	"Connections": "Connessioni",
 	"Contact Admin for WebUI Access": "",
 	"Content": "Contenuto",
@@ -130,6 +135,8 @@
 	"Create new secret key": "Crea nuova chiave segreta",
 	"Created at": "Creato il",
 	"Created At": "Creato il",
+	"Created by": "",
+	"CSV Import": "",
 	"Current Model": "Modello corrente",
 	"Current Password": "Password corrente",
 	"Custom": "Personalizzato",
@@ -151,6 +158,7 @@
 	"Delete All Chats": "Elimina tutte le chat",
 	"Delete chat": "Elimina chat",
 	"Delete Chat": "Elimina chat",
+	"Delete chat?": "",
 	"delete this link": "elimina questo link",
 	"Delete User": "Elimina utente",
 	"Deleted {{deleteModelTag}}": "Eliminato {{deleteModelTag}}",
@@ -224,6 +232,7 @@
 	"Export chat (.json)": "",
 	"Export Chats": "Esporta chat",
 	"Export Documents Mapping": "Esporta mappatura documenti",
+	"Export Functions": "",
 	"Export Models": "Esporta modelli",
 	"Export Prompts": "Esporta prompt",
 	"Export Tools": "",
@@ -233,14 +242,18 @@
 	"Failed to update settings": "",
 	"February": "Febbraio",
 	"Feel free to add specific details": "Sentiti libero/a di aggiungere dettagli specifici",
+	"File": "",
 	"File Mode": "Modalità file",
 	"File not found.": "File non trovato.",
+	"Filters": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Rilevato spoofing delle impronte digitali: impossibile utilizzare le iniziali come avatar. Ripristino all'immagine del profilo predefinita.",
 	"Fluidly stream large external response chunks": "Trasmetti in modo fluido blocchi di risposta esterni di grandi dimensioni",
 	"Focus chat input": "Metti a fuoco l'input della chat",
 	"Followed instructions perfectly": "Ha seguito le istruzioni alla perfezione",
+	"Form": "",
 	"Format your variables using square brackets like this:": "Formatta le tue variabili usando parentesi quadre come questa:",
 	"Frequency Penalty": "Penalità di frequenza",
+	"Functions": "",
 	"General": "Generale",
 	"General Settings": "Impostazioni generali",
 	"Generate Image": "",
@@ -254,6 +267,7 @@
 	"Hello, {{name}}": "Ciao, {{name}}",
 	"Help": "Aiuto",
 	"Hide": "Nascondi",
+	"Hide Model": "",
 	"How can I help you today?": "Come posso aiutarti oggi?",
 	"Hybrid Search": "Ricerca ibrida",
 	"Image Generation (Experimental)": "Generazione di immagini (sperimentale)",
@@ -262,6 +276,7 @@
 	"Images": "Immagini",
 	"Import Chats": "Importa chat",
 	"Import Documents Mapping": "Importa mappatura documenti",
+	"Import Functions": "",
 	"Import Models": "Importazione di modelli",
 	"Import Prompts": "Importa prompt",
 	"Import Tools": "",
@@ -354,6 +369,7 @@
 	"Only alphanumeric characters and hyphens are allowed in the command string.": "Nella stringa di comando sono consentiti solo caratteri alfanumerici e trattini.",
 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Ops! Aspetta! I tuoi file sono ancora in fase di elaborazione. Li stiamo cucinando alla perfezione. Per favore sii paziente e ti faremo sapere quando saranno pronti.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Ops! Sembra che l'URL non sia valido. Si prega di ricontrollare e riprovare.",
+	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ops! Stai utilizzando un metodo non supportato (solo frontend). Si prega di servire la WebUI dal backend.",
 	"Open": "Apri",
 	"Open AI": "Open AI",
@@ -406,6 +422,7 @@
 	"Reranking Model": "Modello di riclassificazione",
 	"Reranking model disabled": "Modello di riclassificazione disabilitato",
 	"Reranking model set to \"{{reranking_model}}\"": "Modello di riclassificazione impostato su \"{{reranking_model}}\"",
+	"Reset": "",
 	"Reset Upload Directory": "",
 	"Reset Vector Storage": "Reimposta archivio vettoriale",
 	"Response AutoCopy to Clipboard": "Copia automatica della risposta negli appunti",
@@ -425,6 +442,7 @@
 	"Search a model": "Cerca un modello",
 	"Search Chats": "Cerca nelle chat",
 	"Search Documents": "Cerca documenti",
+	"Search Functions": "",
 	"Search Models": "Cerca modelli",
 	"Search Prompts": "Cerca prompt",
 	"Search Query Generation Prompt": "",
@@ -475,6 +493,7 @@
 	"short-summary": "riassunto-breve",
 	"Show": "Mostra",
 	"Show Admin Details in Account Pending Overlay": "",
+	"Show Model": "",
 	"Show shortcuts": "Mostra",
 	"Showcased creativity": "Creatività messa in mostra",
 	"sidebar": "barra laterale",
@@ -496,6 +515,7 @@
 	"System": "Sistema",
 	"System Prompt": "Prompt di sistema",
 	"Tags": "Tag",
+	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "Raccontaci di più:",
 	"Temperature": "Temperatura",
@@ -507,9 +527,11 @@
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "Il punteggio dovrebbe essere un valore compreso tra 0.0 (0%) e 1.0 (100%).",
 	"Theme": "Tema",
 	"Thinking...": "",
+	"This action cannot be undone. Do you wish to continue?": "",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Ciò garantisce che le tue preziose conversazioni siano salvate in modo sicuro nel tuo database backend. Grazie!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
 	"This setting does not sync across browsers or devices.": "Questa impostazione non si sincronizza tra browser o dispositivi.",
+	"This will delete": "",
 	"Thorough explanation": "Spiegazione dettagliata",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Suggerimento: aggiorna più slot di variabili consecutivamente premendo il tasto tab nell'input della chat dopo ogni sostituzione.",
 	"Title": "Titolo",
@@ -523,6 +545,7 @@
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
 	"To add documents here, upload them to the \"Documents\" workspace first.": "",
 	"to chat input.": "all'input della chat.",
+	"To select filters here, add them to the \"Functions\" workspace first.": "",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "",
 	"Today": "Oggi",
 	"Toggle settings": "Attiva/disattiva impostazioni",
@@ -538,10 +561,13 @@
 	"Type": "Digitare",
 	"Type Hugging Face Resolve (Download) URL": "Digita l'URL di Hugging Face Resolve (Download)",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Si è verificato un problema durante la connessione a {{provider}}.",
-	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tipo di file sconosciuto '{{file_type}}', ma accettato e trattato come testo normale",
+	"UI": "",
+	"Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "",
 	"Update": "",
 	"Update and Copy Link": "Aggiorna e copia link",
 	"Update password": "Aggiorna password",
+	"Updated at": "",
+	"Upload": "",
 	"Upload a GGUF model": "Carica un modello GGUF",
 	"Upload Files": "Carica file",
 	"Upload Pipeline": "",
@@ -560,6 +586,7 @@
 	"variable": "variabile",
 	"variable to have them replaced with clipboard content.": "variabile per farli sostituire con il contenuto degli appunti.",
 	"Version": "Versione",
+	"Voice": "",
 	"Warning": "Avvertimento",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Attenzione: se aggiorni o cambi il tuo modello di embedding, dovrai reimportare tutti i documenti.",
 	"Web": "Web",
@@ -569,7 +596,6 @@
 	"Web Search": "Ricerca sul Web",
 	"Web Search Engine": "Motore di ricerca Web",
 	"Webhook URL": "URL webhook",
-	"WebUI Add-ons": "Componenti aggiuntivi WebUI",
 	"WebUI Settings": "Impostazioni WebUI",
 	"WebUI will make requests to": "WebUI effettuerà richieste a",
 	"What’s New in": "Novità in",

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác