Bladeren bron

Merge branch 'upstream-dev' into dev

Jannik Streidl 6 maanden geleden
bovenliggende
commit
f47c9c69e3
100 gewijzigde bestanden met toevoegingen van 2763 en 2839 verwijderingen
  1. 1 1
      backend/open_webui/apps/audio/main.py
  2. 18 6
      backend/open_webui/apps/images/utils/comfyui.py
  3. 29 55
      backend/open_webui/apps/ollama/main.py
  4. 12 16
      backend/open_webui/apps/retrieval/main.py
  5. 45 43
      backend/open_webui/apps/retrieval/utils.py
  6. 4 0
      backend/open_webui/apps/retrieval/vector/connector.py
  7. 176 0
      backend/open_webui/apps/retrieval/vector/dbs/qdrant.py
  8. 248 12
      backend/open_webui/apps/webui/models/chats.py
  9. 44 1
      backend/open_webui/apps/webui/models/files.py
  10. 6 0
      backend/open_webui/apps/webui/models/knowledge.py
  11. 17 178
      backend/open_webui/apps/webui/models/tags.py
  12. 10 1
      backend/open_webui/apps/webui/routers/auths.py
  13. 156 88
      backend/open_webui/apps/webui/routers/chats.py
  14. 10 4
      backend/open_webui/apps/webui/routers/files.py
  15. 6 1
      backend/open_webui/apps/webui/routers/knowledge.py
  16. 10 4
      backend/open_webui/config.py
  17. 13 2
      backend/open_webui/env.py
  18. 40 5
      backend/open_webui/main.py
  19. 151 0
      backend/open_webui/migrations/versions/1af9b942657b_migrate_tags.py
  20. 82 0
      backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py
  21. 0 105
      backend/open_webui/test/apps/webui/routers/test_documents.py
  22. 2 0
      backend/requirements.txt
  23. 0 44
      cypress/e2e/documents.cy.ts
  24. 0 44
      cypress/support/e2e.ts
  25. 1 0
      pyproject.toml
  26. 153 7
      src/lib/apis/chats/index.ts
  27. 0 232
      src/lib/apis/documents/index.ts
  28. 1 1
      src/lib/apis/retrieval/index.ts
  29. 20 13
      src/lib/components/AddFilesPlaceholder.svelte
  30. 12 6
      src/lib/components/admin/Settings/Documents.svelte
  31. 13 1
      src/lib/components/chat/Artifacts.svelte
  32. 164 42
      src/lib/components/chat/Chat.svelte
  33. 56 13
      src/lib/components/chat/ChatControls.svelte
  34. 3 3
      src/lib/components/chat/Controls/Controls.svelte
  35. 63 56
      src/lib/components/chat/MessageInput.svelte
  36. 8 68
      src/lib/components/chat/MessageInput/Commands.svelte
  37. 128 40
      src/lib/components/chat/MessageInput/Commands/Knowledge.svelte
  38. 3 7
      src/lib/components/chat/MessageInput/Commands/Models.svelte
  39. 5 9
      src/lib/components/chat/MessageInput/Commands/Prompts.svelte
  40. 1 0
      src/lib/components/chat/Messages/CodeBlock.svelte
  41. 1 1
      src/lib/components/chat/Messages/ResponseMessage.svelte
  42. 1 1
      src/lib/components/chat/Messages/UserMessage.svelte
  43. 15 4
      src/lib/components/chat/Overview.svelte
  44. 5 2
      src/lib/components/chat/Placeholder.svelte
  45. 6 25
      src/lib/components/chat/Tags.svelte
  46. 26 2
      src/lib/components/common/SVGPanZoom.svelte
  47. 0 166
      src/lib/components/documents/AddDocModal.svelte
  48. 0 181
      src/lib/components/documents/EditDocModal.svelte
  49. 15 0
      src/lib/components/icons/ArrowLeft.svelte
  50. 3 2
      src/lib/components/layout/Navbar.svelte
  51. 45 35
      src/lib/components/layout/Navbar/Menu.svelte
  52. 275 178
      src/lib/components/layout/Sidebar.svelte
  53. 8 4
      src/lib/components/layout/Sidebar/ChatItem.svelte
  54. 16 11
      src/lib/components/layout/Sidebar/ChatMenu.svelte
  55. 0 627
      src/lib/components/workspace/Documents.svelte
  56. 2 1
      src/lib/components/workspace/Knowledge.svelte
  57. 87 66
      src/lib/components/workspace/Knowledge/Collection.svelte
  58. 2 2
      src/lib/components/workspace/Knowledge/Collection/AddTextContentModal.svelte
  59. 3 1
      src/lib/components/workspace/Models.svelte
  60. 4 1
      src/lib/i18n/locales/ar-BH/translation.json
  61. 4 1
      src/lib/i18n/locales/bg-BG/translation.json
  62. 4 1
      src/lib/i18n/locales/bn-BD/translation.json
  63. 20 17
      src/lib/i18n/locales/ca-ES/translation.json
  64. 4 1
      src/lib/i18n/locales/ceb-PH/translation.json
  65. 4 1
      src/lib/i18n/locales/de-DE/translation.json
  66. 4 1
      src/lib/i18n/locales/dg-DG/translation.json
  67. 4 1
      src/lib/i18n/locales/en-GB/translation.json
  68. 4 1
      src/lib/i18n/locales/en-US/translation.json
  69. 157 154
      src/lib/i18n/locales/es-ES/translation.json
  70. 4 1
      src/lib/i18n/locales/fa-IR/translation.json
  71. 4 1
      src/lib/i18n/locales/fi-FI/translation.json
  72. 4 1
      src/lib/i18n/locales/fr-CA/translation.json
  73. 63 60
      src/lib/i18n/locales/fr-FR/translation.json
  74. 4 1
      src/lib/i18n/locales/he-IL/translation.json
  75. 4 1
      src/lib/i18n/locales/hi-IN/translation.json
  76. 4 1
      src/lib/i18n/locales/hr-HR/translation.json
  77. 4 1
      src/lib/i18n/locales/id-ID/translation.json
  78. 4 1
      src/lib/i18n/locales/ie-GA/translation.json
  79. 4 1
      src/lib/i18n/locales/it-IT/translation.json
  80. 143 143
      src/lib/i18n/locales/ja-JP/translation.json
  81. 4 1
      src/lib/i18n/locales/ka-GE/translation.json
  82. 4 1
      src/lib/i18n/locales/ko-KR/translation.json
  83. 4 1
      src/lib/i18n/locales/lt-LT/translation.json
  84. 4 1
      src/lib/i18n/locales/ms-MY/translation.json
  85. 4 1
      src/lib/i18n/locales/nb-NO/translation.json
  86. 4 1
      src/lib/i18n/locales/nl-NL/translation.json
  87. 4 1
      src/lib/i18n/locales/pa-IN/translation.json
  88. 4 1
      src/lib/i18n/locales/pl-PL/translation.json
  89. 4 1
      src/lib/i18n/locales/pt-BR/translation.json
  90. 4 1
      src/lib/i18n/locales/pt-PT/translation.json
  91. 4 1
      src/lib/i18n/locales/ro-RO/translation.json
  92. 4 1
      src/lib/i18n/locales/ru-RU/translation.json
  93. 4 1
      src/lib/i18n/locales/sr-RS/translation.json
  94. 4 1
      src/lib/i18n/locales/sv-SE/translation.json
  95. 4 1
      src/lib/i18n/locales/th-TH/translation.json
  96. 4 1
      src/lib/i18n/locales/tk-TW/translation.json
  97. 4 1
      src/lib/i18n/locales/tr-TR/translation.json
  98. 4 1
      src/lib/i18n/locales/uk-UA/translation.json
  99. 4 1
      src/lib/i18n/locales/vi-VN/translation.json
  100. 15 12
      src/lib/i18n/locales/zh-CN/translation.json

+ 1 - 1
backend/open_webui/apps/audio/main.py

@@ -450,7 +450,7 @@ def transcribe(file_path):
                 except Exception:
                     error_detail = f"External: {e}"
 
-            raise error_detail
+            raise Exception(error_detail)
 
 
 @app.post("/transcriptions")

+ 18 - 6
backend/open_webui/apps/images/utils/comfyui.py

@@ -125,22 +125,34 @@ async def comfyui_generate_image(
                     workflow[node_id]["inputs"][node.key] = model
             elif node.type == "prompt":
                 for node_id in node.node_ids:
-                    workflow[node_id]["inputs"]["text"] = payload.prompt
+                    workflow[node_id]["inputs"][
+                        node.key if node.key else "text"
+                    ] = payload.prompt
             elif node.type == "negative_prompt":
                 for node_id in node.node_ids:
-                    workflow[node_id]["inputs"]["text"] = payload.negative_prompt
+                    workflow[node_id]["inputs"][
+                        node.key if node.key else "text"
+                    ] = payload.negative_prompt
             elif node.type == "width":
                 for node_id in node.node_ids:
-                    workflow[node_id]["inputs"]["width"] = payload.width
+                    workflow[node_id]["inputs"][
+                        node.key if node.key else "width"
+                    ] = payload.width
             elif node.type == "height":
                 for node_id in node.node_ids:
-                    workflow[node_id]["inputs"]["height"] = payload.height
+                    workflow[node_id]["inputs"][
+                        node.key if node.key else "height"
+                    ] = payload.height
             elif node.type == "n":
                 for node_id in node.node_ids:
-                    workflow[node_id]["inputs"]["batch_size"] = payload.n
+                    workflow[node_id]["inputs"][
+                        node.key if node.key else "batch_size"
+                    ] = payload.n
             elif node.type == "steps":
                 for node_id in node.node_ids:
-                    workflow[node_id]["inputs"]["steps"] = payload.steps
+                    workflow[node_id]["inputs"][
+                        node.key if node.key else "steps"
+                    ] = payload.steps
             elif node.type == "seed":
                 seed = (
                     payload.seed

+ 29 - 55
backend/open_webui/apps/ollama/main.py

@@ -547,8 +547,8 @@ class GenerateEmbeddingsForm(BaseModel):
 
 class GenerateEmbedForm(BaseModel):
     model: str
-    input: str
-    truncate: Optional[bool]
+    input: list[str]|str
+    truncate: Optional[bool] = None
     options: Optional[dict] = None
     keep_alive: Optional[Union[int, str]] = None
 
@@ -560,48 +560,7 @@ async def generate_embeddings(
     url_idx: Optional[int] = None,
     user=Depends(get_verified_user),
 ):
-    if url_idx is None:
-        model = form_data.model
-
-        if ":" not in model:
-            model = f"{model}:latest"
-
-        if model in app.state.MODELS:
-            url_idx = random.choice(app.state.MODELS[model]["urls"])
-        else:
-            raise HTTPException(
-                status_code=400,
-                detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
-            )
-
-    url = app.state.config.OLLAMA_BASE_URLS[url_idx]
-    log.info(f"url: {url}")
-
-    r = requests.request(
-        method="POST",
-        url=f"{url}/api/embed",
-        headers={"Content-Type": "application/json"},
-        data=form_data.model_dump_json(exclude_none=True).encode(),
-    )
-    try:
-        r.raise_for_status()
-
-        return r.json()
-    except Exception as e:
-        log.exception(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 Exception:
-                error_detail = f"Ollama: {e}"
-
-        raise HTTPException(
-            status_code=r.status_code if r else 500,
-            detail=error_detail,
-        )
+    return generate_ollama_batch_embeddings(form_data, url_idx)
 
 
 @app.post("/api/embeddings")
@@ -611,6 +570,15 @@ async def generate_embeddings(
     url_idx: Optional[int] = None,
     user=Depends(get_verified_user),
 ):
+    return generate_ollama_embeddings(form_data=form_data, url_idx=url_idx)
+
+
+def generate_ollama_embeddings(
+    form_data: GenerateEmbeddingsForm,
+    url_idx: Optional[int] = None,
+):
+    log.info(f"generate_ollama_embeddings {form_data}")
+
     if url_idx is None:
         model = form_data.model
 
@@ -637,7 +605,14 @@ async def generate_embeddings(
     try:
         r.raise_for_status()
 
-        return r.json()
+        data = r.json()
+
+        log.info(f"generate_ollama_embeddings {data}")
+
+        if "embedding" in data:
+            return data
+        else:
+            raise Exception("Something went wrong :/")
     except Exception as e:
         log.exception(e)
         error_detail = "Open WebUI: Server Connection Error"
@@ -655,11 +630,11 @@ async def generate_embeddings(
         )
 
 
-def generate_ollama_embeddings(
-    form_data: GenerateEmbeddingsForm,
+def generate_ollama_batch_embeddings(
+    form_data: GenerateEmbedForm,
     url_idx: Optional[int] = None,
 ):
-    log.info(f"generate_ollama_embeddings {form_data}")
+    log.info(f"generate_ollama_batch_embeddings {form_data}")
 
     if url_idx is None:
         model = form_data.model
@@ -680,7 +655,7 @@ def generate_ollama_embeddings(
 
     r = requests.request(
         method="POST",
-        url=f"{url}/api/embeddings",
+        url=f"{url}/api/embed",
         headers={"Content-Type": "application/json"},
         data=form_data.model_dump_json(exclude_none=True).encode(),
     )
@@ -689,10 +664,10 @@ def generate_ollama_embeddings(
 
         data = r.json()
 
-        log.info(f"generate_ollama_embeddings {data}")
+        log.info(f"generate_ollama_batch_embeddings {data}")
 
-        if "embedding" in data:
-            return data["embedding"]
+        if "embeddings" in data:
+            return data
         else:
             raise Exception("Something went wrong :/")
     except Exception as e:
@@ -788,8 +763,7 @@ async def generate_chat_completion(
     user=Depends(get_verified_user),
 ):
     payload = {**form_data.model_dump(exclude_none=True)}
-    log.debug(f"{payload = }")
-
+    log.debug(f"generate_chat_completion() - 1.payload = {payload}")
     if "metadata" in payload:
         del payload["metadata"]
 
@@ -824,7 +798,7 @@ async def generate_chat_completion(
 
     url = get_ollama_url(url_idx, payload["model"])
     log.info(f"url: {url}")
-    log.debug(payload)
+    log.debug(f"generate_chat_completion() - 2.payload = {payload}")
 
     return await post_streaming_url(
         f"{url}/api/chat",

+ 12 - 16
backend/open_webui/apps/retrieval/main.py

@@ -63,7 +63,7 @@ from open_webui.config import (
     RAG_EMBEDDING_MODEL,
     RAG_EMBEDDING_MODEL_AUTO_UPDATE,
     RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
-    RAG_EMBEDDING_OPENAI_BATCH_SIZE,
+    RAG_EMBEDDING_BATCH_SIZE,
     RAG_FILE_MAX_COUNT,
     RAG_FILE_MAX_SIZE,
     RAG_OPENAI_API_BASE_URL,
@@ -134,7 +134,7 @@ app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
 
 app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
 app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
-app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = RAG_EMBEDDING_OPENAI_BATCH_SIZE
+app.state.config.RAG_EMBEDDING_BATCH_SIZE = RAG_EMBEDDING_BATCH_SIZE
 app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
 app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
 
@@ -233,7 +233,7 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
     app.state.sentence_transformer_ef,
     app.state.config.OPENAI_API_KEY,
     app.state.config.OPENAI_API_BASE_URL,
-    app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
+    app.state.config.RAG_EMBEDDING_BATCH_SIZE,
 )
 
 app.add_middleware(
@@ -267,7 +267,7 @@ async def get_status():
         "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
         "embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
         "reranking_model": app.state.config.RAG_RERANKING_MODEL,
-        "openai_batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
+        "embedding_batch_size": app.state.config.RAG_EMBEDDING_BATCH_SIZE,
     }
 
 
@@ -277,10 +277,10 @@ async def get_embedding_config(user=Depends(get_admin_user)):
         "status": True,
         "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
         "embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
+        "embedding_batch_size": app.state.config.RAG_EMBEDDING_BATCH_SIZE,
         "openai_config": {
             "url": app.state.config.OPENAI_API_BASE_URL,
             "key": app.state.config.OPENAI_API_KEY,
-            "batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
         },
     }
 
@@ -296,13 +296,13 @@ async def get_reraanking_config(user=Depends(get_admin_user)):
 class OpenAIConfigForm(BaseModel):
     url: str
     key: str
-    batch_size: Optional[int] = None
 
 
 class EmbeddingModelUpdateForm(BaseModel):
     openai_config: Optional[OpenAIConfigForm] = None
     embedding_engine: str
     embedding_model: str
+    embedding_batch_size: Optional[int] = 1
 
 
 @app.post("/embedding/update")
@@ -320,11 +320,7 @@ async def update_embedding_config(
             if form_data.openai_config is not None:
                 app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
                 app.state.config.OPENAI_API_KEY = form_data.openai_config.key
-                app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = (
-                    form_data.openai_config.batch_size
-                    if form_data.openai_config.batch_size
-                    else 1
-                )
+            app.state.config.RAG_EMBEDDING_BATCH_SIZE = form_data.embedding_batch_size
 
         update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
 
@@ -334,17 +330,17 @@ async def update_embedding_config(
             app.state.sentence_transformer_ef,
             app.state.config.OPENAI_API_KEY,
             app.state.config.OPENAI_API_BASE_URL,
-            app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
+            app.state.config.RAG_EMBEDDING_BATCH_SIZE,
         )
 
         return {
             "status": True,
             "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
             "embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
+            "embedding_batch_size": app.state.config.RAG_EMBEDDING_BATCH_SIZE,
             "openai_config": {
                 "url": app.state.config.OPENAI_API_BASE_URL,
                 "key": app.state.config.OPENAI_API_KEY,
-                "batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
             },
         }
     except Exception as e:
@@ -645,7 +641,7 @@ def save_docs_to_vector_db(
             filter={"hash": metadata["hash"]},
         )
 
-        if result:
+        if result is not None:
             existing_doc_ids = result.ids[0]
             if existing_doc_ids:
                 log.info(f"Document with hash {metadata['hash']} already exists")
@@ -690,7 +686,7 @@ def save_docs_to_vector_db(
             app.state.sentence_transformer_ef,
             app.state.config.OPENAI_API_KEY,
             app.state.config.OPENAI_API_BASE_URL,
-            app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
+            app.state.config.RAG_EMBEDDING_BATCH_SIZE,
         )
 
         embeddings = embedding_function(
@@ -767,7 +763,7 @@ def process_file(
                 collection_name=f"file-{file.id}", filter={"file_id": file.id}
             )
 
-            if len(result.ids[0]) > 0:
+            if result is not None and len(result.ids[0]) > 0:
                 docs = [
                     Document(
                         page_content=result.documents[0][idx],

+ 45 - 43
backend/open_webui/apps/retrieval/utils.py

@@ -12,8 +12,8 @@ from langchain_core.documents import Document
 
 
 from open_webui.apps.ollama.main import (
-    GenerateEmbeddingsForm,
-    generate_ollama_embeddings,
+    GenerateEmbedForm,
+    generate_ollama_batch_embeddings,
 )
 from open_webui.apps.retrieval.vector.connector import VECTOR_DB_CLIENT
 from open_webui.utils.misc import get_last_user_message
@@ -193,7 +193,8 @@ def query_collection(
                     k=k,
                     query_embedding=query_embedding,
                 )
-                results.append(result.model_dump())
+                if result is not None:
+                    results.append(result.model_dump())
             except Exception as e:
                 log.exception(f"Error when querying the collection: {e}")
         else:
@@ -265,39 +266,27 @@ def get_embedding_function(
     embedding_function,
     openai_key,
     openai_url,
-    batch_size,
+    embedding_batch_size,
 ):
     if embedding_engine == "":
         return lambda query: embedding_function.encode(query).tolist()
     elif embedding_engine in ["ollama", "openai"]:
-        if embedding_engine == "ollama":
-            func = lambda query: generate_ollama_embeddings(
-                GenerateEmbeddingsForm(
-                    **{
-                        "model": embedding_model,
-                        "prompt": query,
-                    }
-                )
-            )
-        elif embedding_engine == "openai":
-            func = lambda query: generate_openai_embeddings(
-                model=embedding_model,
-                text=query,
-                key=openai_key,
-                url=openai_url,
-            )
+        func = lambda query: generate_embeddings(
+            engine=embedding_engine,
+            model=embedding_model,
+            text=query,
+            key=openai_key if embedding_engine == "openai" else "",
+            url=openai_url if embedding_engine == "openai" else "",
+        )
 
-        def generate_multiple(query, f):
+        def generate_multiple(query, func):
             if isinstance(query, list):
-                if embedding_engine == "openai":
-                    embeddings = []
-                    for i in range(0, len(query), batch_size):
-                        embeddings.extend(f(query[i : i + batch_size]))
-                    return embeddings
-                else:
-                    return [f(q) for q in query]
+                embeddings = []
+                for i in range(0, len(query), embedding_batch_size):
+                    embeddings.extend(func(query[i : i + embedding_batch_size]))
+                return embeddings
             else:
-                return f(query)
+                return func(query)
 
         return lambda query: generate_multiple(query, func)
 
@@ -445,20 +434,6 @@ def get_model_path(model: str, update_model: bool = False):
         return model
 
 
-def generate_openai_embeddings(
-    model: str,
-    text: Union[str, list[str]],
-    key: str,
-    url: str = "https://api.openai.com/v1",
-):
-    if isinstance(text, list):
-        embeddings = generate_openai_batch_embeddings(model, text, key, url)
-    else:
-        embeddings = generate_openai_batch_embeddings(model, [text], key, url)
-
-    return embeddings[0] if isinstance(text, str) else embeddings
-
-
 def generate_openai_batch_embeddings(
     model: str, texts: list[str], key: str, url: str = "https://api.openai.com/v1"
 ) -> Optional[list[list[float]]]:
@@ -482,6 +457,33 @@ def generate_openai_batch_embeddings(
         return None
 
 
+def generate_embeddings(engine: str, model: str, text: Union[str, list[str]], **kwargs):
+    if engine == "ollama":
+        if isinstance(text, list):
+            embeddings = generate_ollama_batch_embeddings(
+                GenerateEmbedForm(**{"model": model, "input": text})
+            )
+        else:
+            embeddings = generate_ollama_batch_embeddings(
+                GenerateEmbedForm(**{"model": model, "input": [text]})
+            )
+        return (
+            embeddings["embeddings"][0]
+            if isinstance(text, str)
+            else embeddings["embeddings"]
+        )
+    elif engine == "openai":
+        key = kwargs.get("key", "")
+        url = kwargs.get("url", "https://api.openai.com/v1")
+
+        if isinstance(text, list):
+            embeddings = generate_openai_batch_embeddings(model, text, key, url)
+        else:
+            embeddings = generate_openai_batch_embeddings(model, [text], key, url)
+
+        return embeddings[0] if isinstance(text, str) else embeddings
+
+
 import operator
 from typing import Optional, Sequence
 

+ 4 - 0
backend/open_webui/apps/retrieval/vector/connector.py

@@ -4,6 +4,10 @@ if VECTOR_DB == "milvus":
     from open_webui.apps.retrieval.vector.dbs.milvus import MilvusClient
 
     VECTOR_DB_CLIENT = MilvusClient()
+elif VECTOR_DB == "qdrant":
+    from open_webui.apps.retrieval.vector.dbs.qdrant import QdrantClient
+
+    VECTOR_DB_CLIENT = QdrantClient()
 else:
     from open_webui.apps.retrieval.vector.dbs.chroma import ChromaClient
 

+ 176 - 0
backend/open_webui/apps/retrieval/vector/dbs/qdrant.py

@@ -0,0 +1,176 @@
+from typing import Optional
+
+from qdrant_client import QdrantClient as Qclient
+from qdrant_client.http.models import PointStruct
+from qdrant_client.models import models
+
+from open_webui.apps.retrieval.vector.main import VectorItem, SearchResult, GetResult
+from open_webui.config import QDRANT_URI
+
+NO_LIMIT = 999999999
+
+class QdrantClient:
+    def __init__(self):
+        self.collection_prefix = "open-webui"
+        self.QDRANT_URI = QDRANT_URI
+        self.client = Qclient(url=self.QDRANT_URI) if self.QDRANT_URI else None
+
+    def _result_to_get_result(self, points) -> GetResult:
+        ids = []
+        documents = []
+        metadatas = []
+
+        for point in points:
+            payload = point.payload
+            ids.append(point.id)
+            documents.append(payload["text"])
+            metadatas.append(payload["metadata"])
+
+        return GetResult(
+            **{
+                "ids": [ids],
+                "documents": [documents],
+                "metadatas": [metadatas],
+            }
+        )
+
+    def _create_collection(self, collection_name: str, dimension: int):
+        collection_name_with_prefix = f"{self.collection_prefix}_{collection_name}"
+        self.client.create_collection(
+            collection_name=collection_name_with_prefix,
+            vectors_config=models.VectorParams(size=dimension, distance=models.Distance.COSINE),
+        )
+
+        print(f"collection {collection_name_with_prefix} successfully created!")
+
+    def _create_collection_if_not_exists(self, collection_name, dimension):
+        if not self.has_collection(
+                collection_name=collection_name
+        ):
+            self._create_collection(
+                collection_name=collection_name, dimension=dimension
+            )
+
+    def _create_points(self, items: list[VectorItem]):
+        return [
+            PointStruct(
+                id=item["id"],
+                vector=item["vector"],
+                payload={
+                    "text": item["text"],
+                    "metadata": item["metadata"]
+                },
+            )
+            for item in items
+        ]
+
+    def has_collection(self, collection_name: str) -> bool:
+        return self.client.collection_exists(f"{self.collection_prefix}_{collection_name}")
+
+    def delete_collection(self, collection_name: str):
+        return self.client.delete_collection(collection_name=f"{self.collection_prefix}_{collection_name}")
+
+    def search(
+            self, collection_name: str, vectors: list[list[float | int]], limit: int
+    ) -> Optional[SearchResult]:
+        # Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
+        if limit is None:
+            limit = NO_LIMIT  # otherwise qdrant would set limit to 10!
+
+        query_response = self.client.query_points(
+            collection_name=f"{self.collection_prefix}_{collection_name}",
+            query=vectors[0],
+            limit=limit,
+        )
+        get_result = self._result_to_get_result(query_response.points)
+        return SearchResult(
+            ids=get_result.ids,
+            documents=get_result.documents,
+            metadatas=get_result.metadatas,
+            distances=[[point.score for point in query_response.points]]
+        )
+
+    def query(self, collection_name: str, filter: dict, limit: Optional[int] = None):
+        # Construct the filter string for querying
+        if not self.has_collection(collection_name):
+            return None
+        try:
+            if limit is None:
+                limit = NO_LIMIT  # otherwise qdrant would set limit to 10!
+
+            field_conditions = []
+            for key, value in filter.items():
+                field_conditions.append(
+                    models.FieldCondition(key=f"metadata.{key}", match=models.MatchValue(value=value)))
+
+            points = self.client.query_points(
+                collection_name=f"{self.collection_prefix}_{collection_name}",
+                query_filter=models.Filter(should=field_conditions),
+                limit=limit,
+            )
+            return self._result_to_get_result(points.points)
+        except Exception as e:
+            print(e)
+            return None
+
+    def get(self, collection_name: str) -> Optional[GetResult]:
+        # Get all the items in the collection.
+        points = self.client.query_points(
+            collection_name=f"{self.collection_prefix}_{collection_name}",
+            limit=NO_LIMIT  # otherwise qdrant would set limit to 10!
+        )
+        return self._result_to_get_result(points.points)
+
+    def insert(self, collection_name: str, items: list[VectorItem]):
+        # Insert the items into the collection, if the collection does not exist, it will be created.
+        self._create_collection_if_not_exists(collection_name, len(items[0]["vector"]))
+        points = self._create_points(items)
+        self.client.upload_points(f"{self.collection_prefix}_{collection_name}", points)
+
+    def upsert(self, collection_name: str, items: list[VectorItem]):
+        # Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created.
+        self._create_collection_if_not_exists(collection_name, len(items[0]["vector"]))
+        points = self._create_points(items)
+        return self.client.upsert(f"{self.collection_prefix}_{collection_name}", points)
+
+    def delete(
+            self,
+            collection_name: str,
+            ids: Optional[list[str]] = None,
+            filter: Optional[dict] = None,
+    ):
+        # Delete the items from the collection based on the ids.
+        field_conditions = []
+
+        if ids:
+            for id_value in ids:
+                field_conditions.append(
+                    models.FieldCondition(
+                        key="metadata.id",
+                        match=models.MatchValue(value=id_value),
+                    ),
+                ),
+        elif filter:
+            for key, value in filter.items():
+                field_conditions.append(
+                    models.FieldCondition(
+                        key=f"metadata.{key}",
+                        match=models.MatchValue(value=value),
+                    ),
+                ),
+
+        return self.client.delete(
+            collection_name=f"{self.collection_prefix}_{collection_name}",
+            points_selector=models.FilterSelector(
+                filter=models.Filter(
+                    must=field_conditions
+                )
+            ),
+        )
+
+    def reset(self):
+        # Resets the database. This will delete all collections and item entries.
+        collection_names = self.client.get_collections().collections
+        for collection_name in collection_names:
+            if collection_name.name.startswith(self.collection_prefix):
+                self.client.delete_collection(collection_name=collection_name.name)

+ 248 - 12
backend/open_webui/apps/webui/models/chats.py

@@ -4,8 +4,13 @@ import uuid
 from typing import Optional
 
 from open_webui.apps.webui.internal.db import Base, get_db
+from open_webui.apps.webui.models.tags import TagModel, Tag, Tags
+
+
 from pydantic import BaseModel, ConfigDict
-from sqlalchemy import BigInteger, Boolean, Column, String, Text
+from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
+from sqlalchemy import or_, func, select, and_, text
+from sqlalchemy.sql import exists
 
 ####################
 # Chat DB Schema
@@ -18,13 +23,16 @@ class Chat(Base):
     id = Column(String, primary_key=True)
     user_id = Column(String)
     title = Column(Text)
-    chat = Column(Text)  # Save Chat JSON as Text
+    chat = Column(JSON)
 
     created_at = Column(BigInteger)
     updated_at = Column(BigInteger)
 
     share_id = Column(Text, unique=True, nullable=True)
     archived = Column(Boolean, default=False)
+    pinned = Column(Boolean, default=False, nullable=True)
+
+    meta = Column(JSON, server_default="{}")
 
 
 class ChatModel(BaseModel):
@@ -33,13 +41,16 @@ class ChatModel(BaseModel):
     id: str
     user_id: str
     title: str
-    chat: str
+    chat: dict
 
     created_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
 
     share_id: Optional[str] = None
     archived: bool = False
+    pinned: Optional[bool] = False
+
+    meta: dict = {}
 
 
 ####################
@@ -64,6 +75,8 @@ class ChatResponse(BaseModel):
     created_at: int  # timestamp in epoch
     share_id: Optional[str] = None  # id of the chat to be shared
     archived: bool
+    pinned: Optional[bool] = False
+    meta: dict = {}
 
 
 class ChatTitleIdResponse(BaseModel):
@@ -86,7 +99,7 @@ class ChatTable:
                         if "title" in form_data.chat
                         else "New Chat"
                     ),
-                    "chat": json.dumps(form_data.chat),
+                    "chat": form_data.chat,
                     "created_at": int(time.time()),
                     "updated_at": int(time.time()),
                 }
@@ -101,14 +114,14 @@ class ChatTable:
     def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
         try:
             with get_db() as db:
-                chat_obj = db.get(Chat, id)
-                chat_obj.chat = json.dumps(chat)
-                chat_obj.title = chat["title"] if "title" in chat else "New Chat"
-                chat_obj.updated_at = int(time.time())
+                chat_item = db.get(Chat, id)
+                chat_item.chat = chat
+                chat_item.title = chat["title"] if "title" in chat else "New Chat"
+                chat_item.updated_at = int(time.time())
                 db.commit()
-                db.refresh(chat_obj)
+                db.refresh(chat_item)
 
-                return ChatModel.model_validate(chat_obj)
+                return ChatModel.model_validate(chat_item)
         except Exception:
             return None
 
@@ -182,11 +195,24 @@ class ChatTable:
         except Exception:
             return None
 
+    def toggle_chat_pinned_by_id(self, id: str) -> Optional[ChatModel]:
+        try:
+            with get_db() as db:
+                chat = db.get(Chat, id)
+                chat.pinned = not chat.pinned
+                chat.updated_at = int(time.time())
+                db.commit()
+                db.refresh(chat)
+                return ChatModel.model_validate(chat)
+        except Exception:
+            return None
+
     def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
         try:
             with get_db() as db:
                 chat = db.get(Chat, id)
                 chat.archived = not chat.archived
+                chat.updated_at = int(time.time())
                 db.commit()
                 db.refresh(chat)
                 return ChatModel.model_validate(chat)
@@ -249,10 +275,10 @@ class ChatTable:
                 Chat.id, Chat.title, Chat.updated_at, Chat.created_at
             )
 
-            if limit:
-                query = query.limit(limit)
             if skip:
                 query = query.offset(skip)
+            if limit:
+                query = query.limit(limit)
 
             all_chats = query.all()
 
@@ -328,6 +354,15 @@ class ChatTable:
             )
             return [ChatModel.model_validate(chat) for chat in all_chats]
 
+    def get_pinned_chats_by_user_id(self, user_id: str) -> list[ChatModel]:
+        with get_db() as db:
+            all_chats = (
+                db.query(Chat)
+                .filter_by(user_id=user_id, pinned=True)
+                .order_by(Chat.updated_at.desc())
+            )
+            return [ChatModel.model_validate(chat) for chat in all_chats]
+
     def get_archived_chats_by_user_id(self, user_id: str) -> list[ChatModel]:
         with get_db() as db:
             all_chats = (
@@ -337,6 +372,207 @@ class ChatTable:
             )
             return [ChatModel.model_validate(chat) for chat in all_chats]
 
+    def get_chats_by_user_id_and_search_text(
+        self,
+        user_id: str,
+        search_text: str,
+        include_archived: bool = False,
+        skip: int = 0,
+        limit: int = 60,
+    ) -> list[ChatModel]:
+        """
+        Filters chats based on a search query using Python, allowing pagination using skip and limit.
+        """
+        search_text = search_text.lower().strip()
+        if not search_text:
+            return self.get_chat_list_by_user_id(user_id, include_archived, skip, limit)
+
+        with get_db() as db:
+            query = db.query(Chat).filter(Chat.user_id == user_id)
+
+            if not include_archived:
+                query = query.filter(Chat.archived == False)
+
+            query = query.order_by(Chat.updated_at.desc())
+
+            # Check if the database dialect is either 'sqlite' or 'postgresql'
+            dialect_name = db.bind.dialect.name
+            if dialect_name == "sqlite":
+                # SQLite case: using JSON1 extension for JSON searching
+                query = query.filter(
+                    (
+                        Chat.title.ilike(
+                            f"%{search_text}%"
+                        )  # Case-insensitive search in title
+                        | text(
+                            """
+                            EXISTS (
+                                SELECT 1 
+                                FROM json_each(Chat.chat, '$.messages') AS message 
+                                WHERE LOWER(message.value->>'content') LIKE '%' || :search_text || '%'
+                            )
+                            """
+                        )
+                    ).params(search_text=search_text)
+                )
+            elif dialect_name == "postgresql":
+                # PostgreSQL relies on proper JSON query for search
+                query = query.filter(
+                    (
+                        Chat.title.ilike(
+                            f"%{search_text}%"
+                        )  # Case-insensitive search in title
+                        | text(
+                            """
+                            EXISTS (
+                                SELECT 1
+                                FROM json_array_elements(Chat.chat->'messages') AS message
+                                WHERE LOWER(message->>'content') LIKE '%' || :search_text || '%'
+                            )
+                            """
+                        )
+                    ).params(search_text=search_text)
+                )
+            else:
+                raise NotImplementedError(
+                    f"Unsupported dialect: {db.bind.dialect.name}"
+                )
+
+            # Perform pagination at the SQL level
+            all_chats = query.offset(skip).limit(limit).all()
+
+            # Validate and return chats
+            return [ChatModel.model_validate(chat) for chat in all_chats]
+
+    def get_chat_tags_by_id_and_user_id(self, id: str, user_id: str) -> list[TagModel]:
+        with get_db() as db:
+            chat = db.get(Chat, id)
+            tags = chat.meta.get("tags", [])
+            return [Tags.get_tag_by_name_and_user_id(tag, user_id) for tag in tags]
+
+    def get_chat_list_by_user_id_and_tag_name(
+        self, user_id: str, tag_name: str, skip: int = 0, limit: int = 50
+    ) -> list[ChatModel]:
+        with get_db() as db:
+            query = db.query(Chat).filter_by(user_id=user_id)
+            tag_id = tag_name.replace(" ", "_").lower()
+
+            print(db.bind.dialect.name)
+            if db.bind.dialect.name == "sqlite":
+                # SQLite JSON1 querying for tags within the meta JSON field
+                query = query.filter(
+                    text(
+                        f"EXISTS (SELECT 1 FROM json_each(Chat.meta, '$.tags') WHERE json_each.value = :tag_id)"
+                    )
+                ).params(tag_id=tag_id)
+            elif db.bind.dialect.name == "postgresql":
+                # PostgreSQL JSON query for tags within the meta JSON field (for `json` type)
+                query = query.filter(
+                    text(
+                        "EXISTS (SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') elem WHERE elem = :tag_id)"
+                    )
+                ).params(tag_id=tag_id)
+            else:
+                raise NotImplementedError(
+                    f"Unsupported dialect: {db.bind.dialect.name}"
+                )
+
+            all_chats = query.all()
+            print("all_chats", all_chats)
+            return [ChatModel.model_validate(chat) for chat in all_chats]
+
+    def add_chat_tag_by_id_and_user_id_and_tag_name(
+        self, id: str, user_id: str, tag_name: str
+    ) -> Optional[ChatModel]:
+        tag = Tags.get_tag_by_name_and_user_id(tag_name, user_id)
+        if tag is None:
+            tag = Tags.insert_new_tag(tag_name, user_id)
+        try:
+            with get_db() as db:
+                chat = db.get(Chat, id)
+
+                tag_id = tag.id
+                if tag_id not in chat.meta.get("tags", []):
+                    chat.meta = {
+                        **chat.meta,
+                        "tags": chat.meta.get("tags", []) + [tag_id],
+                    }
+
+                db.commit()
+                db.refresh(chat)
+                return ChatModel.model_validate(chat)
+        except Exception:
+            return None
+
+    def count_chats_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> int:
+        with get_db() as db:  # Assuming `get_db()` returns a session object
+            query = db.query(Chat).filter_by(user_id=user_id)
+
+            # Normalize the tag_name for consistency
+            tag_id = tag_name.replace(" ", "_").lower()
+
+            if db.bind.dialect.name == "sqlite":
+                # SQLite JSON1 support for querying the tags inside the `meta` JSON field
+                query = query.filter(
+                    text(
+                        f"EXISTS (SELECT 1 FROM json_each(Chat.meta, '$.tags') WHERE json_each.value = :tag_id)"
+                    )
+                ).params(tag_id=tag_id)
+
+            elif db.bind.dialect.name == "postgresql":
+                # PostgreSQL JSONB support for querying the tags inside the `meta` JSON field
+                query = query.filter(
+                    text(
+                        "EXISTS (SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') elem WHERE elem = :tag_id)"
+                    )
+                ).params(tag_id=tag_id)
+
+            else:
+                raise NotImplementedError(
+                    f"Unsupported dialect: {db.bind.dialect.name}"
+                )
+
+            # Get the count of matching records
+            count = query.count()
+
+            # Debugging output for inspection
+            print(f"Count of chats for tag '{tag_name}':", count)
+
+            return count
+
+    def delete_tag_by_id_and_user_id_and_tag_name(
+        self, id: str, user_id: str, tag_name: str
+    ) -> bool:
+        try:
+            with get_db() as db:
+                chat = db.get(Chat, id)
+                tags = chat.meta.get("tags", [])
+                tag_id = tag_name.replace(" ", "_").lower()
+
+                tags = [tag for tag in tags if tag != tag_id]
+                chat.meta = {
+                    **chat.meta,
+                    "tags": tags,
+                }
+                db.commit()
+                return True
+        except Exception:
+            return False
+
+    def delete_all_tags_by_id_and_user_id(self, id: str, user_id: str) -> bool:
+        try:
+            with get_db() as db:
+                chat = db.get(Chat, id)
+                chat.meta = {
+                    **chat.meta,
+                    "tags": [],
+                }
+                db.commit()
+
+                return True
+        except Exception:
+            return False
+
     def delete_chat_by_id(self, id: str) -> bool:
         try:
             with get_db() as db:

+ 44 - 1
backend/open_webui/apps/webui/models/files.py

@@ -50,6 +50,14 @@ class FileModel(BaseModel):
 ####################
 
 
+class FileMeta(BaseModel):
+    name: Optional[str] = None
+    content_type: Optional[str] = None
+    size: Optional[int] = None
+
+    model_config = ConfigDict(extra="allow")
+
+
 class FileModelResponse(BaseModel):
     id: str
     user_id: str
@@ -57,8 +65,15 @@ class FileModelResponse(BaseModel):
 
     filename: str
     data: Optional[dict] = None
-    meta: dict
+    meta: FileMeta
+
+    created_at: int  # timestamp in epoch
+    updated_at: int  # timestamp in epoch
+
 
+class FileMetadataResponse(BaseModel):
+    id: str
+    meta: dict
     created_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
 
@@ -104,6 +119,19 @@ class FilesTable:
             except Exception:
                 return None
 
+    def get_file_metadata_by_id(self, id: str) -> Optional[FileMetadataResponse]:
+        with get_db() as db:
+            try:
+                file = db.get(File, id)
+                return FileMetadataResponse(
+                    id=file.id,
+                    meta=file.meta,
+                    created_at=file.created_at,
+                    updated_at=file.updated_at,
+                )
+            except Exception:
+                return None
+
     def get_files(self) -> list[FileModel]:
         with get_db() as db:
             return [FileModel.model_validate(file) for file in db.query(File).all()]
@@ -118,6 +146,21 @@ class FilesTable:
                 .all()
             ]
 
+    def get_file_metadatas_by_ids(self, ids: list[str]) -> list[FileMetadataResponse]:
+        with get_db() as db:
+            return [
+                FileMetadataResponse(
+                    id=file.id,
+                    meta=file.meta,
+                    created_at=file.created_at,
+                    updated_at=file.updated_at,
+                )
+                for file in db.query(File)
+                .filter(File.id.in_(ids))
+                .order_by(File.updated_at.desc())
+                .all()
+            ]
+
     def get_files_by_user_id(self, user_id: str) -> list[FileModel]:
         with get_db() as db:
             return [

+ 6 - 0
backend/open_webui/apps/webui/models/knowledge.py

@@ -6,6 +6,10 @@ import uuid
 
 from open_webui.apps.webui.internal.db import Base, get_db
 from open_webui.env import SRC_LOG_LEVELS
+
+from open_webui.apps.webui.models.files import FileMetadataResponse
+
+
 from pydantic import BaseModel, ConfigDict
 from sqlalchemy import BigInteger, Column, String, Text, JSON
 
@@ -64,6 +68,8 @@ class KnowledgeResponse(BaseModel):
     created_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
 
+    files: Optional[list[FileMetadataResponse | dict]] = None
+
 
 class KnowledgeForm(BaseModel):
     name: str

+ 17 - 178
backend/open_webui/apps/webui/models/tags.py

@@ -4,53 +4,32 @@ import uuid
 from typing import Optional
 
 from open_webui.apps.webui.internal.db import Base, get_db
+
+
 from open_webui.env import SRC_LOG_LEVELS
 from pydantic import BaseModel, ConfigDict
-from sqlalchemy import BigInteger, Column, String, Text
+from sqlalchemy import BigInteger, Column, String, JSON
 
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["MODELS"])
 
+
 ####################
 # Tag DB Schema
 ####################
-
-
 class Tag(Base):
     __tablename__ = "tag"
-
     id = Column(String, primary_key=True)
     name = Column(String)
     user_id = Column(String)
-    data = Column(Text, nullable=True)
-
-
-class ChatIdTag(Base):
-    __tablename__ = "chatidtag"
-
-    id = Column(String, primary_key=True)
-    tag_name = Column(String)
-    chat_id = Column(String)
-    user_id = Column(String)
-    timestamp = Column(BigInteger)
+    meta = Column(JSON, nullable=True)
 
 
 class TagModel(BaseModel):
     id: str
     name: str
     user_id: str
-    data: Optional[str] = None
-
-    model_config = ConfigDict(from_attributes=True)
-
-
-class ChatIdTagModel(BaseModel):
-    id: str
-    tag_name: str
-    chat_id: str
-    user_id: str
-    timestamp: int
-
+    meta: Optional[dict] = None
     model_config = ConfigDict(from_attributes=True)
 
 
@@ -59,23 +38,15 @@ class ChatIdTagModel(BaseModel):
 ####################
 
 
-class ChatIdTagForm(BaseModel):
-    tag_name: str
+class TagChatIdForm(BaseModel):
+    name: str
     chat_id: str
 
 
-class TagChatIdsResponse(BaseModel):
-    chat_ids: list[str]
-
-
-class ChatTagsResponse(BaseModel):
-    tags: list[str]
-
-
 class TagTable:
     def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]:
         with get_db() as db:
-            id = str(uuid.uuid4())
+            id = name.replace(" ", "_").lower()
             tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
             try:
                 result = Tag(**tag.model_dump())
@@ -93,170 +64,38 @@ class TagTable:
         self, name: str, user_id: str
     ) -> Optional[TagModel]:
         try:
+            id = name.replace(" ", "_").lower()
             with get_db() as db:
-                tag = db.query(Tag).filter_by(name=name, user_id=user_id).first()
+                tag = db.query(Tag).filter_by(id=id, user_id=user_id).first()
                 return TagModel.model_validate(tag)
         except Exception:
             return None
 
-    def add_tag_to_chat(
-        self, user_id: str, form_data: ChatIdTagForm
-    ) -> Optional[ChatIdTagModel]:
-        tag = self.get_tag_by_name_and_user_id(form_data.tag_name, user_id)
-        if tag is None:
-            tag = self.insert_new_tag(form_data.tag_name, user_id)
-
-        id = str(uuid.uuid4())
-        chatIdTag = ChatIdTagModel(
-            **{
-                "id": id,
-                "user_id": user_id,
-                "chat_id": form_data.chat_id,
-                "tag_name": tag.name,
-                "timestamp": int(time.time()),
-            }
-        )
-        try:
-            with get_db() as db:
-                result = ChatIdTag(**chatIdTag.model_dump())
-                db.add(result)
-                db.commit()
-                db.refresh(result)
-                if result:
-                    return ChatIdTagModel.model_validate(result)
-                else:
-                    return None
-        except Exception:
-            return None
-
     def get_tags_by_user_id(self, user_id: str) -> list[TagModel]:
         with get_db() as db:
-            tag_names = [
-                chat_id_tag.tag_name
-                for chat_id_tag in (
-                    db.query(ChatIdTag)
-                    .filter_by(user_id=user_id)
-                    .order_by(ChatIdTag.timestamp.desc())
-                    .all()
-                )
-            ]
-
             return [
                 TagModel.model_validate(tag)
-                for tag in (
-                    db.query(Tag)
-                    .filter_by(user_id=user_id)
-                    .filter(Tag.name.in_(tag_names))
-                    .all()
-                )
+                for tag in (db.query(Tag).filter_by(user_id=user_id).all())
             ]
 
-    def get_tags_by_chat_id_and_user_id(
-        self, chat_id: str, user_id: str
-    ) -> list[TagModel]:
+    def get_tags_by_ids(self, ids: list[str]) -> list[TagModel]:
         with get_db() as db:
-            tag_names = [
-                chat_id_tag.tag_name
-                for chat_id_tag in (
-                    db.query(ChatIdTag)
-                    .filter_by(user_id=user_id, chat_id=chat_id)
-                    .order_by(ChatIdTag.timestamp.desc())
-                    .all()
-                )
-            ]
-
             return [
                 TagModel.model_validate(tag)
-                for tag in (
-                    db.query(Tag)
-                    .filter_by(user_id=user_id)
-                    .filter(Tag.name.in_(tag_names))
-                    .all()
-                )
+                for tag in (db.query(Tag).filter(Tag.id.in_(ids)).all())
             ]
 
-    def get_chat_ids_by_tag_name_and_user_id(
-        self, tag_name: str, user_id: str
-    ) -> list[ChatIdTagModel]:
-        with get_db() as db:
-            return [
-                ChatIdTagModel.model_validate(chat_id_tag)
-                for chat_id_tag in (
-                    db.query(ChatIdTag)
-                    .filter_by(user_id=user_id, tag_name=tag_name)
-                    .order_by(ChatIdTag.timestamp.desc())
-                    .all()
-                )
-            ]
-
-    def count_chat_ids_by_tag_name_and_user_id(
-        self, tag_name: str, user_id: str
-    ) -> int:
-        with get_db() as db:
-            return (
-                db.query(ChatIdTag)
-                .filter_by(tag_name=tag_name, user_id=user_id)
-                .count()
-            )
-
-    def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool:
+    def delete_tag_by_name_and_user_id(self, name: str, user_id: str) -> bool:
         try:
             with get_db() as db:
-                res = (
-                    db.query(ChatIdTag)
-                    .filter_by(tag_name=tag_name, user_id=user_id)
-                    .delete()
-                )
+                id = name.replace(" ", "_").lower()
+                res = db.query(Tag).filter_by(id=id, user_id=user_id).delete()
                 log.debug(f"res: {res}")
                 db.commit()
-
-                tag_count = self.count_chat_ids_by_tag_name_and_user_id(
-                    tag_name, user_id
-                )
-                if tag_count == 0:
-                    # Remove tag item from Tag col as well
-                    db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
-                    db.commit()
                 return True
         except Exception as e:
             log.error(f"delete_tag: {e}")
             return False
 
-    def delete_tag_by_tag_name_and_chat_id_and_user_id(
-        self, tag_name: str, chat_id: str, user_id: str
-    ) -> bool:
-        try:
-            with get_db() as db:
-                res = (
-                    db.query(ChatIdTag)
-                    .filter_by(tag_name=tag_name, chat_id=chat_id, user_id=user_id)
-                    .delete()
-                )
-                log.debug(f"res: {res}")
-                db.commit()
-
-                tag_count = self.count_chat_ids_by_tag_name_and_user_id(
-                    tag_name, user_id
-                )
-                if tag_count == 0:
-                    # Remove tag item from Tag col as well
-                    db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
-                    db.commit()
-
-                return True
-        except Exception as e:
-            log.error(f"delete_tag: {e}")
-            return False
-
-    def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool:
-        tags = self.get_tags_by_chat_id_and_user_id(chat_id, user_id)
-
-        for tag in tags:
-            self.delete_tag_by_tag_name_and_chat_id_and_user_id(
-                tag.tag_name, chat_id, user_id
-            )
-
-        return True
-
 
 Tags = TagTable()

+ 10 - 1
backend/open_webui/apps/webui/routers/auths.py

@@ -18,6 +18,8 @@ from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
 from open_webui.env import (
     WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
     WEBUI_AUTH_TRUSTED_NAME_HEADER,
+    WEBUI_SESSION_COOKIE_SAME_SITE,
+    WEBUI_SESSION_COOKIE_SECURE,
 )
 from fastapi import APIRouter, Depends, HTTPException, Request, status
 from fastapi.responses import Response
@@ -27,6 +29,7 @@ from open_webui.utils.utils import (
     create_api_key,
     create_token,
     get_admin_user,
+    get_verified_user,
     get_current_user,
     get_password_hash,
 )
@@ -53,6 +56,8 @@ async def get_session_user(
         key="token",
         value=token,
         httponly=True,  # Ensures the cookie is not accessible via JavaScript
+        samesite=WEBUI_SESSION_COOKIE_SAME_SITE, 
+        secure=WEBUI_SESSION_COOKIE_SECURE,        
     )
 
     return {
@@ -71,7 +76,7 @@ async def get_session_user(
 
 @router.post("/update/profile", response_model=UserResponse)
 async def update_profile(
-    form_data: UpdateProfileForm, session_user=Depends(get_current_user)
+    form_data: UpdateProfileForm, session_user=Depends(get_verified_user)
 ):
     if session_user:
         user = Users.update_user_by_id(
@@ -166,6 +171,8 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
             key="token",
             value=token,
             httponly=True,  # Ensures the cookie is not accessible via JavaScript
+            samesite=WEBUI_SESSION_COOKIE_SAME_SITE, 
+            secure=WEBUI_SESSION_COOKIE_SECURE,            
         )
 
         return {
@@ -236,6 +243,8 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
                 key="token",
                 value=token,
                 httponly=True,  # Ensures the cookie is not accessible via JavaScript
+                samesite=WEBUI_SESSION_COOKIE_SAME_SITE, 
+                secure=WEBUI_SESSION_COOKIE_SECURE,                
             )
 
             if request.app.state.config.WEBHOOK_URL:

+ 156 - 88
backend/open_webui/apps/webui/routers/chats.py

@@ -8,12 +8,8 @@ from open_webui.apps.webui.models.chats import (
     Chats,
     ChatTitleIdResponse,
 )
-from open_webui.apps.webui.models.tags import (
-    ChatIdTagForm,
-    ChatIdTagModel,
-    TagModel,
-    Tags,
-)
+from open_webui.apps.webui.models.tags import TagModel, Tags
+
 from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.env import SRC_LOG_LEVELS
@@ -95,7 +91,7 @@ async def get_user_chat_list_by_user_id(
 async def create_new_chat(form_data: ChatForm, user=Depends(get_verified_user)):
     try:
         chat = Chats.insert_new_chat(user.id, form_data)
-        return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
+        return ChatResponse(**chat.model_dump())
     except Exception as e:
         log.exception(e)
         raise HTTPException(
@@ -108,10 +104,46 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_verified_user)):
 ############################
 
 
+@router.get("/search", response_model=list[ChatTitleIdResponse])
+async def search_user_chats(
+    text: str, page: Optional[int] = None, user=Depends(get_verified_user)
+):
+    if page is None:
+        page = 1
+
+    limit = 60
+    skip = (page - 1) * limit
+
+    return [
+        ChatTitleIdResponse(**chat.model_dump())
+        for chat in Chats.get_chats_by_user_id_and_search_text(
+            user.id, text, skip=skip, limit=limit
+        )
+    ]
+
+
+############################
+# GetPinnedChats
+############################
+
+
+@router.get("/pinned", response_model=list[ChatResponse])
+async def get_user_pinned_chats(user=Depends(get_verified_user)):
+    return [
+        ChatResponse(**chat.model_dump())
+        for chat in Chats.get_pinned_chats_by_user_id(user.id)
+    ]
+
+
+############################
+# GetChats
+############################
+
+
 @router.get("/all", response_model=list[ChatResponse])
 async def get_user_chats(user=Depends(get_verified_user)):
     return [
-        ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
+        ChatResponse(**chat.model_dump())
         for chat in Chats.get_chats_by_user_id(user.id)
     ]
 
@@ -124,11 +156,28 @@ async def get_user_chats(user=Depends(get_verified_user)):
 @router.get("/all/archived", response_model=list[ChatResponse])
 async def get_user_archived_chats(user=Depends(get_verified_user)):
     return [
-        ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
+        ChatResponse(**chat.model_dump())
         for chat in Chats.get_archived_chats_by_user_id(user.id)
     ]
 
 
+############################
+# GetAllTags
+############################
+
+
+@router.get("/all/tags", response_model=list[TagModel])
+async def get_all_user_tags(user=Depends(get_verified_user)):
+    try:
+        tags = Tags.get_tags_by_user_id(user.id)
+        return tags
+    except Exception as e:
+        log.exception(e)
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+        )
+
+
 ############################
 # GetAllChatsInDB
 ############################
@@ -141,10 +190,7 @@ async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
         )
-    return [
-        ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
-        for chat in Chats.get_chats()
-    ]
+    return [ChatResponse(**chat.model_dump()) for chat in Chats.get_chats()]
 
 
 ############################
@@ -187,7 +233,8 @@ async def get_shared_chat_by_id(share_id: str, user=Depends(get_verified_user)):
         chat = Chats.get_chat_by_id(share_id)
 
     if chat:
-        return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
+        return ChatResponse(**chat.model_dump())
+
     else:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
@@ -199,48 +246,28 @@ async def get_shared_chat_by_id(share_id: str, user=Depends(get_verified_user)):
 ############################
 
 
-class TagNameForm(BaseModel):
+class TagForm(BaseModel):
     name: str
+
+
+class TagFilterForm(TagForm):
     skip: Optional[int] = 0
     limit: Optional[int] = 50
 
 
 @router.post("/tags", response_model=list[ChatTitleIdResponse])
 async def get_user_chat_list_by_tag_name(
-    form_data: TagNameForm, user=Depends(get_verified_user)
+    form_data: TagFilterForm, user=Depends(get_verified_user)
 ):
-    chat_ids = [
-        chat_id_tag.chat_id
-        for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(
-            form_data.name, user.id
-        )
-    ]
-
-    chats = Chats.get_chat_list_by_chat_ids(chat_ids, form_data.skip, form_data.limit)
-
+    chats = Chats.get_chat_list_by_user_id_and_tag_name(
+        user.id, form_data.name, form_data.skip, form_data.limit
+    )
     if len(chats) == 0:
-        Tags.delete_tag_by_tag_name_and_user_id(form_data.name, user.id)
+        Tags.delete_tag_by_name_and_user_id(form_data.name, user.id)
 
     return chats
 
 
-############################
-# GetAllTags
-############################
-
-
-@router.get("/tags/all", response_model=list[TagModel])
-async def get_all_tags(user=Depends(get_verified_user)):
-    try:
-        tags = Tags.get_tags_by_user_id(user.id)
-        return tags
-    except Exception as e:
-        log.exception(e)
-        raise HTTPException(
-            status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
-        )
-
-
 ############################
 # GetChatById
 ############################
@@ -251,7 +278,8 @@ async def get_chat_by_id(id: str, user=Depends(get_verified_user)):
     chat = Chats.get_chat_by_id_and_user_id(id, user.id)
 
     if chat:
-        return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
+        return ChatResponse(**chat.model_dump())
+
     else:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
@@ -269,10 +297,9 @@ async def update_chat_by_id(
 ):
     chat = Chats.get_chat_by_id_and_user_id(id, user.id)
     if chat:
-        updated_chat = {**json.loads(chat.chat), **form_data.chat}
-
+        updated_chat = {**chat.chat, **form_data.chat}
         chat = Chats.update_chat_by_id(id, updated_chat)
-        return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
+        return ChatResponse(**chat.model_dump())
     else:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
@@ -303,25 +330,57 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_verified
         return result
 
 
+############################
+# GetPinnedStatusById
+############################
+
+
+@router.get("/{id}/pinned", response_model=Optional[bool])
+async def get_pinned_status_by_id(id: str, user=Depends(get_verified_user)):
+    chat = Chats.get_chat_by_id_and_user_id(id, user.id)
+    if chat:
+        return chat.pinned
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
+        )
+
+
+############################
+# PinChatById
+############################
+
+
+@router.post("/{id}/pin", response_model=Optional[ChatResponse])
+async def pin_chat_by_id(id: str, user=Depends(get_verified_user)):
+    chat = Chats.get_chat_by_id_and_user_id(id, user.id)
+    if chat:
+        chat = Chats.toggle_chat_pinned_by_id(id)
+        return chat
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
+        )
+
+
 ############################
 # CloneChat
 ############################
 
 
-@router.get("/{id}/clone", response_model=Optional[ChatResponse])
+@router.post("/{id}/clone", response_model=Optional[ChatResponse])
 async def clone_chat_by_id(id: str, user=Depends(get_verified_user)):
     chat = Chats.get_chat_by_id_and_user_id(id, user.id)
     if chat:
-        chat_body = json.loads(chat.chat)
         updated_chat = {
-            **chat_body,
+            **chat.chat,
             "originalChatId": chat.id,
-            "branchPointMessageId": chat_body["history"]["currentId"],
+            "branchPointMessageId": chat.chat["history"]["currentId"],
             "title": f"Clone of {chat.title}",
         }
 
         chat = Chats.insert_new_chat(user.id, ChatForm(**{"chat": updated_chat}))
-        return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
+        return ChatResponse(**chat.model_dump())
     else:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
@@ -333,12 +392,12 @@ async def clone_chat_by_id(id: str, user=Depends(get_verified_user)):
 ############################
 
 
-@router.get("/{id}/archive", response_model=Optional[ChatResponse])
+@router.post("/{id}/archive", response_model=Optional[ChatResponse])
 async def archive_chat_by_id(id: str, user=Depends(get_verified_user)):
     chat = Chats.get_chat_by_id_and_user_id(id, user.id)
     if chat:
         chat = Chats.toggle_chat_archive_by_id(id)
-        return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
+        return ChatResponse(**chat.model_dump())
     else:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
@@ -356,9 +415,7 @@ async def share_chat_by_id(id: str, user=Depends(get_verified_user)):
     if chat:
         if chat.share_id:
             shared_chat = Chats.update_shared_chat_by_chat_id(chat.id)
-            return ChatResponse(
-                **{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
-            )
+            return ChatResponse(**shared_chat.model_dump())
 
         shared_chat = Chats.insert_shared_chat_by_chat_id(chat.id)
         if not shared_chat:
@@ -366,10 +423,8 @@ async def share_chat_by_id(id: str, user=Depends(get_verified_user)):
                 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                 detail=ERROR_MESSAGES.DEFAULT(),
             )
+        return ChatResponse(**shared_chat.model_dump())
 
-        return ChatResponse(
-            **{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
-        )
     else:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
@@ -407,10 +462,10 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
 
 @router.get("/{id}/tags", response_model=list[TagModel])
 async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user)):
-    tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id)
-
-    if tags != None:
-        return tags
+    chat = Chats.get_chat_by_id_and_user_id(id, user.id)
+    if chat:
+        tags = chat.meta.get("tags", [])
+        return Tags.get_tags_by_ids(tags)
     else:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
@@ -422,22 +477,24 @@ async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user)):
 ############################
 
 
-@router.post("/{id}/tags", response_model=Optional[ChatIdTagModel])
-async def add_chat_tag_by_id(
-    id: str, form_data: ChatIdTagForm, user=Depends(get_verified_user)
+@router.post("/{id}/tags", response_model=list[TagModel])
+async def add_tag_by_id_and_tag_name(
+    id: str, form_data: TagForm, user=Depends(get_verified_user)
 ):
-    tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id)
-
-    if form_data.tag_name not in tags:
-        tag = Tags.add_tag_to_chat(user.id, form_data)
+    chat = Chats.get_chat_by_id_and_user_id(id, user.id)
+    if chat:
+        tags = chat.meta.get("tags", [])
+        tag_id = form_data.name.replace(" ", "_").lower()
 
-        if tag:
-            return tag
-        else:
-            raise HTTPException(
-                status_code=status.HTTP_401_UNAUTHORIZED,
-                detail=ERROR_MESSAGES.NOT_FOUND,
+        print(tags, tag_id)
+        if tag_id not in tags:
+            Chats.add_chat_tag_by_id_and_user_id_and_tag_name(
+                id, user.id, form_data.name
             )
+
+        chat = Chats.get_chat_by_id_and_user_id(id, user.id)
+        tags = chat.meta.get("tags", [])
+        return Tags.get_tags_by_ids(tags)
     else:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
@@ -449,16 +506,20 @@ async def add_chat_tag_by_id(
 ############################
 
 
-@router.delete("/{id}/tags", response_model=Optional[bool])
-async def delete_chat_tag_by_id(
-    id: str, form_data: ChatIdTagForm, user=Depends(get_verified_user)
+@router.delete("/{id}/tags", response_model=list[TagModel])
+async def delete_tag_by_id_and_tag_name(
+    id: str, form_data: TagForm, user=Depends(get_verified_user)
 ):
-    result = Tags.delete_tag_by_tag_name_and_chat_id_and_user_id(
-        form_data.tag_name, id, user.id
-    )
+    chat = Chats.get_chat_by_id_and_user_id(id, user.id)
+    if chat:
+        Chats.delete_tag_by_id_and_user_id_and_tag_name(id, user.id, form_data.name)
 
-    if result:
-        return result
+        if Chats.count_chats_by_tag_name_and_user_id(form_data.name, user.id) == 0:
+            Tags.delete_tag_by_name_and_user_id(form_data.name, user.id)
+
+        chat = Chats.get_chat_by_id_and_user_id(id, user.id)
+        tags = chat.meta.get("tags", [])
+        return Tags.get_tags_by_ids(tags)
     else:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
@@ -472,10 +533,17 @@ async def delete_chat_tag_by_id(
 
 @router.delete("/{id}/tags/all", response_model=Optional[bool])
 async def delete_all_chat_tags_by_id(id: str, user=Depends(get_verified_user)):
-    result = Tags.delete_tags_by_chat_id_and_user_id(id, user.id)
+    chat = Chats.get_chat_by_id_and_user_id(id, user.id)
+    if chat:
+        Chats.delete_all_tags_by_id_and_user_id(id, user.id)
 
-    if result:
-        return result
+        for tag in chat.meta.get("tags", []):
+            if Chats.count_chats_by_tag_name_and_user_id(tag, user.id) == 0:
+                Tags.delete_tag_by_name_and_user_id(tag, user.id)
+
+        chat = Chats.get_chat_by_id_and_user_id(id, user.id)
+        tags = chat.meta.get("tags", [])
+        return Tags.get_tags_by_ids(tags)
     else:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND

+ 10 - 4
backend/open_webui/apps/webui/routers/files.py

@@ -213,7 +213,7 @@ async def update_file_data_content_by_id(
 ############################
 
 
-@router.get("/{id}/content", response_model=Optional[FileModel])
+@router.get("/{id}/content")
 async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
     file = Files.get_file_by_id(id)
 
@@ -223,7 +223,10 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
         # Check if the file already exists in the cache
         if file_path.is_file():
             print(f"file_path: {file_path}")
-            return FileResponse(file_path)
+            headers = {
+                "Content-Disposition": f'attachment; filename="{file.meta.get("name", file.filename)}"'
+            }
+            return FileResponse(file_path, headers=headers)
         else:
             raise HTTPException(
                 status_code=status.HTTP_404_NOT_FOUND,
@@ -236,7 +239,7 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
         )
 
 
-@router.get("/{id}/content/{file_name}", response_model=Optional[FileModel])
+@router.get("/{id}/content/{file_name}")
 async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
     file = Files.get_file_by_id(id)
 
@@ -248,7 +251,10 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
             # Check if the file already exists in the cache
             if file_path.is_file():
                 print(f"file_path: {file_path}")
-                return FileResponse(file_path)
+                headers = {
+                    "Content-Disposition": f'attachment; filename="{file.meta.get("name", file.filename)}"'
+                }
+                return FileResponse(file_path, headers=headers)
             else:
                 raise HTTPException(
                     status_code=status.HTTP_404_NOT_FOUND,

+ 6 - 1
backend/open_webui/apps/webui/routers/knowledge.py

@@ -48,7 +48,12 @@ async def get_knowledge_items(
             )
     else:
         return [
-            KnowledgeResponse(**knowledge.model_dump())
+            KnowledgeResponse(
+                **knowledge.model_dump(),
+                files=Files.get_file_metadatas_by_ids(
+                    knowledge.data.get("file_ids", []) if knowledge.data else []
+                ),
+            )
             for knowledge in Knowledges.get_knowledge_items()
         ]
 

+ 10 - 4
backend/open_webui/config.py

@@ -901,6 +901,9 @@ CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
 
 MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
 
+# Qdrant
+QDRANT_URI = os.environ.get("QDRANT_URI", None)
+
 ####################################
 # Information Retrieval (RAG)
 ####################################
@@ -986,10 +989,13 @@ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
     os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
 )
 
-RAG_EMBEDDING_OPENAI_BATCH_SIZE = PersistentConfig(
-    "RAG_EMBEDDING_OPENAI_BATCH_SIZE",
-    "rag.embedding_openai_batch_size",
-    int(os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", "1")),
+RAG_EMBEDDING_BATCH_SIZE = PersistentConfig(
+    "RAG_EMBEDDING_BATCH_SIZE",
+    "rag.embedding_batch_size",
+    int(
+        os.environ.get("RAG_EMBEDDING_BATCH_SIZE")
+        or os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", "1")
+    ),
 )
 
 RAG_RERANKING_MODEL = PersistentConfig(

+ 13 - 2
backend/open_webui/env.py

@@ -302,6 +302,12 @@ RESET_CONFIG_ON_START = (
     os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
 )
 
+####################################
+# REDIS
+####################################
+
+REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
+
 ####################################
 # WEBUI_AUTH (Required for security)
 ####################################
@@ -343,8 +349,7 @@ ENABLE_WEBSOCKET_SUPPORT = (
 
 WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
 
-WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", "redis://localhost:6379/0")
-
+WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
 
 AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
 
@@ -355,3 +360,9 @@ else:
         AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
     except Exception:
         AIOHTTP_CLIENT_TIMEOUT = 300
+
+####################################
+# OFFLINE_MODE
+####################################
+
+OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"

+ 40 - 5
backend/open_webui/main.py

@@ -102,6 +102,7 @@ from open_webui.env import (
     WEBUI_SESSION_COOKIE_SECURE,
     WEBUI_URL,
     RESET_CONFIG_ON_START,
+    OFFLINE_MODE,
 )
 from fastapi import (
     Depends,
@@ -178,14 +179,14 @@ class SPAStaticFiles(StaticFiles):
 
 print(
     rf"""
-  ___                    __        __   _     _   _ ___ 
+  ___                    __        __   _     _   _ ___
  / _ \ _ __   ___ _ __   \ \      / /__| |__ | | | |_ _|
-| | | | '_ \ / _ \ '_ \   \ \ /\ / / _ \ '_ \| | | || | 
-| |_| | |_) |  __/ | | |   \ V  V /  __/ |_) | |_| || | 
+| | | | '_ \ / _ \ '_ \   \ \ /\ / / _ \ '_ \| | | || |
+| |_| | |_) |  __/ | | |   \ V  V /  __/ |_) | |_| || |
  \___/| .__/ \___|_| |_|    \_/\_/ \___|_.__/ \___/|___|
-      |_|                                               
+      |_|
+
 
-      
 v{VERSION} - building the best open-source AI user interface.
 {f"Commit: {WEBUI_BUILD_HASH}" if WEBUI_BUILD_HASH != "dev-build" else ""}
 https://github.com/open-webui/open-webui
@@ -824,6 +825,32 @@ class PipelineMiddleware(BaseHTTPMiddleware):
 app.add_middleware(PipelineMiddleware)
 
 
+from urllib.parse import urlencode, parse_qs, urlparse
+
+
+class RedirectMiddleware(BaseHTTPMiddleware):
+    async def dispatch(self, request: Request, call_next):
+        # Check if the request is a GET request
+        if request.method == "GET":
+            path = request.url.path
+            query_params = dict(parse_qs(urlparse(str(request.url)).query))
+
+            # Check for the specific watch path and the presence of 'v' parameter
+            if path.endswith("/watch") and "v" in query_params:
+                video_id = query_params["v"][0]  # Extract the first 'v' parameter
+                encoded_video_id = urlencode({"youtube": video_id})
+                redirect_url = f"/?{encoded_video_id}"
+                return RedirectResponse(url=redirect_url)
+
+        # Proceed with the normal flow of other requests
+        response = await call_next(request)
+        return response
+
+
+# Add the middleware to the app
+app.add_middleware(RedirectMiddleware)
+
+
 app.add_middleware(
     CORSMiddleware,
     allow_origins=CORS_ALLOW_ORIGIN,
@@ -2181,6 +2208,11 @@ async def get_app_changelog():
 
 @app.get("/api/version/updates")
 async def get_app_latest_release_version():
+    if OFFLINE_MODE:
+        log.debug(
+            f"Offline mode is enabled, returning current version as latest version"
+        )
+        return {"current": VERSION, "latest": VERSION}
     try:
         timeout = aiohttp.ClientTimeout(total=1)
         async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
@@ -2353,6 +2385,8 @@ async def oauth_callback(provider: str, request: Request, response: Response):
         key="token",
         value=jwt_token,
         httponly=True,  # Ensures the cookie is not accessible via JavaScript
+        samesite=WEBUI_SESSION_COOKIE_SAME_SITE, 
+        secure=WEBUI_SESSION_COOKIE_SECURE,
     )
 
     # Redirect back to the frontend with the JWT token
@@ -2416,6 +2450,7 @@ async def healthcheck_with_db():
 app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
 app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")
 
+
 if os.path.exists(FRONTEND_BUILD_DIR):
     mimetypes.add_type("text/javascript", ".js")
     app.mount(

+ 151 - 0
backend/open_webui/migrations/versions/1af9b942657b_migrate_tags.py

@@ -0,0 +1,151 @@
+"""Migrate tags
+
+Revision ID: 1af9b942657b
+Revises: 242a2047eae0
+Create Date: 2024-10-09 21:02:35.241684
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.sql import table, select, update, column
+from sqlalchemy.engine.reflection import Inspector
+
+import json
+
+revision = "1af9b942657b"
+down_revision = "242a2047eae0"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # Setup an inspection on the existing table to avoid issues
+    conn = op.get_bind()
+    inspector = Inspector.from_engine(conn)
+
+    # Clean up potential leftover temp table from previous failures
+    conn.execute(sa.text("DROP TABLE IF EXISTS _alembic_tmp_tag"))
+
+    # Check if the 'tag' table exists
+    tables = inspector.get_table_names()
+
+    # Step 1: Modify Tag table using batch mode for SQLite support
+    if "tag" in tables:
+        # Get the current columns in the 'tag' table
+        columns = [col["name"] for col in inspector.get_columns("tag")]
+
+        # Get any existing unique constraints on the 'tag' table
+        current_constraints = inspector.get_unique_constraints("tag")
+
+        with op.batch_alter_table("tag", schema=None) as batch_op:
+            # Check if the unique constraint already exists
+            if not any(
+                constraint["name"] == "uq_id_user_id"
+                for constraint in current_constraints
+            ):
+                # Create unique constraint if it doesn't exist
+                batch_op.create_unique_constraint("uq_id_user_id", ["id", "user_id"])
+
+            # Check if the 'data' column exists before trying to drop it
+            if "data" in columns:
+                batch_op.drop_column("data")
+
+            # Check if the 'meta' column needs to be created
+            if "meta" not in columns:
+                # Add the 'meta' column if it doesn't already exist
+                batch_op.add_column(sa.Column("meta", sa.JSON(), nullable=True))
+
+    tag = table(
+        "tag",
+        column("id", sa.String()),
+        column("name", sa.String()),
+        column("user_id", sa.String()),
+        column("meta", sa.JSON()),
+    )
+
+    # Step 2: Migrate tags
+    conn = op.get_bind()
+    result = conn.execute(sa.select(tag.c.id, tag.c.name, tag.c.user_id))
+
+    tag_updates = {}
+    for row in result:
+        new_id = row.name.replace(" ", "_").lower()
+        tag_updates[row.id] = new_id
+
+    for tag_id, new_tag_id in tag_updates.items():
+        print(f"Updating tag {tag_id} to {new_tag_id}")
+        if new_tag_id == "pinned":
+            # delete tag
+            delete_stmt = sa.delete(tag).where(tag.c.id == tag_id)
+            conn.execute(delete_stmt)
+        else:
+            # Check if the new_tag_id already exists in the database
+            existing_tag_query = sa.select(tag.c.id).where(tag.c.id == new_tag_id)
+            existing_tag_result = conn.execute(existing_tag_query).fetchone()
+
+            if existing_tag_result:
+                # Handle duplicate case: the new_tag_id already exists
+                print(
+                    f"Tag {new_tag_id} already exists. Removing current tag with ID {tag_id} to avoid duplicates."
+                )
+                # Option 1: Delete the current tag if an update to new_tag_id would cause duplication
+                delete_stmt = sa.delete(tag).where(tag.c.id == tag_id)
+                conn.execute(delete_stmt)
+            else:
+                update_stmt = sa.update(tag).where(tag.c.id == tag_id)
+                update_stmt = update_stmt.values(id=new_tag_id)
+                conn.execute(update_stmt)
+
+    # Add columns `pinned` and `meta` to 'chat'
+    op.add_column("chat", sa.Column("pinned", sa.Boolean(), nullable=True))
+    op.add_column(
+        "chat", sa.Column("meta", sa.JSON(), nullable=False, server_default="{}")
+    )
+
+    chatidtag = table(
+        "chatidtag", column("chat_id", sa.String()), column("tag_name", sa.String())
+    )
+    chat = table(
+        "chat",
+        column("id", sa.String()),
+        column("pinned", sa.Boolean()),
+        column("meta", sa.JSON()),
+    )
+
+    # Fetch existing tags
+    conn = op.get_bind()
+    result = conn.execute(sa.select(chatidtag.c.chat_id, chatidtag.c.tag_name))
+
+    chat_updates = {}
+    for row in result:
+        chat_id = row.chat_id
+        tag_name = row.tag_name.replace(" ", "_").lower()
+
+        if tag_name == "pinned":
+            # Specifically handle 'pinned' tag
+            if chat_id not in chat_updates:
+                chat_updates[chat_id] = {"pinned": True, "meta": {}}
+            else:
+                chat_updates[chat_id]["pinned"] = True
+        else:
+            if chat_id not in chat_updates:
+                chat_updates[chat_id] = {"pinned": False, "meta": {"tags": [tag_name]}}
+            else:
+                tags = chat_updates[chat_id]["meta"].get("tags", [])
+                tags.append(tag_name)
+
+                chat_updates[chat_id]["meta"]["tags"] = tags
+
+    # Update chats based on accumulated changes
+    for chat_id, updates in chat_updates.items():
+        update_stmt = sa.update(chat).where(chat.c.id == chat_id)
+        update_stmt = update_stmt.values(
+            meta=updates.get("meta", {}), pinned=updates.get("pinned", False)
+        )
+        conn.execute(update_stmt)
+    pass
+
+
+def downgrade():
+    pass

+ 82 - 0
backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py

@@ -0,0 +1,82 @@
+"""Update chat table
+
+Revision ID: 242a2047eae0
+Revises: 6a39f3d8e55c
+Create Date: 2024-10-09 21:02:35.241684
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.sql import table, select, update
+
+import json
+
+revision = "242a2047eae0"
+down_revision = "6a39f3d8e55c"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # Step 1: Rename current 'chat' column to 'old_chat'
+    op.alter_column("chat", "chat", new_column_name="old_chat", existing_type=sa.Text)
+
+    # Step 2: Add new 'chat' column of type JSON
+    op.add_column("chat", sa.Column("chat", sa.JSON(), nullable=True))
+
+    # Step 3: Migrate data from 'old_chat' to 'chat'
+    chat_table = table(
+        "chat",
+        sa.Column("id", sa.String, primary_key=True),
+        sa.Column("old_chat", sa.Text),
+        sa.Column("chat", sa.JSON()),
+    )
+
+    # - Selecting all data from the table
+    connection = op.get_bind()
+    results = connection.execute(select(chat_table.c.id, chat_table.c.old_chat))
+    for row in results:
+        try:
+            # Convert text JSON to actual JSON object, assuming the text is in JSON format
+            json_data = json.loads(row.old_chat)
+        except json.JSONDecodeError:
+            json_data = None  # Handle cases where the text cannot be converted to JSON
+
+        connection.execute(
+            sa.update(chat_table)
+            .where(chat_table.c.id == row.id)
+            .values(chat=json_data)
+        )
+
+    # Step 4: Drop 'old_chat' column
+    op.drop_column("chat", "old_chat")
+
+
+def downgrade():
+    # Step 1: Add 'old_chat' column back as Text
+    op.add_column("chat", sa.Column("old_chat", sa.Text(), nullable=True))
+
+    # Step 2: Convert 'chat' JSON data back to text and store in 'old_chat'
+    chat_table = table(
+        "chat",
+        sa.Column("id", sa.String, primary_key=True),
+        sa.Column("chat", sa.JSON()),
+        sa.Column("old_chat", sa.Text()),
+    )
+
+    connection = op.get_bind()
+    results = connection.execute(select(chat_table.c.id, chat_table.c.chat))
+    for row in results:
+        text_data = json.dumps(row.chat) if row.chat is not None else None
+        connection.execute(
+            sa.update(chat_table)
+            .where(chat_table.c.id == row.id)
+            .values(old_chat=text_data)
+        )
+
+    # Step 3: Remove the new 'chat' JSON column
+    op.drop_column("chat", "chat")
+
+    # Step 4: Rename 'old_chat' back to 'chat'
+    op.alter_column("chat", "old_chat", new_column_name="chat", existing_type=sa.Text)

+ 0 - 105
backend/open_webui/test/apps/webui/routers/test_documents.py

@@ -1,105 +0,0 @@
-from test.util.abstract_integration_test import AbstractPostgresTest
-from test.util.mock_user import mock_webui_user
-
-
-class TestDocuments(AbstractPostgresTest):
-    BASE_PATH = "/api/v1/documents"
-
-    def setup_class(cls):
-        super().setup_class()
-        from open_webui.apps.webui.models.documents import Documents
-
-        cls.documents = Documents
-
-    def test_documents(self):
-        # Empty database
-        assert len(self.documents.get_docs()) == 0
-        with mock_webui_user(id="2"):
-            response = self.fast_api_client.get(self.create_url("/"))
-        assert response.status_code == 200
-        assert len(response.json()) == 0
-
-        # Create a new document
-        with mock_webui_user(id="2"):
-            response = self.fast_api_client.post(
-                self.create_url("/create"),
-                json={
-                    "name": "doc_name",
-                    "title": "doc title",
-                    "collection_name": "custom collection",
-                    "filename": "doc_name.pdf",
-                    "content": "",
-                },
-            )
-        assert response.status_code == 200
-        assert response.json()["name"] == "doc_name"
-        assert len(self.documents.get_docs()) == 1
-
-        # Get the document
-        with mock_webui_user(id="2"):
-            response = self.fast_api_client.get(self.create_url("/doc?name=doc_name"))
-        assert response.status_code == 200
-        data = response.json()
-        assert data["collection_name"] == "custom collection"
-        assert data["name"] == "doc_name"
-        assert data["title"] == "doc title"
-        assert data["filename"] == "doc_name.pdf"
-        assert data["content"] == {}
-
-        # Create another document
-        with mock_webui_user(id="2"):
-            response = self.fast_api_client.post(
-                self.create_url("/create"),
-                json={
-                    "name": "doc_name 2",
-                    "title": "doc title 2",
-                    "collection_name": "custom collection 2",
-                    "filename": "doc_name2.pdf",
-                    "content": "",
-                },
-            )
-        assert response.status_code == 200
-        assert response.json()["name"] == "doc_name 2"
-        assert len(self.documents.get_docs()) == 2
-
-        # Get all documents
-        with mock_webui_user(id="2"):
-            response = self.fast_api_client.get(self.create_url("/"))
-        assert response.status_code == 200
-        assert len(response.json()) == 2
-
-        # Update the first document
-        with mock_webui_user(id="2"):
-            response = self.fast_api_client.post(
-                self.create_url("/doc/update?name=doc_name"),
-                json={"name": "doc_name rework", "title": "updated title"},
-            )
-        assert response.status_code == 200
-        data = response.json()
-        assert data["name"] == "doc_name rework"
-        assert data["title"] == "updated title"
-
-        # Tag the first document
-        with mock_webui_user(id="2"):
-            response = self.fast_api_client.post(
-                self.create_url("/doc/tags"),
-                json={
-                    "name": "doc_name rework",
-                    "tags": [{"name": "testing-tag"}, {"name": "another-tag"}],
-                },
-            )
-        assert response.status_code == 200
-        data = response.json()
-        assert data["name"] == "doc_name rework"
-        assert data["content"] == {
-            "tags": [{"name": "testing-tag"}, {"name": "another-tag"}]
-        }
-        assert len(self.documents.get_docs()) == 2
-
-        # Delete the first document
-        with mock_webui_user(id="2"):
-            response = self.fast_api_client.delete(
-                self.create_url("/doc/delete?name=doc_name rework")
-            )
-        assert response.status_code == 200
-        assert len(self.documents.get_docs()) == 1

+ 2 - 0
backend/requirements.txt

@@ -12,6 +12,7 @@ passlib[bcrypt]==1.7.4
 
 requests==2.32.3
 aiohttp==3.10.8
+async-timeout
 
 sqlalchemy==2.0.32
 alembic==1.13.2
@@ -41,6 +42,7 @@ langchain-chroma==0.1.4
 fake-useragent==1.5.1
 chromadb==0.5.9
 pymilvus==2.4.7
+qdrant-client~=1.12.0
 
 sentence-transformers==3.0.1
 colbert-ai==0.2.21

+ 0 - 44
cypress/e2e/documents.cy.ts

@@ -1,46 +1,2 @@
 // eslint-disable-next-line @typescript-eslint/triple-slash-reference
 /// <reference path="../support/index.d.ts" />
-
-describe('Documents', () => {
-	const timestamp = Date.now();
-
-	before(() => {
-		cy.uploadTestDocument(timestamp);
-	});
-
-	after(() => {
-		cy.deleteTestDocument(timestamp);
-	});
-
-	context('Admin', () => {
-		beforeEach(() => {
-			// Login as the admin user
-			cy.loginAdmin();
-			// Visit the home page
-			cy.visit('/workspace/documents');
-			cy.get('button').contains('#cypress-test').click();
-		});
-
-		it('can see documents', () => {
-			cy.get('div').contains(`document-test-initial-${timestamp}.txt`).should('have.length', 1);
-		});
-
-		it('can see edit button', () => {
-			cy.get('div')
-				.contains(`document-test-initial-${timestamp}.txt`)
-				.get("button[aria-label='Edit Doc']")
-				.should('exist');
-		});
-
-		it('can see delete button', () => {
-			cy.get('div')
-				.contains(`document-test-initial-${timestamp}.txt`)
-				.get("button[aria-label='Delete Doc']")
-				.should('exist');
-		});
-
-		it('can see upload button', () => {
-			cy.get("button[aria-label='Add Docs']").should('exist');
-		});
-	});
-});

+ 0 - 44
cypress/support/e2e.ts

@@ -73,50 +73,6 @@ Cypress.Commands.add('register', (name, email, password) => register(name, email
 Cypress.Commands.add('registerAdmin', () => registerAdmin());
 Cypress.Commands.add('loginAdmin', () => loginAdmin());
 
-Cypress.Commands.add('uploadTestDocument', (suffix: any) => {
-	// Login as admin
-	cy.loginAdmin();
-	// upload example document
-	cy.visit('/workspace/documents');
-	// Create a document
-	cy.get("button[aria-label='Add Docs']").click();
-	cy.readFile('cypress/data/example-doc.txt').then((text) => {
-		// select file
-		cy.get('#upload-doc-input').selectFile(
-			{
-				contents: Cypress.Buffer.from(text + Date.now()),
-				fileName: `document-test-initial-${suffix}.txt`,
-				mimeType: 'text/plain',
-				lastModified: Date.now()
-			},
-			{
-				force: true
-			}
-		);
-		// open tag input
-		cy.get("button[aria-label='Add Tag']").click();
-		cy.get("input[placeholder='Add a tag']").type('cypress-test');
-		cy.get("button[aria-label='Save Tag']").click();
-
-		// submit to upload
-		cy.get("button[type='submit']").click();
-
-		// wait for upload to finish
-		cy.get('button').contains('#cypress-test').should('exist');
-		cy.get('div').contains(`document-test-initial-${suffix}.txt`).should('exist');
-	});
-});
-
-Cypress.Commands.add('deleteTestDocument', (suffix: any) => {
-	cy.loginAdmin();
-	cy.visit('/workspace/documents');
-	// clean up uploaded documents
-	cy.get('div')
-		.contains(`document-test-initial-${suffix}.txt`)
-		.find("button[aria-label='Delete Doc']")
-		.click();
-});
-
 before(() => {
 	cy.registerAdmin();
 });

+ 1 - 0
pyproject.toml

@@ -20,6 +20,7 @@ dependencies = [
 
     "requests==2.32.3",
     "aiohttp==3.10.8",
+    "async-timeout",
 
     "sqlalchemy==2.0.32",
     "alembic==1.13.2",

+ 153 - 7
src/lib/apis/chats/index.ts

@@ -167,6 +167,44 @@ export const getAllChats = async (token: string) => {
 	return res;
 };
 
+export const getChatListBySearchText = async (token: string, text: string, page: number = 1) => {
+	let error = null;
+
+	const searchParams = new URLSearchParams();
+	searchParams.append('text', text);
+	searchParams.append('page', `${page}`);
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/search?${searchParams.toString()}`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res.map((chat) => ({
+		...chat,
+		time_range: getTimeRange(chat.updated_at)
+	}));
+};
+
 export const getAllArchivedChats = async (token: string) => {
 	let error = null;
 
@@ -232,7 +270,7 @@ export const getAllUserChats = async (token: string) => {
 export const getAllChatTags = async (token: string) => {
 	let error = null;
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/tags/all`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all/tags`, {
 		method: 'GET',
 		headers: {
 			Accept: 'application/json',
@@ -260,6 +298,40 @@ export const getAllChatTags = async (token: string) => {
 	return res;
 };
 
+export const getPinnedChatList = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/pinned`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res.map((chat) => ({
+		...chat,
+		time_range: getTimeRange(chat.updated_at)
+	}));
+};
+
 export const getChatListByTagName = async (token: string = '', tagName: string) => {
 	let error = null;
 
@@ -361,11 +433,87 @@ export const getChatByShareId = async (token: string, share_id: string) => {
 	return res;
 };
 
+export const getChatPinnedStatusById = async (token: string, id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/pinned`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err;
+
+			if ('detail' in err) {
+				error = err.detail;
+			} else {
+				error = err;
+			}
+
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const toggleChatPinnedStatusById = async (token: string, id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/pin`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err;
+
+			if ('detail' in err) {
+				error = err.detail;
+			} else {
+				error = err;
+			}
+
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
 export const cloneChatById = async (token: string, id: string) => {
 	let error = null;
 
 	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/clone`, {
-		method: 'GET',
+		method: 'POST',
 		headers: {
 			Accept: 'application/json',
 			'Content-Type': 'application/json',
@@ -435,7 +583,7 @@ export const archiveChatById = async (token: string, id: string) => {
 	let error = null;
 
 	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/archive`, {
-		method: 'GET',
+		method: 'POST',
 		headers: {
 			Accept: 'application/json',
 			'Content-Type': 'application/json',
@@ -605,8 +753,7 @@ export const addTagById = async (token: string, id: string, tagName: string) =>
 			...(token && { authorization: `Bearer ${token}` })
 		},
 		body: JSON.stringify({
-			tag_name: tagName,
-			chat_id: id
+			name: tagName
 		})
 	})
 		.then(async (res) => {
@@ -641,8 +788,7 @@ export const deleteTagById = async (token: string, id: string, tagName: string)
 			...(token && { authorization: `Bearer ${token}` })
 		},
 		body: JSON.stringify({
-			tag_name: tagName,
-			chat_id: id
+			name: tagName
 		})
 	})
 		.then(async (res) => {

+ 0 - 232
src/lib/apis/documents/index.ts

@@ -1,232 +0,0 @@
-import { WEBUI_API_BASE_URL } from '$lib/constants';
-
-export const createNewDoc = async (
-	token: string,
-	collection_name: string,
-	filename: string,
-	name: string,
-	title: string,
-	content: object | null = null
-) => {
-	let error = null;
-
-	const res = await fetch(`${WEBUI_API_BASE_URL}/documents/create`, {
-		method: 'POST',
-		headers: {
-			Accept: 'application/json',
-			'Content-Type': 'application/json',
-			authorization: `Bearer ${token}`
-		},
-		body: JSON.stringify({
-			collection_name: collection_name,
-			filename: filename,
-			name: name,
-			title: title,
-			...(content ? { content: JSON.stringify(content) } : {})
-		})
-	})
-		.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 getDocs = async (token: string = '') => {
-	let error = null;
-
-	const res = await fetch(`${WEBUI_API_BASE_URL}/documents/`, {
-		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 getDocByName = async (token: string, name: string) => {
-	let error = null;
-
-	const searchParams = new URLSearchParams();
-	searchParams.append('name', name);
-
-	const res = await fetch(`${WEBUI_API_BASE_URL}/documents/docs?${searchParams.toString()}`, {
-		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;
-};
-
-type DocUpdateForm = {
-	name: string;
-	title: string;
-};
-
-export const updateDocByName = async (token: string, name: string, form: DocUpdateForm) => {
-	let error = null;
-
-	const searchParams = new URLSearchParams();
-	searchParams.append('name', name);
-
-	const res = await fetch(`${WEBUI_API_BASE_URL}/documents/doc/update?${searchParams.toString()}`, {
-		method: 'POST',
-		headers: {
-			Accept: 'application/json',
-			'Content-Type': 'application/json',
-			authorization: `Bearer ${token}`
-		},
-		body: JSON.stringify({
-			name: form.name,
-			title: form.title
-		})
-	})
-		.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;
-};
-
-type TagDocForm = {
-	name: string;
-	tags: string[];
-};
-
-export const tagDocByName = async (token: string, name: string, form: TagDocForm) => {
-	let error = null;
-
-	const searchParams = new URLSearchParams();
-	searchParams.append('name', name);
-
-	const res = await fetch(`${WEBUI_API_BASE_URL}/documents/doc/tags?${searchParams.toString()}`, {
-		method: 'POST',
-		headers: {
-			Accept: 'application/json',
-			'Content-Type': 'application/json',
-			authorization: `Bearer ${token}`
-		},
-		body: JSON.stringify({
-			name: form.name,
-			tags: form.tags
-		})
-	})
-		.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 deleteDocByName = async (token: string, name: string) => {
-	let error = null;
-
-	const searchParams = new URLSearchParams();
-	searchParams.append('name', name);
-
-	const res = await fetch(`${WEBUI_API_BASE_URL}/documents/doc/delete?${searchParams.toString()}`, {
-		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;
-};

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

@@ -200,13 +200,13 @@ export const getEmbeddingConfig = async (token: string) => {
 type OpenAIConfigForm = {
 	key: string;
 	url: string;
-	batch_size: number;
 };
 
 type EmbeddingModelUpdateForm = {
 	openai_config?: OpenAIConfigForm;
 	embedding_engine: string;
 	embedding_model: string;
+	embedding_batch_size?: number;
 };
 
 export const updateEmbeddingConfig = async (token: string, payload: EmbeddingModelUpdateForm) => {

+ 20 - 13
src/lib/components/AddFilesPlaceholder.svelte

@@ -2,20 +2,27 @@
 	import { getContext } from 'svelte';
 
 	export let title = '';
+	export let content = '';
 	const i18n = getContext('i18n');
 </script>
 
-<div class="  text-center text-6xl mb-3">📄</div>
-<div class="text-center dark:text-white text-2xl font-semibold z-50">
-	{#if title}
-		{title}
-	{:else}
-		{$i18n.t('Add Files')}
-	{/if}
-</div>
-
-<slot
-	><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
-		{$i18n.t('Drop any files here to add to the conversation')}
+<div class="px-3">
+	<div class="text-center text-6xl mb-3">📄</div>
+	<div class="text-center dark:text-white text-xl font-semibold z-50">
+		{#if title}
+			{title}
+		{:else}
+			{$i18n.t('Add Files')}
+		{/if}
 	</div>
-</slot>
+
+	<slot
+		><div class="px-2 mt-2 text-center text-sm dark:text-gray-200 w-full">
+			{#if content}
+				{content}
+			{:else}
+				{$i18n.t('Drop any files here to add to the conversation')}
+			{/if}
+		</div>
+	</slot>
+</div>

+ 12 - 6
src/lib/components/admin/Settings/Documents.svelte

@@ -38,6 +38,7 @@
 
 	let embeddingEngine = '';
 	let embeddingModel = '';
+	let embeddingBatchSize = 1;
 	let rerankingModel = '';
 
 	let fileMaxSize = null;
@@ -53,7 +54,6 @@
 
 	let OpenAIKey = '';
 	let OpenAIUrl = '';
-	let OpenAIBatchSize = 1;
 
 	let querySettings = {
 		template: '',
@@ -100,12 +100,16 @@
 		const res = await updateEmbeddingConfig(localStorage.token, {
 			embedding_engine: embeddingEngine,
 			embedding_model: embeddingModel,
+			...(embeddingEngine === 'openai' || embeddingEngine === 'ollama'
+				? {
+						embedding_batch_size: embeddingBatchSize
+					}
+				: {}),
 			...(embeddingEngine === 'openai'
 				? {
 						openai_config: {
 							key: OpenAIKey,
-							url: OpenAIUrl,
-							batch_size: OpenAIBatchSize
+							url: OpenAIUrl
 						}
 					}
 				: {})
@@ -193,10 +197,10 @@
 		if (embeddingConfig) {
 			embeddingEngine = embeddingConfig.embedding_engine;
 			embeddingModel = embeddingConfig.embedding_model;
+			embeddingBatchSize = embeddingConfig.embedding_batch_size ?? 1;
 
 			OpenAIKey = embeddingConfig.openai_config.key;
 			OpenAIUrl = embeddingConfig.openai_config.url;
-			OpenAIBatchSize = embeddingConfig.openai_config.batch_size ?? 1;
 		}
 	};
 
@@ -309,6 +313,8 @@
 
 					<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OpenAIKey} />
 				</div>
+			{/if}
+			{#if embeddingEngine === 'ollama' || embeddingEngine === 'openai'}
 				<div class="flex mt-0.5 space-x-2">
 					<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Batch Size')}</div>
 					<div class=" flex-1">
@@ -318,13 +324,13 @@
 							min="1"
 							max="2048"
 							step="1"
-							bind:value={OpenAIBatchSize}
+							bind:value={embeddingBatchSize}
 							class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
 						/>
 					</div>
 					<div class="">
 						<input
-							bind:value={OpenAIBatchSize}
+							bind:value={embeddingBatchSize}
 							type="number"
 							class=" bg-transparent text-center w-14"
 							min="-2"

+ 13 - 1
src/lib/components/chat/Artifacts.svelte

@@ -10,6 +10,7 @@
 	import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte';
 	import Tooltip from '../common/Tooltip.svelte';
 	import SvgPanZoom from '../common/SVGPanZoom.svelte';
+	import ArrowLeft from '../icons/ArrowLeft.svelte';
 
 	export let overlay = false;
 	export let history;
@@ -183,6 +184,17 @@
 			<div class=" absolute top-0 left-0 right-0 bottom-0 z-10"></div>
 		{/if}
 
+		<div class="absolute pointer-events-none z-50 w-full flex items-center justify-start p-4">
+			<button
+				class="self-center pointer-events-auto p-1 rounded-full bg-white dark:bg-gray-850"
+				on:click={() => {
+					showArtifacts.set(false);
+				}}
+			>
+				<ArrowLeft className="size-3.5  text-gray-900 dark:text-white" />
+			</button>
+		</div>
+
 		<div class=" absolute pointer-events-none z-50 w-full flex items-center justify-end p-4">
 			<button
 				class="self-center pointer-events-auto p-1 rounded-full bg-white dark:bg-gray-850"
@@ -192,7 +204,7 @@
 					showArtifacts.set(false);
 				}}
 			>
-				<XMark className="size-3 text-gray-900 dark:text-white" />
+				<XMark className="size-3.5 text-gray-900 dark:text-white" />
 			</button>
 		</div>
 

+ 164 - 42
src/lib/components/chat/Chat.svelte

@@ -53,7 +53,7 @@
 		updateChatById
 	} from '$lib/apis/chats';
 	import { generateOpenAIChatCompletion } from '$lib/apis/openai';
-	import { processWebSearch } from '$lib/apis/retrieval';
+	import { processWeb, processWebSearch, processYoutubeVideo } from '$lib/apis/retrieval';
 	import { createOpenAITextStream } from '$lib/apis/streaming';
 	import { queryMemory } from '$lib/apis/memories';
 	import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
@@ -78,6 +78,7 @@
 	let loaded = false;
 	const eventTarget = new EventTarget();
 	let controlPane;
+	let controlPaneComponent;
 
 	let stopResponseFlag = false;
 	let autoScroll = true;
@@ -199,6 +200,20 @@
 
 				eventConfirmationTitle = data.title;
 				eventConfirmationMessage = data.message;
+			} else if (type === 'execute') {
+				eventCallback = cb;
+
+				try {
+					// Use Function constructor to evaluate code in a safer way
+					const asyncFunction = new Function(`return (async () => { ${data.code} })()`);
+					const result = await asyncFunction(); // Await the result of the async function
+
+					if (cb) {
+						cb(result);
+					}
+				} catch (error) {
+					console.error('Error executing code:', error);
+				}
 			} else if (type === 'input') {
 				eventCallback = cb;
 
@@ -276,14 +291,9 @@
 			if (controlPane && !$mobile) {
 				try {
 					if (value) {
-						const currentSize = controlPane.getSize();
-
-						if (currentSize === 0) {
-							const size = parseInt(localStorage?.chatControlsSize ?? '30');
-							controlPane.resize(size ? size : 30);
-						}
+						controlPaneComponent.openPane();
 					} else {
-						controlPane.resize(0);
+						controlPane.collapse();
 					}
 				} catch (e) {
 					// ignore
@@ -293,6 +303,7 @@
 			if (!value) {
 				showCallOverlay.set(false);
 				showOverview.set(false);
+				showArtifacts.set(false);
 			}
 		});
 
@@ -308,6 +319,74 @@
 		$socket?.off('chat-events');
 	});
 
+	// File upload functions
+
+	const uploadWeb = async (url) => {
+		console.log(url);
+
+		const fileItem = {
+			type: 'doc',
+			name: url,
+			collection_name: '',
+			status: 'uploading',
+			url: url,
+			error: ''
+		};
+
+		try {
+			files = [...files, fileItem];
+			const res = await processWeb(localStorage.token, '', url);
+
+			if (res) {
+				fileItem.status = 'uploaded';
+				fileItem.collection_name = res.collection_name;
+				fileItem.file = {
+					...res.file,
+					...fileItem.file
+				};
+
+				files = files;
+			}
+		} catch (e) {
+			// Remove the failed doc from the files array
+			files = files.filter((f) => f.name !== url);
+			toast.error(JSON.stringify(e));
+		}
+	};
+
+	const uploadYoutubeTranscription = async (url) => {
+		console.log(url);
+
+		const fileItem = {
+			type: 'doc',
+			name: url,
+			collection_name: '',
+			status: 'uploading',
+			context: 'full',
+			url: url,
+			error: ''
+		};
+
+		try {
+			files = [...files, fileItem];
+			const res = await processYoutubeVideo(localStorage.token, url);
+
+			if (res) {
+				fileItem.status = 'uploaded';
+				fileItem.collection_name = res.collection_name;
+				fileItem.file = {
+					...res.file,
+					...fileItem.file
+				};
+				files = files;
+			}
+		} catch (e) {
+			// Remove the failed doc from the files array
+			files = files.filter((f) => f.name !== url);
+			toast.error(e);
+		}
+	};
+
 	//////////////////////////
 	// Web functions
 	//////////////////////////
@@ -345,7 +424,17 @@
 			console.log($config?.default_models.split(',') ?? '');
 			selectedModels = $config?.default_models.split(',');
 		} else {
-			selectedModels = [''];
+			if ($models.length > 0) {
+				selectedModels = [$models[0].id];
+			} else {
+				selectedModels = [''];
+			}
+		}
+
+		if ($page.url.searchParams.get('youtube')) {
+			uploadYoutubeTranscription(
+				`https://www.youtube.com/watch?v=${$page.url.searchParams.get('youtube')}`
+			);
 		}
 
 		if ($page.url.searchParams.get('web-search') === 'true') {
@@ -366,6 +455,11 @@
 				.filter((id) => id);
 		}
 
+		if ($page.url.searchParams.get('call') === 'true') {
+			showCallOverlay.set(true);
+			showControls.set(true);
+		}
+
 		if ($page.url.searchParams.get('q')) {
 			prompt = $page.url.searchParams.get('q') ?? '';
 
@@ -375,11 +469,6 @@
 			}
 		}
 
-		if ($page.url.searchParams.get('call') === 'true') {
-			showCallOverlay.set(true);
-			showControls.set(true);
-		}
-
 		selectedModels = selectedModels.map((modelId) =>
 			$models.map((m) => m.id).includes(modelId) ? modelId : ''
 		);
@@ -1855,6 +1944,7 @@
 				system: $settings.system ?? undefined,
 				params: params,
 				history: history,
+				messages: createMessagesList(history.currentId),
 				tags: [],
 				timestamp: Date.now()
 			});
@@ -1920,6 +2010,7 @@
 		class="h-screen max-h-[100dvh] {$showSidebar
 			? 'md:max-w-[calc(100%-260px)]'
 			: ''} w-full max-w-full flex flex-col"
+		id="chat-container"
 	>
 		{#if $settings?.backgroundImageUrl ?? null}
 			<div
@@ -1935,7 +2026,17 @@
 		{/if}
 
 		<Navbar
-			{chat}
+			chat={{
+				id: $chatId,
+				chat: {
+					title: $chatTitle,
+					models: selectedModels,
+					system: $settings.system ?? undefined,
+					params: params,
+					history: history,
+					timestamp: Date.now()
+				}
+			}}
 			title={$chatTitle}
 			bind:selectedModels
 			shareEnabled={!!history.currentId}
@@ -2050,6 +2151,15 @@
 								transparentBackground={$settings?.backgroundImageUrl ?? false}
 								{stopResponse}
 								{createMessagePair}
+								on:upload={async (e) => {
+									const { type, data } = e.detail;
+
+									if (type === 'web') {
+										await uploadWeb(data);
+									} else if (type === 'youtube') {
+										await uploadYoutubeTranscription(data);
+									}
+								}}
 								on:submit={async (e) => {
 									if (e.detail) {
 										prompt = '';
@@ -2066,38 +2176,50 @@
 							</div>
 						</div>
 					{:else}
-						<Placeholder
-							{history}
-							{selectedModels}
-							bind:files
-							bind:prompt
-							bind:autoScroll
-							bind:selectedToolIds
-							bind:webSearchEnabled
-							bind:atSelectedModel
-							availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
-								const model = $models.find((m) => m.id === e);
-								if (model?.info?.meta?.toolIds ?? false) {
-									return [...new Set([...a, ...model.info.meta.toolIds])];
-								}
-								return a;
-							}, [])}
-							transparentBackground={$settings?.backgroundImageUrl ?? false}
-							{stopResponse}
-							{createMessagePair}
-							on:submit={async (e) => {
-								if (e.detail) {
-									prompt = '';
-									await tick();
-									submitPrompt(e.detail);
-								}
-							}}
-						/>
+						<div class="overflow-auto w-full h-full flex items-center">
+							<Placeholder
+								{history}
+								{selectedModels}
+								bind:files
+								bind:prompt
+								bind:autoScroll
+								bind:selectedToolIds
+								bind:webSearchEnabled
+								bind:atSelectedModel
+								availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
+									const model = $models.find((m) => m.id === e);
+									if (model?.info?.meta?.toolIds ?? false) {
+										return [...new Set([...a, ...model.info.meta.toolIds])];
+									}
+									return a;
+								}, [])}
+								transparentBackground={$settings?.backgroundImageUrl ?? false}
+								{stopResponse}
+								{createMessagePair}
+								on:upload={async (e) => {
+									const { type, data } = e.detail;
+
+									if (type === 'web') {
+										await uploadWeb(data);
+									} else if (type === 'youtube') {
+										await uploadYoutubeTranscription(data);
+									}
+								}}
+								on:submit={async (e) => {
+									if (e.detail) {
+										prompt = '';
+										await tick();
+										submitPrompt(e.detail);
+									}
+								}}
+							/>
+						</div>
 					{/if}
 				</div>
 			</Pane>
 
 			<ChatControls
+				bind:this={controlPaneComponent}
 				bind:history
 				bind:chatFiles
 				bind:params

+ 56 - 13
src/lib/components/chat/ChatControls.svelte

@@ -1,6 +1,7 @@
 <script lang="ts">
 	import { SvelteFlowProvider } from '@xyflow/svelte';
 	import { slide } from 'svelte/transition';
+	import { Pane, PaneResizer } from 'paneforge';
 
 	import { onDestroy, onMount, tick } from 'svelte';
 	import { mobile, showControls, showCallOverlay, showOverview, showArtifacts } from '$lib/stores';
@@ -10,9 +11,9 @@
 	import CallOverlay from './MessageInput/CallOverlay.svelte';
 	import Drawer from '../common/Drawer.svelte';
 	import Overview from './Overview.svelte';
-	import { Pane, PaneResizer } from 'paneforge';
 	import EllipsisVertical from '../icons/EllipsisVertical.svelte';
 	import Artifacts from './Artifacts.svelte';
+	import { min } from '@floating-ui/utils';
 
 	export let history;
 	export let models = [];
@@ -35,6 +36,16 @@
 	let largeScreen = false;
 	let dragged = false;
 
+	let minSize = 0;
+
+	export const openPane = () => {
+		if (parseInt(localStorage?.chatControlsSize)) {
+			pane.resize(parseInt(localStorage?.chatControlsSize));
+		} else {
+			pane.resize(minSize);
+		}
+	};
+
 	const handleMediaQuery = async (e) => {
 		if (e.matches) {
 			largeScreen = true;
@@ -71,6 +82,32 @@
 		mediaQuery.addEventListener('change', handleMediaQuery);
 		handleMediaQuery(mediaQuery);
 
+		// Select the container element you want to observe
+		const container = document.getElementById('chat-container');
+
+		// initialize the minSize based on the container width
+		minSize = Math.floor((350 / container.clientWidth) * 100);
+
+		// Create a new ResizeObserver instance
+		const resizeObserver = new ResizeObserver((entries) => {
+			for (let entry of entries) {
+				const width = entry.contentRect.width;
+				// calculate the percentage of 200px
+				const percentage = (350 / width) * 100;
+				// set the minSize to the percentage, must be an integer
+				minSize = Math.floor(percentage);
+
+				if ($showControls) {
+					if (pane && pane.isExpanded() && pane.getSize() < minSize) {
+						pane.resize(minSize);
+					}
+				}
+			}
+		});
+
+		// Start observing the container's size changes
+		resizeObserver.observe(container);
+
 		document.addEventListener('mousedown', onMouseDown);
 		document.addEventListener('mouseup', onMouseUp);
 	});
@@ -163,23 +200,29 @@
 				</div>
 			</PaneResizer>
 		{/if}
+
 		<Pane
 			bind:pane
-			defaultSize={$showControls
-				? parseInt(localStorage?.chatControlsSize ?? '30')
-					? parseInt(localStorage?.chatControlsSize ?? '30')
-					: 30
-				: 0}
+			defaultSize={0}
 			onResize={(size) => {
-				if (size === 0) {
-					showControls.set(false);
-				} else {
-					if (!$showControls) {
-						showControls.set(true);
+				console.log('size', size, minSize);
+
+				if ($showControls && pane.isExpanded()) {
+					if (size < minSize) {
+						pane.resize(minSize);
+					}
+
+					if (size < minSize) {
+						localStorage.chatControlsSize = 0;
+					} else {
+						localStorage.chatControlsSize = size;
 					}
-					localStorage.chatControlsSize = size;
 				}
 			}}
+			onCollapse={() => {
+				showControls.set(false);
+			}}
+			collapsible={true}
 			class="pt-8"
 		>
 			{#if $showControls}
@@ -187,7 +230,7 @@
 					<div
 						class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay
 							? ' '
-							: 'px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850  border border-gray-50 dark:border-gray-800'}  rounded-lg z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
+							: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850  border border-gray-50 dark:border-gray-800'}  rounded-lg z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
 					>
 						{#if $showCallOverlay}
 							<div class="w-full h-full flex justify-center">

+ 3 - 3
src/lib/components/chat/Controls/Controls.svelte

@@ -16,7 +16,7 @@
 </script>
 
 <div class=" dark:text-white">
-	<div class=" flex justify-between dark:text-gray-100 mb-2">
+	<div class=" flex items-center justify-between dark:text-gray-100 mb-2">
 		<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Controls')}</div>
 		<button
 			class="self-center"
@@ -24,11 +24,11 @@
 				dispatch('close');
 			}}
 		>
-			<XMark className="size-4" />
+			<XMark className="size-3.5" />
 		</button>
 	</div>
 
-	<div class=" dark:text-gray-200 text-sm font-primary py-0.5">
+	<div class=" dark:text-gray-200 text-sm font-primary py-0.5 px-0.5">
 		{#if chatFiles.length > 0}
 			<Collapsible title={$i18n.t('Files')} open={true}>
 				<div class="flex flex-col gap-1 mt-1.5" slot="content">

+ 63 - 56
src/lib/components/chat/MessageInput.svelte

@@ -1,6 +1,6 @@
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
-	import { onMount, tick, getContext, createEventDispatcher } from 'svelte';
+	import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
 	const dispatch = createEventDispatcher();
 
 	import {
@@ -175,57 +175,59 @@
 		});
 	};
 
-	onMount(() => {
-		window.setTimeout(() => chatTextAreaElement?.focus(), 0);
-
-		const dropZone = document.querySelector('body');
+	const handleKeyDown = (event: KeyboardEvent) => {
+		if (event.key === 'Escape') {
+			console.log('Escape');
+			dragged = false;
+		}
+	};
 
-		const handleKeyDown = (event: KeyboardEvent) => {
-			if (event.key === 'Escape') {
-				console.log('Escape');
-				dragged = false;
-			}
-		};
+	const onDragOver = (e) => {
+		e.preventDefault();
+		dragged = true;
+	};
 
-		const onDragOver = (e) => {
-			e.preventDefault();
-			dragged = true;
-		};
+	const onDragLeave = () => {
+		dragged = false;
+	};
 
-		const onDragLeave = () => {
-			dragged = false;
-		};
+	const onDrop = async (e) => {
+		e.preventDefault();
+		console.log(e);
 
-		const onDrop = async (e) => {
-			e.preventDefault();
-			console.log(e);
-
-			if (e.dataTransfer?.files) {
-				const inputFiles = Array.from(e.dataTransfer?.files);
-				if (inputFiles && inputFiles.length > 0) {
-					console.log(inputFiles);
-					inputFilesHandler(inputFiles);
-				} else {
-					toast.error($i18n.t(`File not found.`));
-				}
+		if (e.dataTransfer?.files) {
+			const inputFiles = Array.from(e.dataTransfer?.files);
+			if (inputFiles && inputFiles.length > 0) {
+				console.log(inputFiles);
+				inputFilesHandler(inputFiles);
+			} else {
+				toast.error($i18n.t(`File not found.`));
 			}
+		}
 
-			dragged = false;
-		};
+		dragged = false;
+	};
+
+	onMount(() => {
+		window.setTimeout(() => chatTextAreaElement?.focus(), 0);
 
 		window.addEventListener('keydown', handleKeyDown);
 
+		const dropZone = document.getElementById('chat-container');
+
 		dropZone?.addEventListener('dragover', onDragOver);
 		dropZone?.addEventListener('drop', onDrop);
 		dropZone?.addEventListener('dragleave', onDragLeave);
+	});
 
-		return () => {
-			window.removeEventListener('keydown', handleKeyDown);
+	onDestroy(() => {
+		window.removeEventListener('keydown', handleKeyDown);
 
-			dropZone?.removeEventListener('dragover', onDragOver);
-			dropZone?.removeEventListener('drop', onDrop);
-			dropZone?.removeEventListener('dragleave', onDragLeave);
-		};
+		const dropZone = document.getElementById('chat-container');
+
+		dropZone?.removeEventListener('dragover', onDragOver);
+		dropZone?.removeEventListener('drop', onDrop);
+		dropZone?.removeEventListener('dragleave', onDragLeave);
 	});
 </script>
 
@@ -300,6 +302,9 @@
 					bind:this={commandsElement}
 					bind:prompt
 					bind:files
+					on:upload={(e) => {
+						dispatch('upload', e.detail);
+					}}
 					on:select={(e) => {
 						const data = e.detail;
 
@@ -791,25 +796,27 @@
 								{/if}
 							{:else}
 								<div class=" flex items-center mb-1.5">
-									<button
-										class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
-										on:click={() => {
-											stopResponse();
-										}}
-									>
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											viewBox="0 0 24 24"
-											fill="currentColor"
-											class="size-6"
+									<Tooltip content={$i18n.t('Stop')}>
+										<button
+											class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
+											on:click={() => {
+												stopResponse();
+											}}
 										>
-											<path
-												fill-rule="evenodd"
-												d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
-												clip-rule="evenodd"
-											/>
-										</svg>
-									</button>
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												viewBox="0 0 24 24"
+												fill="currentColor"
+												class="size-6"
+											>
+												<path
+													fill-rule="evenodd"
+													d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
+													clip-rule="evenodd"
+												/>
+											</svg>
+										</button>
+									</Tooltip>
 								</div>
 							{/if}
 						</div>

+ 8 - 68
src/lib/components/chat/MessageInput/Commands.svelte

@@ -26,71 +26,6 @@
 
 	let command = '';
 	$: command = (prompt?.trim() ?? '').split(' ')?.at(-1) ?? '';
-
-	const uploadWeb = async (url) => {
-		console.log(url);
-
-		const fileItem = {
-			type: 'doc',
-			name: url,
-			collection_name: '',
-			status: 'uploading',
-			url: url,
-			error: ''
-		};
-
-		try {
-			files = [...files, fileItem];
-			const res = await processWeb(localStorage.token, '', url);
-
-			if (res) {
-				fileItem.status = 'uploaded';
-				fileItem.collection_name = res.collection_name;
-				fileItem.file = {
-					...res.file,
-					...fileItem.file
-				};
-
-				files = files;
-			}
-		} catch (e) {
-			// Remove the failed doc from the files array
-			files = files.filter((f) => f.name !== url);
-			toast.error(JSON.stringify(e));
-		}
-	};
-
-	const uploadYoutubeTranscription = async (url) => {
-		console.log(url);
-
-		const fileItem = {
-			type: 'doc',
-			name: url,
-			collection_name: '',
-			status: 'uploading',
-			url: url,
-			error: ''
-		};
-
-		try {
-			files = [...files, fileItem];
-			const res = await processYoutubeVideo(localStorage.token, url);
-
-			if (res) {
-				fileItem.status = 'uploaded';
-				fileItem.collection_name = res.collection_name;
-				fileItem.file = {
-					...res.file,
-					...fileItem.file
-				};
-				files = files;
-			}
-		} catch (e) {
-			// Remove the failed doc from the files array
-			files = files.filter((f) => f.name !== url);
-			toast.error(e);
-		}
-	};
 </script>
 
 {#if ['/', '#', '@'].includes(command?.charAt(0))}
@@ -103,18 +38,23 @@
 			{command}
 			on:youtube={(e) => {
 				console.log(e);
-				uploadYoutubeTranscription(e.detail);
+				dispatch('upload', {
+					type: 'youtube',
+					data: e.detail
+				});
 			}}
 			on:url={(e) => {
 				console.log(e);
-				uploadWeb(e.detail);
+				dispatch('upload', {
+					type: 'web',
+					data: e.detail
+				});
 			}}
 			on:select={(e) => {
 				console.log(e);
 				files = [
 					...files,
 					{
-						type: e?.detail?.meta?.document ? 'file' : 'collection',
 						...e.detail,
 						status: 'processed'
 					}

+ 128 - 40
src/lib/components/chat/MessageInput/Commands/Knowledge.svelte

@@ -2,6 +2,10 @@
 	import { toast } from 'svelte-sonner';
 	import Fuse from 'fuse.js';
 
+	import dayjs from 'dayjs';
+	import relativeTime from 'dayjs/plugin/relativeTime';
+	dayjs.extend(relativeTime);
+
 	import { createEventDispatcher, tick, getContext, onMount } from 'svelte';
 	import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
 	import { knowledge } from '$lib/stores';
@@ -72,7 +76,13 @@
 	};
 
 	onMount(() => {
-		let legacy_documents = $knowledge.filter((item) => item?.meta?.document);
+		let legacy_documents = $knowledge
+			.filter((item) => item?.meta?.document)
+			.map((item) => ({
+				...item,
+				type: 'file'
+			}));
+
 		let legacy_collections =
 			legacy_documents.length > 0
 				? [
@@ -101,12 +111,44 @@
 					]
 				: [];
 
-		items = [...$knowledge, ...legacy_collections].map((item) => {
-			return {
+		let collections = $knowledge
+			.filter((item) => !item?.meta?.document)
+			.map((item) => ({
 				...item,
-				...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
-			};
-		});
+				type: 'collection'
+			}));
+		let collection_files =
+			$knowledge.length > 0
+				? [
+						...$knowledge
+							.reduce((a, item) => {
+								return [
+									...new Set([
+										...a,
+										...(item?.files ?? []).map((file) => ({
+											...file,
+											collection: { name: item.name, description: item.description }
+										}))
+									])
+								];
+							}, [])
+							.map((file) => ({
+								...file,
+								name: file?.meta?.name,
+								description: `${file?.collection?.name} - ${file?.collection?.description}`,
+								type: 'file'
+							}))
+					]
+				: [];
+
+		items = [...collections, ...collection_files, ...legacy_collections, ...legacy_documents].map(
+			(item) => {
+				return {
+					...item,
+					...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
+				};
+			}
+		);
 
 		fuse = new Fuse(items, {
 			keys: ['name', 'description']
@@ -117,20 +159,17 @@
 {#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
 	<div
 		id="commands-container"
-		class="pl-2 pr-14 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
+		class="pl-8 pr-16 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-medium mt-2">#</div>
-			</div>
-
+		<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
 			<div
-				class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-900 dark:text-gray-100"
+				class="max-h-60 flex flex-col w-full rounded-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 scrollbar-hidden">
 					{#each filteredItems as item, idx}
 						<button
-							class=" px-3 py-1.5 rounded-xl w-full text-left {idx === selectedIdx
+							class=" px-3 py-1.5 rounded-xl w-full text-left flex justify-between items-center {idx ===
+							selectedIdx
 								? ' bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button'
 								: ''}"
 							type="button"
@@ -141,38 +180,87 @@
 							on:mousemove={() => {
 								selectedIdx = idx;
 							}}
-							on:focus={() => {}}
 						>
-							<div class=" font-medium text-black dark:text-gray-100 flex items-center gap-1">
-								{#if item.legacy}
-									<div
-										class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1"
-									>
-										Legacy
-									</div>
-								{:else if item?.meta?.document}
-									<div
-										class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1"
-									>
-										Document
-									</div>
-								{:else}
-									<div
-										class="bg-green-500/20 text-green-700 dark:text-green-200 rounded uppercase text-xs font-bold px-1"
-									>
-										Collection
-									</div>
-								{/if}
+							<div>
+								<div class=" font-medium text-black dark:text-gray-100 flex items-center gap-1">
+									{#if item.legacy}
+										<div
+											class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
+										>
+											Legacy
+										</div>
+									{:else if item?.meta?.document}
+										<div
+											class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
+										>
+											Document
+										</div>
+									{:else if item?.type === 'file'}
+										<div
+											class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
+										>
+											File
+										</div>
+									{:else}
+										<div
+											class="bg-green-500/20 text-green-700 dark:text-green-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
+										>
+											Collection
+										</div>
+									{/if}
 
-								<div class="line-clamp-1">
-									{item.name}
+									<div class="line-clamp-1">
+										{item?.name}
+									</div>
 								</div>
-							</div>
 
-							<div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
-								{item?.description}
+								<div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
+									{item?.description}
+								</div>
 							</div>
 						</button>
+
+						<!-- <div slot="content" class=" pl-2 pt-1 flex flex-col gap-0.5">
+								{#if !item.legacy && (item?.files ?? []).length > 0}
+									{#each item?.files ?? [] as file, fileIdx}
+										<button
+											class=" px-3 py-1.5 rounded-xl w-full text-left flex justify-between items-center hover:bg-gray-50 dark:hover:bg-gray-850 dark:hover:text-gray-100 selected-command-option-button"
+											type="button"
+											on:click={() => {
+												console.log(file);
+											}}
+											on:mousemove={() => {
+												selectedIdx = idx;
+											}}
+										>
+											<div>
+												<div
+													class=" font-medium text-black dark:text-gray-100 flex items-center gap-1"
+												>
+													<div
+														class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
+													>
+														File
+													</div>
+
+													<div class="line-clamp-1">
+														{file?.meta?.name}
+													</div>
+												</div>
+
+												<div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
+													{$i18n.t('Updated')}
+													{dayjs(file.updated_at * 1000).fromNow()}
+												</div>
+											</div>
+										</button>
+									{/each}
+								{:else}
+									<div class=" text-gray-500 text-xs mt-1 mb-2">
+										{$i18n.t('No files found.')}
+									</div>
+								{/if}
+							</div> -->
 					{/each}
 
 					{#if prompt

+ 3 - 7
src/lib/components/chat/MessageInput/Commands/Models.svelte

@@ -68,15 +68,11 @@
 {#if filteredItems.length > 0}
 	<div
 		id="commands-container"
-		class="pl-2 pr-14 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
+		class="pl-8 pr-16 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-medium mt-2">@</div>
-			</div>
-
+		<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
 			<div
-				class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
+				class="max-h-60 flex flex-col w-full rounded-xl 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 filteredItems as model, modelIdx}

+ 5 - 9
src/lib/components/chat/MessageInput/Commands/Prompts.svelte

@@ -132,17 +132,13 @@
 {#if filteredPrompts.length > 0}
 	<div
 		id="commands-container"
-		class="pl-2 pr-14 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
+		class="pl-8 pr-16 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-medium mt-2">/</div>
-			</div>
-
+		<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
 			<div
-				class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
+				class="max-h-60 flex flex-col w-full rounded-xl 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">
+				<div class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden">
 					{#each filteredPrompts as prompt, promptIdx}
 						<button
 							class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx
@@ -169,7 +165,7 @@
 				</div>
 
 				<div
-					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"
+					class=" px-2 pt-0.5 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-900 rounded-b-xl flex items-center space-x-1"
 				>
 					<div>
 						<svg

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

@@ -302,6 +302,7 @@ __builtins__.input = input`);
 				<SvgPanZoom
 					className=" border border-gray-50 dark:border-gray-850 rounded-lg max-h-fit overflow-hidden"
 					svg={mermaidHtml}
+					content={_token.text}
 				/>
 			{:else}
 				<pre class="mermaid">{code}</pre>

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

@@ -479,7 +479,7 @@
 										id={message.id}
 										content={message.content}
 										floatingButtons={message?.done}
-										save={true}
+										save={!readOnly}
 										{model}
 										on:update={(e) => {
 											const { raw, oldContent, newContent } = e.detail;

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

@@ -94,7 +94,7 @@
 				: (user?.profile_image_url ?? '/user.png')}
 		/>
 	{/if}
-	<div class="w-full w-0 pl-1">
+	<div class="flex-auto w-0 max-w-full pl-1">
 		{#if !($settings?.chatBubble ?? true)}
 			<div>
 				<Name>

+ 15 - 4
src/lib/components/chat/Overview.svelte

@@ -15,6 +15,7 @@
 	import CustomNode from './Overview/Node.svelte';
 	import Flow from './Overview/Flow.svelte';
 	import XMark from '../icons/XMark.svelte';
+	import ArrowLeft from '../icons/ArrowLeft.svelte';
 
 	const { width, height } = useStore();
 
@@ -159,16 +160,26 @@
 </script>
 
 <div class="w-full h-full relative">
-	<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-5 py-4">
-		<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div>
+	<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-4 py-3.5">
+		<div class="flex items-center gap-2.5">
+			<button
+				class="self-center p-0.5"
+				on:click={() => {
+					showOverview.set(false);
+				}}
+			>
+				<ArrowLeft className="size-3.5" />
+			</button>
+			<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div>
+		</div>
 		<button
-			class="self-center"
+			class="self-center p-0.5"
 			on:click={() => {
 				dispatch('close');
 				showOverview.set(false);
 			}}
 		>
-			<XMark className="size-4" />
+			<XMark className="size-3.5" />
 		</button>
 	</div>
 

+ 5 - 2
src/lib/components/chat/Placeholder.svelte

@@ -89,7 +89,7 @@
 </script>
 
 {#key mounted}
-	<div class="m-auto w-full max-w-6xl px-2 xl:px-20 translate-y-6 text-center">
+	<div class="m-auto w-full max-w-6xl px-2 xl:px-20 translate-y-6 py-24 text-center">
 		{#if $temporaryChatEnabled}
 			<Tooltip
 				content="This chat won't appear in history and your messages will not be saved."
@@ -186,7 +186,7 @@
 				</div>
 
 				<div
-					class="text-base font-normal xl:translate-x-6 lg:max-w-3xl w-full py-3 {atSelectedModel
+					class="text-base font-normal xl:translate-x-6 md:max-w-3xl w-full py-3 {atSelectedModel
 						? 'mt-2'
 						: ''}"
 				>
@@ -204,6 +204,9 @@
 						{stopResponse}
 						{createMessagePair}
 						placeholder={$i18n.t('How can I help you today?')}
+						on:upload={(e) => {
+							dispatch('upload', e.detail);
+						}}
 						on:submit={(e) => {
 							dispatch('submit', e.detail);
 						}}

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

@@ -25,51 +25,32 @@
 	let tags = [];
 
 	const getTags = async () => {
-		return (
-			await getTagsById(localStorage.token, chatId).catch(async (error) => {
-				return [];
-			})
-		).filter((tag) => tag.name !== 'pinned');
+		return await getTagsById(localStorage.token, chatId).catch(async (error) => {
+			return [];
+		});
 	};
 
 	const addTag = async (tagName) => {
 		const res = await addTagById(localStorage.token, chatId, tagName);
 		tags = await getTags();
-
 		await updateChatById(localStorage.token, chatId, {
 			tags: tags
 		});
 
 		_tags.set(await getAllChatTags(localStorage.token));
-		await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
 	};
 
 	const deleteTag = async (tagName) => {
 		const res = await deleteTagById(localStorage.token, chatId, tagName);
 		tags = await getTags();
-
 		await updateChatById(localStorage.token, chatId, {
 			tags: tags
 		});
 
 		await _tags.set(await getAllChatTags(localStorage.token));
-		if ($_tags.map((t) => t.name).includes(tagName)) {
-			if (tagName === 'pinned') {
-				await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
-			} else {
-				await chats.set(await getChatListByTagName(localStorage.token, tagName));
-			}
-
-			if ($chats.find((chat) => chat.id === chatId)) {
-				dispatch('close');
-			}
-		} else {
-			// if the tag we deleted is no longer a valid tag, return to main chat list view
-			currentChatPage.set(1);
-			await chats.set(await getChatList(localStorage.token, $currentChatPage));
-			await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
-			await scrollPaginationEnabled.set(true);
-		}
+		dispatch('delete', {
+			name: tagName
+		});
 	};
 
 	onMount(async () => {

+ 26 - 2
src/lib/components/common/SVGPanZoom.svelte

@@ -1,11 +1,19 @@
 <script lang="ts">
-	import { onMount } from 'svelte';
+	import { onMount, getContext } from 'svelte';
+	const i18n = getContext('i18n');
+
 	import panzoom from 'panzoom';
 
 	import DOMPurify from 'dompurify';
+	import DocumentDuplicate from '../icons/DocumentDuplicate.svelte';
+	import { copyToClipboard } from '$lib/utils';
+	import { toast } from 'svelte-sonner';
+	import Tooltip from './Tooltip.svelte';
+	import Clipboard from '../icons/Clipboard.svelte';
 
 	export let className = '';
 	export let svg = '';
+	export let content = '';
 
 	let instance;
 
@@ -22,8 +30,24 @@
 	}
 </script>
 
-<div bind:this={sceneParentElement} class={className}>
+<div bind:this={sceneParentElement} class="relative {className}">
 	<div bind:this={sceneElement} class="flex h-full max-h-full justify-center items-center">
 		{@html svg}
 	</div>
+
+	{#if content}
+		<div class=" absolute top-1 right-1">
+			<Tooltip content={$i18n.t('Copy to clipboard')}>
+				<button
+					class="p-1.5 rounded-lg border border-gray-100 dark:border-none dark:bg-gray-850 hover:bg-gray-50 dark:hover:bg-gray-800 transition"
+					on:click={() => {
+						copyToClipboard(content);
+						toast.success($i18n.t('Copied to clipboard'));
+					}}
+				>
+					<Clipboard className=" size-4" strokeWidth="1.5" />
+				</button>
+			</Tooltip>
+		</div>
+	{/if}
 </div>

+ 0 - 166
src/lib/components/documents/AddDocModal.svelte

@@ -1,166 +0,0 @@
-<script lang="ts">
-	import { toast } from 'svelte-sonner';
-	import dayjs from 'dayjs';
-	import { onMount, getContext } from 'svelte';
-
-	import { getDocs } from '$lib/apis/documents';
-	import Modal from '../common/Modal.svelte';
-	import { documents } from '$lib/stores';
-	import { SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPE } from '$lib/constants';
-
-	import Tags from '../common/Tags.svelte';
-
-	const i18n = getContext('i18n');
-
-	export let show = false;
-	export let uploadDoc: Function;
-	let uploadDocInputElement: HTMLInputElement;
-	let inputFiles;
-	let tags = [];
-
-	let doc = {
-		name: '',
-		title: '',
-		content: null
-	};
-
-	const submitHandler = async () => {
-		if (inputFiles && inputFiles.length > 0) {
-			for (const file of inputFiles) {
-				console.log(file, file.name.split('.').at(-1));
-				if (
-					SUPPORTED_FILE_TYPE.includes(file['type']) ||
-					SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
-				) {
-					uploadDoc(file, tags);
-				} else {
-					toast.error(
-						`Unknown File Type '${file['type']}', but accepting and treating as plain text`
-					);
-					uploadDoc(file, tags);
-				}
-			}
-
-			inputFiles = null;
-			uploadDocInputElement.value = '';
-		} else {
-			toast.error($i18n.t(`File not found.`));
-		}
-
-		show = false;
-		documents.set(await getDocs(localStorage.token));
-	};
-
-	const addTagHandler = async (tagName) => {
-		if (!tags.find((tag) => tag.name === tagName) && tagName !== '') {
-			tags = [...tags, { name: tagName }];
-		} else {
-			console.log('tag already exists');
-		}
-	};
-
-	const deleteTagHandler = async (tagName) => {
-		tags = tags.filter((tag) => tag.name !== tagName);
-	};
-
-	onMount(() => {});
-</script>
-
-<Modal size="sm" bind:show>
-	<div>
-		<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
-			<div class=" text-lg font-medium self-center">{$i18n.t('Add Docs')}</div>
-			<button
-				class="self-center"
-				on:click={() => {
-					show = false;
-				}}
-			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<path
-						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
-					/>
-				</svg>
-			</button>
-		</div>
-		<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
-			<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
-				<form
-					class="flex flex-col w-full"
-					on:submit|preventDefault={() => {
-						submitHandler();
-					}}
-				>
-					<div class="mb-3 w-full">
-						<input
-							id="upload-doc-input"
-							bind:this={uploadDocInputElement}
-							hidden
-							bind:files={inputFiles}
-							type="file"
-							multiple
-						/>
-
-						<button
-							class="w-full text-sm font-medium py-3 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 text-center rounded-xl"
-							type="button"
-							on:click={() => {
-								uploadDocInputElement.click();
-							}}
-						>
-							{#if inputFiles}
-								{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
-							{:else}
-								{$i18n.t('Click here to select documents.')}
-							{/if}
-						</button>
-					</div>
-
-					<div class=" flex flex-col space-y-1.5">
-						<div class="flex flex-col w-full">
-							<div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div>
-
-							<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
-						</div>
-					</div>
-
-					<div class="flex justify-end pt-5 text-sm font-medium">
-						<button
-							class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
-							type="submit"
-						>
-							{$i18n.t('Save')}
-						</button>
-					</div>
-				</form>
-			</div>
-		</div>
-	</div>
-</Modal>
-
-<style>
-	input::-webkit-outer-spin-button,
-	input::-webkit-inner-spin-button {
-		/* display: none; <- Crashes Chrome on hover */
-		-webkit-appearance: none;
-		margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
-	}
-
-	.tabs::-webkit-scrollbar {
-		display: none; /* for Chrome, Safari and Opera */
-	}
-
-	.tabs {
-		-ms-overflow-style: none; /* IE and Edge */
-		scrollbar-width: none; /* Firefox */
-	}
-
-	input[type='number'] {
-		-moz-appearance: textfield; /* Firefox */
-	}
-</style>

+ 0 - 181
src/lib/components/documents/EditDocModal.svelte

@@ -1,181 +0,0 @@
-<script lang="ts">
-	import { toast } from 'svelte-sonner';
-	import dayjs from 'dayjs';
-	import { onMount, getContext } from 'svelte';
-
-	import { getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
-	import Modal from '../common/Modal.svelte';
-	import { documents } from '$lib/stores';
-	import TagInput from '../common/Tags/TagInput.svelte';
-	import Tags from '../common/Tags.svelte';
-	import { addTagById } from '$lib/apis/chats';
-
-	const i18n = getContext('i18n');
-
-	export let show = false;
-	export let selectedDoc;
-
-	let tags = [];
-
-	let doc = {
-		name: '',
-		title: '',
-		content: null
-	};
-
-	const submitHandler = async () => {
-		const res = await updateDocByName(localStorage.token, selectedDoc.name, {
-			title: doc.title,
-			name: doc.name
-		}).catch((error) => {
-			toast.error(error);
-		});
-
-		if (res) {
-			show = false;
-
-			documents.set(await getDocs(localStorage.token));
-		}
-	};
-
-	const addTagHandler = async (tagName) => {
-		if (!tags.find((tag) => tag.name === tagName) && tagName !== '') {
-			tags = [...tags, { name: tagName }];
-
-			await tagDocByName(localStorage.token, doc.name, {
-				name: doc.name,
-				tags: tags
-			});
-
-			documents.set(await getDocs(localStorage.token));
-		} else {
-			console.log('tag already exists');
-		}
-	};
-
-	const deleteTagHandler = async (tagName) => {
-		tags = tags.filter((tag) => tag.name !== tagName);
-
-		await tagDocByName(localStorage.token, doc.name, {
-			name: doc.name,
-			tags: tags
-		});
-
-		documents.set(await getDocs(localStorage.token));
-	};
-
-	onMount(() => {
-		if (selectedDoc) {
-			doc = JSON.parse(JSON.stringify(selectedDoc));
-
-			tags = doc?.content?.tags ?? [];
-		}
-	});
-</script>
-
-<Modal size="sm" bind:show>
-	<div>
-		<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
-			<div class=" text-lg font-medium self-center">{$i18n.t('Edit Doc')}</div>
-			<button
-				class="self-center"
-				on:click={() => {
-					show = false;
-				}}
-			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<path
-						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
-					/>
-				</svg>
-			</button>
-		</div>
-		<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
-			<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
-				<form
-					class="flex flex-col w-full"
-					on:submit|preventDefault={() => {
-						submitHandler();
-					}}
-				>
-					<div class=" flex flex-col space-y-1.5">
-						<div class="flex flex-col w-full">
-							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name Tag')}</div>
-
-							<div class="flex flex-1">
-								<div
-									class="bg-gray-200 dark:bg-gray-800 font-semibold px-3 py-0.5 border border-r-0 dark:border-gray-800 rounded-l-xl flex items-center"
-								>
-									#
-								</div>
-								<input
-									class="w-full rounded-r-xl py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
-									type="text"
-									bind:value={doc.name}
-									autocomplete="off"
-									required
-								/>
-							</div>
-						</div>
-
-						<div class="flex flex-col w-full">
-							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Title')}</div>
-
-							<div class="flex-1">
-								<input
-									class="w-full rounded-xl py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
-									type="text"
-									bind:value={doc.title}
-									autocomplete="off"
-									required
-								/>
-							</div>
-						</div>
-
-						<div class="flex flex-col w-full">
-							<div class=" mb-2 text-xs text-gray-500">{$i18n.t('Tags')}</div>
-
-							<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
-						</div>
-					</div>
-
-					<div class="flex justify-end pt-5 text-sm font-medium">
-						<button
-							class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
-							type="submit"
-						>
-							{$i18n.t('Save')}
-						</button>
-					</div>
-				</form>
-			</div>
-		</div>
-	</div>
-</Modal>
-
-<style>
-	input::-webkit-outer-spin-button,
-	input::-webkit-inner-spin-button {
-		/* display: none; <- Crashes Chrome on hover */
-		-webkit-appearance: none;
-		margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
-	}
-
-	.tabs::-webkit-scrollbar {
-		display: none; /* for Chrome, Safari and Opera */
-	}
-
-	.tabs {
-		-ms-overflow-style: none; /* IE and Edge */
-		scrollbar-width: none; /* Firefox */
-	}
-
-	input[type='number'] {
-		-moz-appearance: textfield; /* Firefox */
-	}
-</style>

+ 15 - 0
src/lib/components/icons/ArrowLeft.svelte

@@ -0,0 +1,15 @@
+<script lang="ts">
+	export let className = 'size-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+>
+	<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
+</svg>

+ 3 - 2
src/lib/components/layout/Navbar.svelte

@@ -10,6 +10,7 @@
 		showArchivedChats,
 		showControls,
 		showSidebar,
+		temporaryChatEnabled,
 		user
 	} from '$lib/stores';
 
@@ -23,6 +24,7 @@
 	import MenuLines from '../icons/MenuLines.svelte';
 	import AdjustmentsHorizontal from '../icons/AdjustmentsHorizontal.svelte';
 	import Map from '../icons/Map.svelte';
+	import { stringify } from 'postcss';
 
 	const i18n = getContext('i18n');
 
@@ -74,8 +76,7 @@
 
 			<div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400">
 				<!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
-
-				{#if shareEnabled && chat && chat.id}
+				{#if shareEnabled && chat && (chat.id || $temporaryChatEnabled)}
 					<Menu
 						{chat}
 						{shareEnabled}

+ 45 - 35
src/lib/components/layout/Navbar/Menu.svelte

@@ -9,7 +9,13 @@
 	import { downloadChatAsPDF } from '$lib/apis/utils';
 	import { copyToClipboard, createMessagesList } from '$lib/utils';
 
-	import { showOverview, showControls, showArtifacts, mobile } from '$lib/stores';
+	import {
+		showOverview,
+		showControls,
+		showArtifacts,
+		mobile,
+		temporaryChatEnabled
+	} from '$lib/stores';
 	import { flyAndScale } from '$lib/utils/transitions';
 
 	import Dropdown from '$lib/components/common/Dropdown.svelte';
@@ -18,6 +24,7 @@
 	import Clipboard from '$lib/components/icons/Clipboard.svelte';
 	import AdjustmentsHorizontal from '$lib/components/icons/AdjustmentsHorizontal.svelte';
 	import Cube from '$lib/components/icons/Cube.svelte';
+	import { getChatById } from '$lib/apis/chats';
 
 	const i18n = getContext('i18n');
 
@@ -31,9 +38,8 @@
 	export let onClose: Function = () => {};
 
 	const getChatAsText = async () => {
-		const _chat = chat.chat;
-
-		const messages = createMessagesList(_chat.history, _chat.history.currentId);
+		const history = chat.chat.history;
+		const messages = createMessagesList(history, history.currentId);
 		const chatText = messages.reduce((a, message, i, arr) => {
 			return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
 		}, '');
@@ -52,12 +58,9 @@
 	};
 
 	const downloadPdf = async () => {
-		const _chat = chat.chat;
-		const messages = createMessagesList(_chat.history, _chat.history.currentId);
-
-		console.log('download', chat);
-
-		const blob = await downloadChatAsPDF(_chat.title, messages);
+		const history = chat.chat.history;
+		const messages = createMessagesList(history, history.currentId);
+		const blob = await downloadChatAsPDF(chat.chat.title, messages);
 
 		// Create a URL for the blob
 		const url = window.URL.createObjectURL(blob);
@@ -65,7 +68,7 @@
 		// Create a link element to trigger the download
 		const a = document.createElement('a');
 		a.href = url;
-		a.download = `chat-${_chat.title}.pdf`;
+		a.download = `chat-${chat.chat.title}.pdf`;
 
 		// Append the link to the body and click it programmatically
 		document.body.appendChild(a);
@@ -79,6 +82,9 @@
 	};
 
 	const downloadJSONExport = async () => {
+		if (chat.id) {
+			chat = await getChatById(localStorage.token, chat.id);
+		}
 		let blob = new Blob([JSON.stringify([chat])], {
 			type: 'application/json'
 		});
@@ -189,27 +195,29 @@
 				<div class="flex items-center">{$i18n.t('Copy')}</div>
 			</DropdownMenu.Item>
 
-			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
-				id="chat-share-button"
-				on:click={() => {
-					shareHandler();
-				}}
-			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 24 24"
-					fill="currentColor"
-					class="size-4"
+			{#if !$temporaryChatEnabled}
+				<DropdownMenu.Item
+					class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+					id="chat-share-button"
+					on:click={() => {
+						shareHandler();
+					}}
 				>
-					<path
-						fill-rule="evenodd"
-						d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
-						clip-rule="evenodd"
-					/>
-				</svg>
-				<div class="flex items-center">{$i18n.t('Share')}</div>
-			</DropdownMenu.Item>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						viewBox="0 0 24 24"
+						fill="currentColor"
+						class="size-4"
+					>
+						<path
+							fill-rule="evenodd"
+							d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
+							clip-rule="evenodd"
+						/>
+					</svg>
+					<div class="flex items-center">{$i18n.t('Share')}</div>
+				</DropdownMenu.Item>
+			{/if}
 
 			<DropdownMenu.Sub>
 				<DropdownMenu.SubTrigger
@@ -265,11 +273,13 @@
 				</DropdownMenu.SubContent>
 			</DropdownMenu.Sub>
 
-			<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
+			{#if !$temporaryChatEnabled}
+				<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
 
-			<div class="flex p-1">
-				<Tags chatId={chat.id} />
-			</div>
+				<div class="flex p-1">
+					<Tags chatId={chat.id} />
+				</div>
+			{/if}
 		</DropdownMenu.Content>
 	</div>
 </Dropdown>

+ 275 - 178
src/lib/components/layout/Sidebar.svelte

@@ -19,7 +19,7 @@
 		showOverview,
 		showControls
 	} from '$lib/stores';
-	import { onMount, getContext, tick } from 'svelte';
+	import { onMount, getContext, tick, onDestroy } from 'svelte';
 
 	const i18n = getContext('i18n');
 
@@ -32,7 +32,10 @@
 		updateChatById,
 		getAllChatTags,
 		archiveChatById,
-		cloneChatById
+		cloneChatById,
+		getChatListBySearchText,
+		createNewChat,
+		getPinnedChatList
 	} from '$lib/apis/chats';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 
@@ -42,6 +45,9 @@
 	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import Spinner from '../common/Spinner.svelte';
 	import Loader from '../common/Loader.svelte';
+	import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
+	import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
+	import { select } from 'd3-selection';
 
 	const BREAKPOINT = 768;
 
@@ -58,33 +64,11 @@
 
 	let selectedTagName = null;
 
-	let filteredChatList = [];
-
 	// Pagination variables
 	let chatListLoading = false;
 	let allChatsLoaded = false;
 
-	$: filteredChatList = $chats.filter((chat) => {
-		if (search === '') {
-			return true;
-		} else {
-			let title = chat.title.toLowerCase();
-			const query = search.toLowerCase();
-
-			let contentMatches = false;
-			// Access the messages within chat.chat.messages
-			if (chat.chat && chat.chat.messages && Array.isArray(chat.chat.messages)) {
-				contentMatches = chat.chat.messages.some((message) => {
-					// Check if message.content exists and includes the search query
-					return message.content && message.content.toLowerCase().includes(query);
-				});
-			}
-
-			return title.includes(query) || contentMatches;
-		}
-	});
-
-	const enablePagination = async () => {
+	const initChatList = async () => {
 		// Reset pagination variables
 		currentChatPage.set(1);
 		allChatsLoaded = false;
@@ -98,7 +82,14 @@
 		chatListLoading = true;
 
 		currentChatPage.set($currentChatPage + 1);
-		const newChatList = await getChatList(localStorage.token, $currentChatPage);
+
+		let newChatList = [];
+
+		if (search) {
+			newChatList = await getChatListBySearchText(localStorage.token, search, $currentChatPage);
+		} else {
+			newChatList = await getChatList(localStorage.token, $currentChatPage);
+		}
 
 		// once the bottom of the list has been reached (no results) there is no need to continue querying
 		allChatsLoaded = newChatList.length === 0;
@@ -107,69 +98,181 @@
 		chatListLoading = false;
 	};
 
-	onMount(async () => {
-		mobile.subscribe((e) => {
-			if ($showSidebar && e) {
-				showSidebar.set(false);
-			}
+	let searchDebounceTimeout;
 
-			if (!$showSidebar && !e) {
-				showSidebar.set(true);
-			}
-		});
+	const searchDebounceHandler = async () => {
+		console.log('search', search);
+		chats.set(null);
+		selectedTagName = null;
 
-		showSidebar.set(!$mobile ? localStorage.sidebar === 'true' : false);
-		showSidebar.subscribe((value) => {
-			localStorage.sidebar = value;
+		if (searchDebounceTimeout) {
+			clearTimeout(searchDebounceTimeout);
+		}
+
+		if (search === '') {
+			await initChatList();
+			return;
+		} else {
+			searchDebounceTimeout = setTimeout(async () => {
+				currentChatPage.set(1);
+				await chats.set(await getChatListBySearchText(localStorage.token, search));
+			}, 1000);
+		}
+	};
+
+	const deleteChatHandler = async (id) => {
+		const res = await deleteChatById(localStorage.token, id).catch((error) => {
+			toast.error(error);
+			return null;
 		});
 
-		await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
-		await enablePagination();
+		if (res) {
+			if ($chatId === id) {
+				await chatId.set('');
+				await tick();
+				goto('/');
+			}
+
+			allChatsLoaded = false;
+			currentChatPage.set(1);
+			await chats.set(await getChatList(localStorage.token, $currentChatPage));
+
+			await pinnedChats.set(await getPinnedChatList(localStorage.token));
+		}
+	};
+
+	const inputFilesHandler = async (files) => {
+		console.log(files);
 
-		let touchstart;
-		let touchend;
+		for (const file of files) {
+			const reader = new FileReader();
+			reader.onload = async (e) => {
+				const content = e.target.result;
 
-		function checkDirection() {
-			const screenWidth = window.innerWidth;
-			const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX);
-			if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 8) {
-				if (touchend.screenX < touchstart.screenX) {
-					showSidebar.set(false);
+				try {
+					const items = JSON.parse(content);
+
+					for (const item of items) {
+						if (item.chat) {
+							await createNewChat(localStorage.token, item.chat);
+						}
+					}
+				} catch {
+					toast.error($i18n.t(`Invalid file format.`));
 				}
-				if (touchend.screenX > touchstart.screenX) {
-					showSidebar.set(true);
+
+				initChatList();
+			};
+
+			reader.readAsText(file);
+		}
+	};
+
+	const tagEventHandler = async (type, tagName, chatId) => {
+		console.log(type, tagName, chatId);
+		if (type === 'delete') {
+			if (selectedTagName === tagName) {
+				if ($tags.map((t) => t.name).includes(tagName)) {
+					await chats.set(await getChatListByTagName(localStorage.token, tagName));
+				} else {
+					selectedTagName = null;
+					await initChatList();
 				}
 			}
 		}
+	};
 
-		const onTouchStart = (e) => {
-			touchstart = e.changedTouches[0];
-			console.log(touchstart.clientX);
-		};
+	let dragged = false;
 
-		const onTouchEnd = (e) => {
-			touchend = e.changedTouches[0];
-			checkDirection();
-		};
+	const onDragOver = (e) => {
+		e.preventDefault();
+		dragged = true;
+	};
 
-		const onKeyDown = (e) => {
-			if (e.key === 'Shift') {
-				shiftKey = true;
+	const onDragLeave = () => {
+		dragged = false;
+	};
+
+	const onDrop = async (e) => {
+		e.preventDefault();
+		console.log(e);
+
+		if (e.dataTransfer?.files) {
+			const inputFiles = Array.from(e.dataTransfer?.files);
+			if (inputFiles && inputFiles.length > 0) {
+				console.log(inputFiles);
+				inputFilesHandler(inputFiles);
+			} else {
+				toast.error($i18n.t(`File not found.`));
 			}
-		};
+		}
+
+		dragged = false;
+	};
+
+	let touchstart;
+	let touchend;
 
-		const onKeyUp = (e) => {
-			if (e.key === 'Shift') {
-				shiftKey = false;
+	function checkDirection() {
+		const screenWidth = window.innerWidth;
+		const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX);
+		if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 8) {
+			if (touchend.screenX < touchstart.screenX) {
+				showSidebar.set(false);
+			}
+			if (touchend.screenX > touchstart.screenX) {
+				showSidebar.set(true);
 			}
-		};
+		}
+	}
 
-		const onFocus = () => {};
+	const onTouchStart = (e) => {
+		touchstart = e.changedTouches[0];
+		console.log(touchstart.clientX);
+	};
+
+	const onTouchEnd = (e) => {
+		touchend = e.changedTouches[0];
+		checkDirection();
+	};
 
-		const onBlur = () => {
+	const onKeyDown = (e) => {
+		if (e.key === 'Shift') {
+			shiftKey = true;
+		}
+	};
+
+	const onKeyUp = (e) => {
+		if (e.key === 'Shift') {
 			shiftKey = false;
-			selectedChatId = null;
-		};
+		}
+	};
+
+	const onFocus = () => {};
+
+	const onBlur = () => {
+		shiftKey = false;
+		selectedChatId = null;
+	};
+
+	onMount(async () => {
+		mobile.subscribe((e) => {
+			if ($showSidebar && e) {
+				showSidebar.set(false);
+			}
+
+			if (!$showSidebar && !e) {
+				showSidebar.set(true);
+			}
+		});
+
+		showSidebar.set(!$mobile ? localStorage.sidebar === 'true' : false);
+		showSidebar.subscribe((value) => {
+			localStorage.sidebar = value;
+		});
+
+		await pinnedChats.set(await getPinnedChatList(localStorage.token));
+		await initChatList();
 
 		window.addEventListener('keydown', onKeyDown);
 		window.addEventListener('keyup', onKeyUp);
@@ -180,59 +283,29 @@
 		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);
+		const dropZone = document.getElementById('sidebar');
 
-			window.removeEventListener('focus', onFocus);
-			window.removeEventListener('blur', onBlur);
-		};
+		dropZone?.addEventListener('dragover', onDragOver);
+		dropZone?.addEventListener('drop', onDrop);
+		dropZone?.addEventListener('dragleave', onDragLeave);
 	});
 
-	// Helper function to fetch and add chat content to each chat
-	const enrichChatsWithContent = async (chatList) => {
-		const enrichedChats = await Promise.all(
-			chatList.map(async (chat) => {
-				const chatDetails = await getChatById(localStorage.token, chat.id).catch((error) => null); // Handle error or non-existent chat gracefully
-				if (chatDetails) {
-					chat.chat = chatDetails.chat; // Assuming chatDetails.chat contains the chat content
-				}
-				return chat;
-			})
-		);
+	onDestroy(() => {
+		window.removeEventListener('keydown', onKeyDown);
+		window.removeEventListener('keyup', onKeyUp);
 
-		await chats.set(enrichedChats);
-	};
+		window.removeEventListener('touchstart', onTouchStart);
+		window.removeEventListener('touchend', onTouchEnd);
 
-	const saveSettings = async (updated) => {
-		await settings.set({ ...$settings, ...updated });
-		await updateUserSettings(localStorage.token, { ui: $settings });
-		location.href = '/';
-	};
-
-	const deleteChatHandler = async (id) => {
-		const res = await deleteChatById(localStorage.token, id).catch((error) => {
-			toast.error(error);
-			return null;
-		});
+		window.removeEventListener('focus', onFocus);
+		window.removeEventListener('blur', onBlur);
 
-		if (res) {
-			if ($chatId === id) {
-				await chatId.set('');
-				await tick();
-				goto('/');
-			}
-
-			allChatsLoaded = false;
-			currentChatPage.set(1);
-			await chats.set(await getChatList(localStorage.token, $currentChatPage));
+		const dropZone = document.getElementById('sidebar');
 
-			await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
-		}
-	};
+		dropZone?.removeEventListener('dragover', onDragOver);
+		dropZone?.removeEventListener('drop', onDrop);
+		dropZone?.removeEventListener('dragleave', onDragLeave);
+	});
 </script>
 
 <ArchivedChatsModal
@@ -274,6 +347,18 @@
         "
 	data-state={$showSidebar}
 >
+	{#if dragged}
+		<div
+			class="absolute w-full h-full max-h-full backdrop-blur bg-gray-800/40 flex justify-center z-[999] touch-none pointer-events-none"
+		>
+			<div class="m-auto pt-64 flex flex-col justify-center">
+				<AddFilesPlaceholder
+					title={$i18n.t('Drop Chat Export')}
+					content={$i18n.t('Drop a chat export file here to import it.')}
+				/>
+			</div>
+		</div>
+	{/if}
 	<div
 		class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] z-50 {$showSidebar
 			? ''
@@ -419,30 +504,27 @@
 						class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm bg-transparent dark:text-gray-300 outline-none"
 						placeholder={$i18n.t('Search')}
 						bind:value={search}
-						on:focus={async () => {
-							// TODO: migrate backend for more scalable search mechanism
-							scrollPaginationEnabled.set(false);
-							await chats.set(await getChatList(localStorage.token)); // when searching, load all chats
-							enrichChatsWithContent($chats);
+						on:input={() => {
+							searchDebounceHandler();
 						}}
 					/>
 				</div>
 			</div>
 
-			{#if $tags.filter((t) => t.name !== 'pinned').length > 0}
-				<div class="px-3.5 mb-1 flex gap-0.5 flex-wrap">
+			{#if $tags.length > 0}
+				<div class="px-3.5 mb-2.5 flex gap-0.5 flex-wrap">
 					<button
 						class="px-2.5 py-[1px] text-xs transition {selectedTagName === null
 							? 'bg-gray-100 dark:bg-gray-900'
 							: ' '} rounded-md font-medium"
 						on:click={async () => {
 							selectedTagName = null;
-							await enablePagination();
+							await initChatList();
 						}}
 					>
 						{$i18n.t('all')}
 					</button>
-					{#each $tags.filter((t) => t.name !== 'pinned') as tag}
+					{#each $tags as tag}
 						<button
 							class="px-2.5 py-[1px] text-xs transition {selectedTagName === tag.name
 								? 'bg-gray-100 dark:bg-gray-900'
@@ -450,15 +532,15 @@
 							on:click={async () => {
 								selectedTagName = tag.name;
 								scrollPaginationEnabled.set(false);
-								let chatIds = await getChatListByTagName(localStorage.token, tag.name);
-								if (chatIds.length === 0) {
-									await tags.set(await getAllChatTags(localStorage.token));
 
+								let taggedChatList = await getChatListByTagName(localStorage.token, tag.name);
+								if (taggedChatList.length === 0) {
+									await tags.set(await getAllChatTags(localStorage.token));
 									// if the tag we deleted is no longer a valid tag, return to main chat list view
-									await enablePagination();
+									await initChatList();
+								} else {
+									await chats.set(taggedChatList);
 								}
-								await chats.set(chatIds);
-
 								chatListLoading = false;
 							}}
 						>
@@ -469,7 +551,7 @@
 			{/if}
 
 			{#if !search && $pinnedChats.length > 0}
-				<div class="pl-2 py-2 flex flex-col space-y-1">
+				<div class="pl-2 pb-2 flex flex-col space-y-1">
 					<div class="">
 						<div class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium pb-1.5">
 							{$i18n.t('Pinned')}
@@ -494,22 +576,27 @@
 										showDeleteConfirm = true;
 									}
 								}}
+								on:tag={(e) => {
+									const { type, name } = e.detail;
+									tagEventHandler(type, name, chat.id);
+								}}
 							/>
 						{/each}
 					</div>
 				</div>
 			{/if}
 
-			<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden">
-				{#each filteredChatList as chat, idx}
-					{#if idx === 0 || (idx > 0 && chat.time_range !== filteredChatList[idx - 1].time_range)}
-						<div
-							class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
-								? ''
-								: 'pt-5'} pb-0.5"
-						>
-							{$i18n.t(chat.time_range)}
-							<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
+			<div class="pl-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden">
+				{#if $chats}
+					{#each $chats as chat, idx}
+						{#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
+							<div
+								class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
+									? ''
+									: 'pt-5'} pb-0.5"
+							>
+								{$i18n.t(chat.time_range)}
+								<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
 							{$i18n.t('Today')}
 							{$i18n.t('Yesterday')}
 							{$i18n.t('Previous 7 days')}
@@ -527,43 +614,53 @@
 							{$i18n.t('November')}
 							{$i18n.t('December')}
 							-->
-						</div>
-					{/if}
-
-					<ChatItem
-						{chat}
-						{shiftKey}
-						selected={selectedChatId === chat.id}
-						on:select={() => {
-							selectedChatId = chat.id;
-						}}
-						on:unselect={() => {
-							selectedChatId = null;
-						}}
-						on:delete={(e) => {
-							if ((e?.detail ?? '') === 'shift') {
-								deleteChatHandler(chat.id);
-							} else {
-								deleteChat = chat;
-								showDeleteConfirm = true;
-							}
-						}}
-					/>
-				{/each}
+							</div>
+						{/if}
+
+						<ChatItem
+							{chat}
+							{shiftKey}
+							selected={selectedChatId === chat.id}
+							on:select={() => {
+								selectedChatId = chat.id;
+							}}
+							on:unselect={() => {
+								selectedChatId = null;
+							}}
+							on:delete={(e) => {
+								if ((e?.detail ?? '') === 'shift') {
+									deleteChatHandler(chat.id);
+								} else {
+									deleteChat = chat;
+									showDeleteConfirm = true;
+								}
+							}}
+							on:tag={(e) => {
+								const { type, name } = e.detail;
+								tagEventHandler(type, name, chat.id);
+							}}
+						/>
+					{/each}
 
-				{#if $scrollPaginationEnabled && !allChatsLoaded}
-					<Loader
-						on:visible={(e) => {
-							if (!chatListLoading) {
-								loadMoreChats();
-							}
-						}}
-					>
-						<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
-							<Spinner className=" size-4" />
-							<div class=" ">Loading...</div>
-						</div>
-					</Loader>
+					{#if $scrollPaginationEnabled && !allChatsLoaded}
+						<Loader
+							on:visible={(e) => {
+								if (!chatListLoading) {
+									loadMoreChats();
+								}
+							}}
+						>
+							<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
+								<Spinner className=" size-4" />
+								<div class=" ">Loading...</div>
+							</div>
+						</Loader>
+					{/if}
+				{:else}
+					<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
+						<Spinner className=" size-4" />
+						<div class=" ">Loading...</div>
+					</div>
 				{/if}
 			</div>
 		</div>

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

@@ -12,6 +12,7 @@
 		deleteChatById,
 		getChatList,
 		getChatListByTagName,
+		getPinnedChatList,
 		updateChatById
 	} from '$lib/apis/chats';
 	import {
@@ -55,7 +56,7 @@
 
 			currentChatPage.set(1);
 			await chats.set(await getChatList(localStorage.token, $currentChatPage));
-			await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
+			await pinnedChats.set(await getPinnedChatList(localStorage.token));
 		}
 	};
 
@@ -70,7 +71,7 @@
 
 			currentChatPage.set(1);
 			await chats.set(await getChatList(localStorage.token, $currentChatPage));
-			await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
+			await pinnedChats.set(await getPinnedChatList(localStorage.token));
 		}
 	};
 
@@ -79,7 +80,7 @@
 
 		currentChatPage.set(1);
 		await chats.set(await getChatList(localStorage.token, $currentChatPage));
-		await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
+		await pinnedChats.set(await getPinnedChatList(localStorage.token));
 	};
 
 	const focusEdit = async (node: HTMLInputElement) => {
@@ -256,7 +257,10 @@
 						dispatch('unselect');
 					}}
 					on:change={async () => {
-						await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
+						await pinnedChats.set(await getPinnedChatList(localStorage.token));
+					}}
+					on:tag={(e) => {
+						dispatch('tag', e.detail);
 					}}
 				>
 					<button

+ 16 - 11
src/lib/components/layout/Sidebar/ChatMenu.svelte

@@ -15,7 +15,13 @@
 	import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
 	import Bookmark from '$lib/components/icons/Bookmark.svelte';
 	import BookmarkSlash from '$lib/components/icons/BookmarkSlash.svelte';
-	import { addTagById, deleteTagById, getTagsById } from '$lib/apis/chats';
+	import {
+		addTagById,
+		deleteTagById,
+		getChatPinnedStatusById,
+		getTagsById,
+		toggleChatPinnedStatusById
+	} from '$lib/apis/chats';
 
 	const i18n = getContext('i18n');
 
@@ -32,20 +38,12 @@
 	let pinned = false;
 
 	const pinHandler = async () => {
-		if (pinned) {
-			await deleteTagById(localStorage.token, chatId, 'pinned');
-		} else {
-			await addTagById(localStorage.token, chatId, 'pinned');
-		}
+		await toggleChatPinnedStatusById(localStorage.token, chatId);
 		dispatch('change');
 	};
 
 	const checkPinned = async () => {
-		pinned = (
-			await getTagsById(localStorage.token, chatId).catch(async (error) => {
-				return [];
-			})
-		).find((tag) => tag.name === 'pinned');
+		pinned = await getChatPinnedStatusById(localStorage.token, chatId);
 	};
 
 	$: if (show) {
@@ -143,6 +141,13 @@
 			<div class="flex p-1">
 				<Tags
 					{chatId}
+					on:delete={(e) => {
+						dispatch('tag', {
+							type: 'delete',
+							name: e.detail.name
+						});
+						show = false;
+					}}
 					on:close={() => {
 						show = false;
 						onClose();

+ 0 - 627
src/lib/components/workspace/Documents.svelte

@@ -1,627 +0,0 @@
-<script lang="ts">
-	import { toast } from 'svelte-sonner';
-	import fileSaver from 'file-saver';
-	const { saveAs } = fileSaver;
-
-	import { onMount, getContext } from 'svelte';
-	import { WEBUI_NAME, documents, showSidebar } from '$lib/stores';
-	import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
-
-	import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
-	import { processFile } from '$lib/apis/retrieval';
-	import { blobToFile, transformFileName } from '$lib/utils';
-
-	import Checkbox from '$lib/components/common/Checkbox.svelte';
-
-	import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
-	import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
-	import AddDocModal from '$lib/components/documents/AddDocModal.svelte';
-	import { transcribeAudio } from '$lib/apis/audio';
-	import { uploadFile } from '$lib/apis/files';
-
-	const i18n = getContext('i18n');
-
-	let importFiles = '';
-
-	let inputFiles = '';
-
-	let query = '';
-	let documentsImportInputElement: HTMLInputElement;
-	let tags = [];
-
-	let showSettingsModal = false;
-	let showAddDocModal = false;
-	let showEditDocModal = false;
-	let selectedDoc;
-	let selectedTag = '';
-
-	let dragged = false;
-
-	const deleteDoc = async (name) => {
-		await deleteDocByName(localStorage.token, name);
-		await documents.set(await getDocs(localStorage.token));
-	};
-
-	const deleteDocs = async (docs) => {
-		const res = await Promise.all(
-			docs.map(async (doc) => {
-				return await deleteDocByName(localStorage.token, doc.name);
-			})
-		);
-
-		await documents.set(await getDocs(localStorage.token));
-	};
-
-	const uploadDoc = async (file, tags?: object) => {
-		console.log(file);
-		// Check if the file is an audio file and transcribe/convert it to text file
-		if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
-			const transcribeRes = await transcribeAudio(localStorage.token, file).catch((error) => {
-				toast.error(error);
-				return null;
-			});
-
-			if (transcribeRes) {
-				console.log(transcribeRes);
-				const blob = new Blob([transcribeRes.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;
-		});
-
-		const res = await processFile(localStorage.token, uploadedFile.id).catch((error) => {
-			toast.error(error);
-			return null;
-		});
-
-		if (res) {
-			await createNewDoc(
-				localStorage.token,
-				res.collection_name,
-				res.filename,
-				transformFileName(res.filename),
-				res.filename,
-				tags?.length > 0
-					? {
-							tags: tags
-						}
-					: null
-			).catch((error) => {
-				toast.error(error);
-				return null;
-			});
-			await documents.set(await getDocs(localStorage.token));
-		}
-	};
-
-	onMount(() => {
-		documents.subscribe((docs) => {
-			tags = docs.reduce((a, e, i, arr) => {
-				return [...new Set([...a, ...(e?.content?.tags ?? []).map((tag) => tag.name)])];
-			}, []);
-		});
-		const dropZone = document.querySelector('body');
-
-		const onDragOver = (e) => {
-			e.preventDefault();
-			dragged = true;
-		};
-
-		const onDragLeave = () => {
-			dragged = false;
-		};
-
-		const onDrop = async (e) => {
-			e.preventDefault();
-
-			if (e.dataTransfer?.files) {
-				let reader = new FileReader();
-
-				reader.onload = (event) => {
-					files = [
-						...files,
-						{
-							type: 'image',
-							url: `${event.target.result}`
-						}
-					];
-				};
-
-				const inputFiles = e.dataTransfer?.files;
-
-				if (inputFiles && inputFiles.length > 0) {
-					for (const file of inputFiles) {
-						console.log(file, file.name.split('.').at(-1));
-						if (
-							SUPPORTED_FILE_TYPE.includes(file['type']) ||
-							SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
-						) {
-							uploadDoc(file);
-						} else {
-							toast.error(
-								`Unknown File Type '${file['type']}', but accepting and treating as plain text`
-							);
-							uploadDoc(file);
-						}
-					}
-				} else {
-					toast.error($i18n.t(`File not found.`));
-				}
-			}
-
-			dragged = false;
-		};
-
-		dropZone?.addEventListener('dragover', onDragOver);
-		dropZone?.addEventListener('drop', onDrop);
-		dropZone?.addEventListener('dragleave', onDragLeave);
-
-		return () => {
-			dropZone?.removeEventListener('dragover', onDragOver);
-			dropZone?.removeEventListener('drop', onDrop);
-			dropZone?.removeEventListener('dragleave', onDragLeave);
-		};
-	});
-
-	let filteredDocs;
-
-	$: filteredDocs = $documents.filter(
-		(doc) =>
-			(selectedTag === '' ||
-				(doc?.content?.tags ?? []).map((tag) => tag.name).includes(selectedTag)) &&
-			(query === '' || doc.name.includes(query))
-	);
-</script>
-
-<svelte:head>
-	<title>
-		{$i18n.t('Documents')} | {$WEBUI_NAME}
-	</title>
-</svelte:head>
-
-{#if dragged}
-	<div
-		class="fixed {$showSidebar
-			? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
-			: 'left-0'}  w-full h-full flex z-50 touch-none pointer-events-none"
-		id="dropzone"
-		role="region"
-		aria-label="Drag and Drop Container"
-	>
-		<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
-			<div class="m-auto pt-64 flex flex-col justify-center">
-				<div class="max-w-md">
-					<AddFilesPlaceholder>
-						<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
-							Drop any files here to add to my documents
-						</div>
-					</AddFilesPlaceholder>
-				</div>
-			</div>
-		</div>
-	</div>
-{/if}
-
-{#key selectedDoc}
-	<EditDocModal bind:show={showEditDocModal} {selectedDoc} />
-{/key}
-
-<AddDocModal bind:show={showAddDocModal} {uploadDoc} />
-
-<div class="mb-3">
-	<div class="flex justify-between items-center">
-		<div class="flex md:self-center text-lg font-medium px-0.5">
-			{$i18n.t('Documents')}
-			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
-			<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{$documents.length}</span>
-		</div>
-	</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 Documents')}
-		/>
-	</div>
-
-	<div>
-		<button
-			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"
-			aria-label={$i18n.t('Add Docs')}
-			on:click={() => {
-				showAddDocModal = true;
-			}}
-		>
-			<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>
-		</button>
-	</div>
-</div>
-
-<!-- <div>
-    <div
-        class="my-3 py-16 rounded-lg border-2 border-dashed dark:border-gray-600 {dragged &&
-            ' dark:bg-gray-700'} "
-        role="region"
-        on:drop={onDrop}
-        on:dragover={onDragOver}
-        on:dragleave={onDragLeave}
-    >
-        <div class="  pointer-events-none">
-            <div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div>
-
-            <div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
-                Drop any files here to add to my documents
-            </div>
-        </div>
-    </div>
-</div> -->
-
-<hr class=" dark:border-gray-850 my-2.5" />
-
-{#if tags.length > 0}
-	<div class="px-2.5 pt-1 flex gap-1 flex-wrap">
-		<div class="ml-0.5 pr-3 my-auto flex items-center">
-			<Checkbox
-				state={filteredDocs.filter((doc) => doc?.selected === 'checked').length ===
-				filteredDocs.length
-					? 'checked'
-					: 'unchecked'}
-				indeterminate={filteredDocs.filter((doc) => doc?.selected === 'checked').length > 0 &&
-					filteredDocs.filter((doc) => doc?.selected === 'checked').length !== filteredDocs.length}
-				on:change={(e) => {
-					if (e.detail === 'checked') {
-						filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'checked' }));
-					} else if (e.detail === 'unchecked') {
-						filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'unchecked' }));
-					}
-				}}
-			/>
-		</div>
-
-		{#if filteredDocs.filter((doc) => doc?.selected === 'checked').length === 0}
-			<button
-				class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
-				on:click={async () => {
-					selectedTag = '';
-					// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
-				}}
-			>
-				<div class=" text-xs font-medium self-center line-clamp-1">{$i18n.t('all')}</div>
-			</button>
-
-			{#each tags as tag}
-				<button
-					class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
-					on:click={async () => {
-						selectedTag = tag;
-						// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
-					}}
-				>
-					<div class=" text-xs font-medium self-center line-clamp-1">
-						#{tag}
-					</div>
-				</button>
-			{/each}
-		{:else}
-			<div class="flex-1 flex w-full justify-between items-center">
-				<div class="text-xs font-medium py-0.5 self-center mr-1">
-					{filteredDocs.filter((doc) => doc?.selected === 'checked').length} Selected
-				</div>
-
-				<div class="flex gap-1">
-					<!-- <button
-                        class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
-                        on:click={async () => {
-                            selectedTag = '';
-                            // await chats.set(await getChatListByTagName(localStorage.token, tag.name));
-                        }}
-                    >
-                        <div class=" text-xs font-medium self-center line-clamp-1">add tags</div>
-                    </button> -->
-
-					<button
-						class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
-						on:click={async () => {
-							deleteDocs(filteredDocs.filter((doc) => doc.selected === 'checked'));
-							// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
-						}}
-					>
-						<div class=" text-xs font-medium self-center line-clamp-1">
-							{$i18n.t('delete')}
-						</div>
-					</button>
-				</div>
-			</div>
-		{/if}
-	</div>
-{/if}
-
-<div class="my-3 mb-5">
-	{#each filteredDocs as doc}
-		<button
-			class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
-			on:click={() => {
-				if (doc?.selected === 'checked') {
-					doc.selected = 'unchecked';
-				} else {
-					doc.selected = 'checked';
-				}
-			}}
-		>
-			<div class="my-auto flex items-center">
-				<Checkbox state={doc?.selected ?? 'unchecked'} />
-			</div>
-			<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
-				<div class=" flex items-center space-x-3">
-					<div class="p-2.5 bg-red-400 text-white rounded-lg">
-						{#if doc}
-							<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>
-						{:else}
-							<svg
-								class=" w-6 h-6 translate-y-[0.5px]"
-								fill="currentColor"
-								viewBox="0 0 24 24"
-								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);
-										}
-										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="2.5" /><circle
-									class="spinner_qM83 spinner_oXPr"
-									cx="12"
-									cy="12"
-									r="2.5"
-								/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
-							>
-						{/if}
-					</div>
-					<div class=" self-center flex-1">
-						<div class=" font-semibold line-clamp-1">#{doc.name} ({doc.filename})</div>
-						<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
-							{doc.title}
-						</div>
-					</div>
-				</div>
-			</div>
-			<div class="flex flex-row space-x-1 self-center">
-				<button
-					class="self-center w-fit text-sm z-20 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"
-					aria-label={$i18n.t('Edit Doc')}
-					on:click={async (e) => {
-						e.stopPropagation();
-						showEditDocModal = !showEditDocModal;
-						selectedDoc = doc;
-					}}
-				>
-					<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>
-				</button>
-
-				<!-- <button
-            class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl"
-            type="button"
-            on:click={() => {
-                console.log('download file');
-            }}
-        >
-            <svg
-                xmlns="http://www.w3.org/2000/svg"
-                viewBox="0 0 16 16"
-                fill="currentColor"
-                class="w-4 h-4"
-            >
-                <path
-                    d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
-                />
-                <path
-                    d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
-                />
-            </svg>
-        </button> -->
-
-				<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"
-					aria-label={$i18n.t('Delete Doc')}
-					on:click={(e) => {
-						e.stopPropagation();
-
-						deleteDoc(doc.name);
-					}}
-				>
-					<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>
-			</div>
-		</button>
-	{/each}
-</div>
-
-<div class=" text-gray-500 text-xs mt-1 mb-2">
-	ⓘ {$i18n.t("Use '#' in the prompt input to load and select your documents.")}
-</div>
-
-<div class=" flex justify-end w-full mb-2">
-	<div class="flex space-x-2">
-		<input
-			id="documents-import-input"
-			bind:this={documentsImportInputElement}
-			bind:files={importFiles}
-			type="file"
-			accept=".json"
-			hidden
-			on:change={() => {
-				console.log(importFiles);
-
-				const reader = new FileReader();
-				reader.onload = async (event) => {
-					const savedDocs = JSON.parse(event.target.result);
-					console.log(savedDocs);
-
-					for (const doc of savedDocs) {
-						await createNewDoc(
-							localStorage.token,
-							doc.collection_name,
-							doc.filename,
-							doc.name,
-							doc.title,
-							doc.content
-						).catch((error) => {
-							toast.error(error);
-							return null;
-						});
-					}
-
-					await documents.set(await getDocs(localStorage.token));
-				};
-
-				reader.readAsText(importFiles[0]);
-			}}
-		/>
-
-		<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={() => {
-				documentsImportInputElement.click();
-			}}
-		>
-			<div class=" self-center mr-2 font-medium line-clamp-1">
-				{$i18n.t('Import Documents Mapping')}
-			</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 () => {
-				let blob = new Blob([JSON.stringify($documents)], {
-					type: 'application/json'
-				});
-				saveAs(blob, `documents-mapping-export-${Date.now()}.json`);
-			}}
-		>
-			<div class=" self-center mr-2 font-medium line-clamp-1">
-				{$i18n.t('Export Documents Mapping')}
-			</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>

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

@@ -181,7 +181,8 @@
 							{/if}
 						</div>
 						<div class=" text-xs text-gray-500">
-							Updated {dayjs(item.updated_at * 1000).fromNow()}
+							{$i18n.t('Updated')}
+							{dayjs(item.updated_at * 1000).fromNow()}
 						</div>
 					</div>
 				</div>

+ 87 - 66
src/lib/components/workspace/Knowledge/Collection.svelte

@@ -1,6 +1,7 @@
 <script lang="ts">
 	import Fuse from 'fuse.js';
 	import { toast } from 'svelte-sonner';
+	import { v4 as uuidv4 } from 'uuid';
 
 	import { onMount, getContext, onDestroy, tick } from 'svelte';
 	const i18n = getContext('i18n');
@@ -101,6 +102,7 @@
 	const uploadFileHandler = async (file) => {
 		console.log(file);
 
+		const tempItemId = uuidv4();
 		const fileItem = {
 			type: 'file',
 			file: '',
@@ -109,7 +111,8 @@
 			name: file.name,
 			size: file.size,
 			status: 'uploading',
-			error: ''
+			error: '',
+			itemId: tempItemId
 		};
 
 		knowledge.files = [...(knowledge.files ?? []), fileItem];
@@ -131,10 +134,20 @@
 		try {
 			const uploadedFile = await uploadFile(localStorage.token, file).catch((e) => {
 				toast.error(e);
+				return null;
 			});
 
 			if (uploadedFile) {
 				console.log(uploadedFile);
+				knowledge.files = knowledge.files.map((item) => {
+					if (item.itemId === tempItemId) {
+						item.id = uploadedFile.id;
+					}
+
+					// Remove temporary item id
+					delete item.itemId;
+					return item;
+				});
 				await addFileHandler(uploadedFile.id);
 			} else {
 				toast.error($i18n.t('Failed to upload file.'));
@@ -329,12 +342,16 @@
 		const updatedKnowledge = await addFileToKnowledgeById(localStorage.token, id, fileId).catch(
 			(e) => {
 				toast.error(e);
+				return null;
 			}
 		);
 
 		if (updatedKnowledge) {
 			knowledge = updatedKnowledge;
 			toast.success($i18n.t('File added successfully.'));
+		} else {
+			toast.error($i18n.t('Failed to add file.'));
+			knowledge.files = knowledge.files.filter((file) => file.id !== fileId);
 		}
 	};
 
@@ -517,10 +534,10 @@
 	type="file"
 	multiple
 	hidden
-	on:change={() => {
+	on:change={async () => {
 		if (inputFiles && inputFiles.length > 0) {
 			for (const file of inputFiles) {
-				uploadFileHandler(file);
+				await uploadFileHandler(file);
 			}
 
 			inputFiles = null;
@@ -536,65 +553,38 @@
 />
 
 <div class="flex flex-col w-full max-h-[100dvh] h-full">
-	<button
-		class="flex space-x-1 w-fit"
-		on:click={() => {
-			goto('/workspace/knowledge');
-		}}
-	>
-		<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 class="flex items-center justify-between">
+		<button
+			class="flex space-x-1 w-fit"
+			on:click={() => {
+				goto('/workspace/knowledge');
+			}}
+		>
+			<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 class=" flex-shrink-0">
+			<div>
+				<Badge type="success" content="Collection" />
+			</div>
+		</div>
+	</div>
 	<div class="flex flex-col my-2 flex-1 overflow-auto h-0">
 		{#if id && knowledge}
-			<div class=" flex w-full mt-1 mb-3.5">
-				<div class="flex-1">
-					<div class="flex items-center justify-between w-full px-0.5 mb-1">
-						<div class="w-full">
-							<input
-								type="text"
-								class="w-full font-medium text-2xl font-primary bg-transparent outline-none"
-								bind:value={knowledge.name}
-								on:input={() => {
-									changeDebounceHandler();
-								}}
-							/>
-						</div>
-
-						<div class=" flex-shrink-0">
-							<div>
-								<Badge type="success" content="Collection" />
-							</div>
-						</div>
-					</div>
-
-					<div class="flex w-full px-1">
-						<input
-							type="text"
-							class="w-full text-gray-500 text-sm bg-transparent outline-none"
-							bind:value={knowledge.description}
-							on:input={() => {
-								changeDebounceHandler();
-							}}
-						/>
-					</div>
-				</div>
-			</div>
-
 			<div class="flex flex-row h-0 flex-1 overflow-auto">
 				<div
 					class=" {largeScreen
@@ -623,6 +613,9 @@
 										class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
 										bind:value={query}
 										placeholder={$i18n.t('Search Collection')}
+										on:focus={() => {
+											selectedFileId = null;
+										}}
 									/>
 
 									<div>
@@ -652,7 +645,7 @@
 										files={filteredItems}
 										{selectedFileId}
 										on:click={(e) => {
-											selectedFileId = e.detail;
+											selectedFileId = selectedFileId === e.detail ? null : e.detail;
 										}}
 										on:delete={(e) => {
 											console.log(e.detail);
@@ -663,7 +656,7 @@
 									/>
 								</div>
 							{:else}
-								<div class="m-auto text-gray-500 text-xs">No content found</div>
+								<div class="m-auto text-gray-500 text-xs">{$i18n.t('No content found')}</div>
 							{/if}
 						</div>
 					</div>
@@ -699,12 +692,40 @@
 								</div>
 							</div>
 						{:else}
-							<div class="m-auto">
-								<AddFilesPlaceholder title={$i18n.t('Select/Add Files')}>
-									<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
-										Select a file to view or drag and drop a file to upload
+							<div class="m-auto pb-32">
+								<div>
+									<div class=" flex w-full mt-1 mb-3.5">
+										<div class="flex-1">
+											<div class="flex items-center justify-between w-full px-0.5 mb-1">
+												<div class="w-full">
+													<input
+														type="text"
+														class="text-center w-full font-medium text-3xl font-primary bg-transparent outline-none"
+														bind:value={knowledge.name}
+														on:input={() => {
+															changeDebounceHandler();
+														}}
+													/>
+												</div>
+											</div>
+
+											<div class="flex w-full px-1">
+												<input
+													type="text"
+													class="text-center w-full text-gray-500 bg-transparent outline-none"
+													bind:value={knowledge.description}
+													on:input={() => {
+														changeDebounceHandler();
+													}}
+												/>
+											</div>
+										</div>
 									</div>
-								</AddFilesPlaceholder>
+								</div>
+
+								<div class=" mt-2 text-center text-sm text-gray-200 dark:text-gray-700 w-full">
+									{$i18n.t('Select a file to view or drag and drop a file to upload')}
+								</div>
 							</div>
 						{/if}
 					</div>

+ 2 - 2
src/lib/components/workspace/Knowledge/Collection/AddTextContentModal.svelte

@@ -59,7 +59,7 @@
 					<div class="mb-3 w-full">
 						<div class="w-full flex flex-col gap-2.5">
 							<div class="w-full">
-								<div class=" text-sm mb-2">Title</div>
+								<div class=" text-sm mb-2">{$i18n.t('Title')}</div>
 
 								<div class="w-full mt-1">
 									<input
@@ -73,7 +73,7 @@
 							</div>
 
 							<div>
-								<div class="text-sm mb-2">Content</div>
+								<div class="text-sm mb-2">{$i18n.t('Content')}</div>
 
 								<div class=" w-full mt-1">
 									<textarea

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

@@ -414,7 +414,9 @@
 					<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
 						{!!model?.info?.meta?.description
 							? model?.info?.meta?.description
-							: (model?.ollama?.digest ?? model.id)}
+							: model?.ollama?.digest
+								? `${model.id} (${model?.ollama?.digest})`
+								: model.id}
 					</div>
 				</div>
 			</a>

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "مطالبات التصدير",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "فشل في إنشاء مفتاح API.",
 	"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "قم بتسمية النموذج الخاص بك",
 	"New Chat": "دردشة جديدة",
 	"New Password": "كلمة المرور الجديدة",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -592,6 +594,7 @@
 	"Seed": "Seed",
 	"Select a base model": "حدد نموذجا أساسيا",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "أختار الموديل",
 	"Select a pipeline": "حدد مسارا",
@@ -602,7 +605,6 @@
 	"Select Knowledge": "",
 	"Select model": " أختار موديل",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "النموذج (النماذج) المحددة لا تدعم مدخلات الصور",
 	"Send": "تم",
 	"Send a Message": "يُرجى إدخال طلبك هنا",
@@ -730,6 +732,7 @@
 	"Update and Copy Link": "تحديث ونسخ الرابط",
 	"Update for the latest features and improvements.": "",
 	"Update password": "تحديث كلمة المرور",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "GGUF رفع موديل نوع",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Експортване на промптове",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Неуспешно създаване на API ключ.",
 	"Failed to read clipboard contents": "Грешка при четене на съдържанието от клипборда",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "Дайте име на вашия модел",
 	"New Chat": "Нов чат",
 	"New Password": "Нова парола",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "Seed",
 	"Select a base model": "Изберете базов модел",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "Изберете модел",
 	"Select a pipeline": "Изберете тръбопровод",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "Изберете модел",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Избраният(те) модел(и) не поддържа въвеждане на изображения",
 	"Send": "Изпрати",
 	"Send a Message": "Изпращане на Съобщение",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "Обнови и копирай връзка",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Обновяване на парола",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "Качване на GGUF модел",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "প্রম্পটগুলো একপোর্ট করুন",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "API Key তৈরি করা যায়নি।",
 	"Failed to read clipboard contents": "ক্লিপবোর্ডের বিষয়বস্তু পড়া সম্ভব হয়নি",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "আপনার মডেলের নাম দিন",
 	"New Chat": "নতুন চ্যাট",
 	"New Password": "নতুন পাসওয়ার্ড",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "সীড",
 	"Select a base model": "একটি বেস মডেল নির্বাচন করুন",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "একটি মডেল নির্বাচন করুন",
 	"Select a pipeline": "একটি পাইপলাইন নির্বাচন করুন",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "মডেল নির্বাচন করুন",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "নির্বাচিত মডেল(গুলি) চিত্র ইনপুট সমর্থন করে না",
 	"Send": "পাঠান",
 	"Send a Message": "একটি মেসেজ পাঠান",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "আপডেট এবং লিংক কপি করুন",
 	"Update for the latest features and improvements.": "",
 	"Update password": "পাসওয়ার্ড আপডেট করুন",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "একটি GGUF মডেল আপলোড করুন",

+ 20 - 17
src/lib/i18n/locales/ca-ES/translation.json

@@ -68,8 +68,8 @@
 	"Archived Chats": "Xats arxivats",
 	"are allowed - Activate this command by typing": "estan permesos - Activa aquesta comanda escrivint",
 	"Are you sure?": "Estàs segur?",
-	"Artifacts": "",
-	"Ask a question": "",
+	"Artifacts": "Artefactes",
+	"Ask a question": "Fer una pregunta",
 	"Attach file": "Adjuntar arxiu",
 	"Attention to detail": "Atenció al detall",
 	"Audio": "Àudio",
@@ -294,6 +294,7 @@
 	"Export Prompts": "Exportar les indicacions",
 	"Export Tools": "Exportar les eines",
 	"External Models": "Models externs",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "No s'ha pogut crear la clau API.",
 	"Failed to read clipboard contents": "No s'ha pogut llegir el contingut del porta-retalls",
 	"Failed to update settings": "No s'han pogut actualitzar les preferències",
@@ -382,9 +383,9 @@
 	"Knowledge": "Coneixement",
 	"Knowledge created successfully.": "Coneixement creat correctament.",
 	"Knowledge deleted successfully.": "Coneixement eliminat correctament.",
-	"Knowledge reset successfully.": "",
+	"Knowledge reset successfully.": "Coneixement restablert correctament.",
 	"Knowledge updated successfully": "Coneixement actualitzat correctament.",
-	"Landing Page Mode": "",
+	"Landing Page Mode": "Mode de la pàgina d'entrada",
 	"Language": "Idioma",
 	"large language models, locally.": "models de llenguatge extensos, localment",
 	"Last Active": "Activitat recent",
@@ -449,9 +450,10 @@
 	"Name your model": "Posa un nom al teu model",
 	"New Chat": "Nou xat",
 	"New Password": "Nova contrasenya",
+	"No content found": "",
 	"No content to speak": "No hi ha contingut per parlar",
 	"No file selected": "No s'ha escollit cap fitxer",
-	"No HTML, CSS, or JavaScript content found.": "",
+	"No HTML, CSS, or JavaScript content found.": "No s'ha trobat contingut HTML, CSS o JavaScript.",
 	"No knowledge found": "No s'ha trobat Coneixement",
 	"No results found": "No s'han trobat resultats",
 	"No search query generated": "No s'ha generat cap consulta",
@@ -483,7 +485,7 @@
 	"Oops! There was an error in the previous response. Please try again or contact admin.": "Ui! Hi ha hagut un error en la resposta anterior. Torna a provar-ho o contacta amb un administrador",
 	"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 file": "Obrir arxiu",
-	"Open in full screen": "",
+	"Open in full screen": "Obrir en pantalla complerta",
 	"Open new chat": "Obre un xat nou",
 	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "La versió d'Open WebUI (v{{OPEN_WEBUI_VERSION}}) és inferior a la versió requerida (v{{REQUIRED_VERSION}})",
 	"OpenAI": "OpenAI",
@@ -514,7 +516,7 @@
 	"Plain text (.txt)": "Text pla (.txt)",
 	"Playground": "Zona de jocs",
 	"Please carefully review the following warnings:": "Si us plau, revisa els següents avisos amb cura:",
-	"Please fill in all fields.": "",
+	"Please fill in all fields.": "Emplena tots els camps, si us plau.",
 	"Please select a reason": "Si us plau, selecciona una raó",
 	"Positive attitude": "Actitud positiva",
 	"Previous 30 days": "30 dies anteriors",
@@ -561,7 +563,7 @@
 	"Save & Update": "Desar i actualitzar",
 	"Save As Copy": "Desar com a còpia",
 	"Save Tag": "Desar l'etiqueta",
-	"Saved": "",
+	"Saved": "Desat",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Desar els registres de xat directament a l'emmagatzematge del teu navegador ja no està suportat. Si us plau, descarregr i elimina els registres de xat fent clic al botó de sota. No et preocupis, pots tornar a importar fàcilment els teus registres de xat al backend a través de",
 	"Scroll to bottom when switching between branches": "Desplaçar a la part inferior quan es canviï de branca",
 	"Search": "Cercar",
@@ -589,6 +591,7 @@
 	"Seed": "Llavor",
 	"Select a base model": "Seleccionar un model base",
 	"Select a engine": "Seleccionar un motor",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Seleccionar una funció",
 	"Select a model": "Seleccionar un model",
 	"Select a pipeline": "Seleccionar una Pipeline",
@@ -599,7 +602,6 @@
 	"Select Knowledge": "Seleccionar coneixement",
 	"Select model": "Seleccionar un model",
 	"Select only one model to call": "Seleccionar només un model per trucar",
-	"Select/Add Files": "Seleccionar/Afegir arxius",
 	"Selected model(s) do not support image inputs": "El(s) model(s) seleccionats no admeten l'entrada d'imatges",
 	"Send": "Enviar",
 	"Send a Message": "Enviar un missatge",
@@ -634,11 +636,11 @@
 	"Show your support!": "Mostra el teu suport!",
 	"Showcased creativity": "Creativitat mostrada",
 	"Sign in": "Iniciar sessió",
-	"Sign in to {{WEBUI_NAME}}": "",
+	"Sign in to {{WEBUI_NAME}}": "Iniciar sessió a {{WEBUI_NAME}}",
 	"Sign Out": "Tancar sessió",
 	"Sign up": "Registrar-se",
-	"Sign up to {{WEBUI_NAME}}": "",
-	"Signing in to {{WEBUI_NAME}}": "",
+	"Sign up to {{WEBUI_NAME}}": "Registrar-se a {{WEBUI_NAME}}",
+	"Signing in to {{WEBUI_NAME}}": "Iniciant sessió a {{WEBUI_NAME}}",
 	"Source": "Font",
 	"Speech Playback Speed": "Velocitat de la parla",
 	"Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}",
@@ -654,7 +656,7 @@
 	"Suggested": "Suggerit",
 	"Support": "Dona suport",
 	"Support this plugin:": "Dona suport a aquest complement:",
-	"Sync directory": "",
+	"Sync directory": "Sincronitzar directori",
 	"System": "Sistema",
 	"System Prompt": "Indicació del Sistema",
 	"Tags": "Etiquetes",
@@ -677,9 +679,9 @@
 	"This action cannot be undone. Do you wish to continue?": "Aquesta acció no es pot desfer. Vols continuar?",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Això assegura que les teves converses valuoses queden desades de manera segura a la teva base de dades. Gràcies!",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "Aquesta és una funció experimental, és possible que no funcioni com s'espera i està subjecta a canvis en qualsevol moment.",
-	"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
+	"This option will delete all existing files in the collection and replace them with newly uploaded files.": "Aquesta opció eliminarà tots els fitxers existents de la col·lecció i els substituirà per fitxers recentment penjats.",
 	"This will delete": "Això eliminarà",
-	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
+	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Això restablirà la base de coneixement i sincronitzarà tots els fitxers. Vols continuar?",
 	"Thorough explanation": "Explicació en detall",
 	"Tika": "Tika",
 	"Tika Server URL required.": "La URL del servidor Tika és obligatòria.",
@@ -727,10 +729,11 @@
 	"Update and Copy Link": "Actualitzar i copiar l'enllaç",
 	"Update for the latest features and improvements.": "Actualitza per a les darreres característiques i millores.",
 	"Update password": "Actualitzar la contrasenya",
+	"Updated": "",
 	"Updated at": "Actualitzat",
 	"Upload": "Pujar",
 	"Upload a GGUF model": "Pujar un model GGUF",
-	"Upload directory": "",
+	"Upload directory": "Pujar directori",
 	"Upload files": "Pujar fitxers",
 	"Upload Files": "Pujar fitxers",
 	"Upload Pipeline": "Pujar una Pipeline",
@@ -754,7 +757,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ó",
-	"Version {{selectedVersion}} of {{totalVersions}}": "",
+	"Version {{selectedVersion}} of {{totalVersions}}": "Versió {{selectedVersion}} de {{totalVersions}}",
 	"Voice": "Veu",
 	"Warning": "Avís",
 	"Warning:": "Avís:",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Export prompts",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "",
 	"Failed to read clipboard contents": "Napakyas sa pagbasa sa sulod sa clipboard",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "",
 	"New Chat": "Bag-ong diskusyon",
 	"New Password": "Bag-ong Password",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "Binhi",
 	"Select a base model": "",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "Pagpili og modelo",
 	"Select a pipeline": "",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "Pagpili og modelo",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "",
 	"Send": "",
 	"Send a Message": "Magpadala ug mensahe",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "",
 	"Update for the latest features and improvements.": "",
 	"Update password": "I-update ang password",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "Pag-upload ug modelo sa GGUF",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Prompts exportieren",
 	"Export Tools": "Werkzeuge exportieren",
 	"External Models": "Externe Modelle",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Fehler beim Erstellen des API-Schlüssels.",
 	"Failed to read clipboard contents": "Fehler beim Abruf der Zwischenablage",
 	"Failed to update settings": "Fehler beim Aktualisieren der Einstellungen",
@@ -450,6 +451,7 @@
 	"Name your model": "Benennen Sie Ihr Modell",
 	"New Chat": "Neue Unterhaltung",
 	"New Password": "Neues Passwort",
+	"No content found": "",
 	"No content to speak": "Kein Inhalt zum Vorlesen",
 	"No file selected": "Keine Datei ausgewählt",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -589,6 +591,7 @@
 	"Seed": "Seed",
 	"Select a base model": "Wählen Sie ein Basismodell",
 	"Select a engine": "Wählen Sie eine Engine",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Wählen Sie eine Funktion",
 	"Select a model": "Wählen Sie ein Modell",
 	"Select a pipeline": "Wählen Sie eine Pipeline",
@@ -599,7 +602,6 @@
 	"Select Knowledge": "",
 	"Select model": "Modell auswählen",
 	"Select only one model to call": "Wählen Sie nur ein Modell zum Anrufen aus",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Ihre ausgewählten Modelle unterstützen keine Bildeingaben",
 	"Send": "Senden",
 	"Send a Message": "Eine Nachricht senden",
@@ -727,6 +729,7 @@
 	"Update and Copy Link": "Aktualisieren und Link kopieren",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Passwort aktualisieren",
+	"Updated": "",
 	"Updated at": "Aktualisiert am",
 	"Upload": "Hochladen",
 	"Upload a GGUF model": "GGUF-Model hochladen",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Export Promptos",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "",
 	"Failed to read clipboard contents": "Failed to read clipboard borks",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "",
 	"New Chat": "New Bark",
 	"New Password": "New Barkword",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -590,6 +592,7 @@
 	"Seed": "Seed very plant",
 	"Select a base model": "",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "Select a model much choice",
 	"Select a pipeline": "",
@@ -600,7 +603,6 @@
 	"Select Knowledge": "",
 	"Select model": "Select model much choice",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "",
 	"Send": "",
 	"Send a Message": "Send a Message much message",
@@ -728,6 +730,7 @@
 	"Update and Copy Link": "",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Update password much change",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "Upload a GGUF model very upload",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "",
 	"Failed to read clipboard contents": "",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "",
 	"New Chat": "",
 	"New Password": "",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "",
 	"Select a base model": "",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "",
 	"Select a pipeline": "",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "",
 	"Send": "",
 	"Send a Message": "",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "",
 	"Update for the latest features and improvements.": "",
 	"Update password": "",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "",
 	"Failed to read clipboard contents": "",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "",
 	"New Chat": "",
 	"New Password": "",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "",
 	"Select a base model": "",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "",
 	"Select a pipeline": "",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "",
 	"Send": "",
 	"Send a Message": "",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "",
 	"Update for the latest features and improvements.": "",
 	"Update password": "",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "",

+ 157 - 154
src/lib/i18n/locales/es-ES/translation.json

@@ -1,14 +1,14 @@
 {
 	"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' o '-1' para evitar expiración.",
 	"(Beta)": "(Beta)",
-	"(e.g. `sh webui.sh --api --api-auth username_password`)": "",
+	"(e.g. `sh webui.sh --api --api-auth username_password`)": "(p.ej. `sh webui.sh --api --api-auth username_password`)",
 	"(e.g. `sh webui.sh --api`)": "(p.ej. `sh webui.sh --api`)",
 	"(latest)": "(latest)",
 	"{{ models }}": "{{ models }}",
 	"{{ owner }}: You cannot delete a base model": "{{ owner }}: No se puede eliminar un modelo base",
-	"{{user}}'s Chats": "{{user}}'s Chats",
+	"{{user}}'s Chats": "Chats de {{user}}",
 	"{{webUIName}} Backend Required": "{{webUIName}} Servidor Requerido",
-	"*Prompt node ID(s) are required for image generation": "",
+	"*Prompt node ID(s) are required for image generation": "Los ID de nodo son requeridos para la generación de imágenes",
 	"A new version (v{{LATEST_VERSION}}) is now available.": "",
 	"A task model is used when performing tasks such as generating titles for chats and web search queries": "Un modelo de tareas se utiliza cuando se realizan tareas como la generación de títulos para chats y consultas de búsqueda web",
 	"a user": "un usuario",
@@ -16,24 +16,24 @@
 	"Account": "Cuenta",
 	"Account Activation Pending": "Activación de cuenta pendiente",
 	"Accurate information": "Información precisa",
-	"Actions": "",
+	"Actions": "Acciones",
 	"Active Users": "Usuarios activos",
 	"Add": "Agregar",
 	"Add a model id": "Adición de un identificador de modelo",
 	"Add a short description about what this model does": "Agregue una breve descripción sobre lo que hace este modelo",
 	"Add a short title for this prompt": "Agregue un título corto para este Prompt",
 	"Add a tag": "Agregar una etiqueta",
-	"Add Content": "",
-	"Add content here": "",
+	"Add Content": "Agregar Contenido",
+	"Add content here": "Agrege contenido aquí",
 	"Add custom prompt": "Agregar un prompt personalizado",
 	"Add Docs": "Agregar Documentos",
 	"Add Files": "Agregar Archivos",
 	"Add Memory": "Agregar Memoria",
-	"Add message": "Agregar Prompt",
+	"Add message": "Agregar mensaje",
 	"Add Model": "Agregar Modelo",
-	"Add Tag": "",
+	"Add Tag": "Agregar etiqueta",
 	"Add Tags": "agregar etiquetas",
-	"Add text content": "",
+	"Add text content": "Añade contenido de texto",
 	"Add User": "Agregar Usuario",
 	"Adjusting these settings will apply changes universally to all users.": "Ajustar estas opciones aplicará los cambios universalmente a todos los usuarios.",
 	"admin": "admin",
@@ -48,9 +48,9 @@
 	"All Users": "Todos los Usuarios",
 	"Allow": "Permitir",
 	"Allow Chat Deletion": "Permitir Borrar Chats",
-	"Allow Chat Editing": "",
+	"Allow Chat Editing": "Permitir Editar Chat",
 	"Allow non-local voices": "Permitir voces no locales",
-	"Allow Temporary Chat": "",
+	"Allow Temporary Chat": "Permitir Chat Temporal",
 	"Allow User Location": "Permitir Ubicación del Usuario",
 	"Allow Voice Interruption in Call": "Permitir interrupción de voz en llamada",
 	"alphanumeric characters and hyphens": "caracteres alfanuméricos y guiones",
@@ -68,21 +68,21 @@
 	"Archived Chats": "Chats archivados",
 	"are allowed - Activate this command by typing": "están permitidos - Active este comando escribiendo",
 	"Are you sure?": "¿Está seguro?",
-	"Artifacts": "",
-	"Ask a question": "",
+	"Artifacts": "Artefactos",
+	"Ask a question": "Haz una pregunta",
 	"Attach file": "Adjuntar archivo",
 	"Attention to detail": "Detalle preciso",
 	"Audio": "Audio",
 	"August": "Agosto",
 	"Auto-playback response": "Respuesta de reproducción automática",
 	"Automatic1111": "",
-	"AUTOMATIC1111 Api Auth String": "",
+	"AUTOMATIC1111 Api Auth String": "Cadena de autenticación de API",
 	"AUTOMATIC1111 Base URL": "Dirección URL de AUTOMATIC1111",
 	"AUTOMATIC1111 Base URL is required.": "La dirección URL de AUTOMATIC1111 es requerida.",
-	"Available list": "",
+	"Available list": "Lista disponible",
 	"available!": "¡disponible!",
 	"Azure AI Speech": "",
-	"Azure Region": "",
+	"Azure Region": "Región de Azure",
 	"Back": "Volver",
 	"Bad Response": "Respuesta incorrecta",
 	"Banners": "Banners",
@@ -101,9 +101,9 @@
 	"Chat": "Chat",
 	"Chat Background Image": "Imágen de fondo del Chat",
 	"Chat Bubble UI": "Burbuja de chat UI",
-	"Chat Controls": "",
+	"Chat Controls": "Controles de chat",
 	"Chat direction": "Dirección del Chat",
-	"Chat Overview": "",
+	"Chat Overview": "Vista general del chat",
 	"Chats": "Chats",
 	"Check Again": "Verifica de nuevo",
 	"Check for updates": "Verificar actualizaciones",
@@ -121,7 +121,7 @@
 	"Click here to select a csv file.": "Presiona aquí para seleccionar un archivo csv.",
 	"Click here to select a py file.": "Presiona aquí para seleccionar un archivo py.",
 	"Click here to select documents.": "Presiona aquí para seleccionar documentos",
-	"Click here to upload a workflow.json file.": "",
+	"Click here to upload a workflow.json file.": "Presiona aquí para subir un archivo workflow.json.",
 	"click here.": "Presiona aquí.",
 	"Click on the user role button to change a user's role.": "Presiona en el botón de roles del usuario para cambiar su rol.",
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Permisos de escritura del portapapeles denegados. Por favor, comprueba las configuraciones de tu navegador para otorgar el acceso necesario.",
@@ -133,7 +133,7 @@
 	"ComfyUI Base URL": "ComfyUI Base URL",
 	"ComfyUI Base URL is required.": "ComfyUI Base URL es requerido.",
 	"ComfyUI Workflow": "",
-	"ComfyUI Workflow Nodes": "",
+	"ComfyUI Workflow Nodes": "Nodos para ComfyUI Workflow",
 	"Command": "Comando",
 	"Concurrent Requests": "Solicitudes simultáneas",
 	"Confirm": "Confirmar",
@@ -142,15 +142,15 @@
 	"Connections": "Conexiones",
 	"Contact Admin for WebUI Access": "Contacta el administrador para obtener acceso al WebUI",
 	"Content": "Contenido",
-	"Content Extraction": "",
+	"Content Extraction": "Extracción de contenido",
 	"Context Length": "Longitud del contexto",
 	"Continue Response": "Continuar Respuesta",
 	"Continue with {{provider}}": "Continuar con {{provider}}",
 	"Control how message text is split for TTS requests. 'Punctuation' splits into sentences, 'paragraphs' splits into paragraphs, and 'none' keeps the message as a single string.": "",
-	"Controls": "",
-	"Copied": "",
+	"Controls": "Controles",
+	"Copied": "Copiado",
 	"Copied shared chat URL to clipboard!": "¡URL de chat compartido copiado al portapapeles!",
-	"Copied to clipboard": "",
+	"Copied to clipboard": "Copiado al portapapeles",
 	"Copy": "Copiar",
 	"Copy last code block": "Copia el último bloque de código",
 	"Copy last response": "Copia la última respuesta",
@@ -158,7 +158,7 @@
 	"Copying to clipboard was successful!": "¡La copia al portapapeles se ha realizado correctamente!",
 	"Create a model": "Crear un modelo",
 	"Create Account": "Crear una cuenta",
-	"Create Knowledge": "",
+	"Create Knowledge": "Crear Conocimiento",
 	"Create new key": "Crear una nueva clave",
 	"Create new secret key": "Crear una nueva clave secreta",
 	"Created at": "Creado en",
@@ -174,8 +174,8 @@
 	"Database": "Base de datos",
 	"December": "Diciembre",
 	"Default": "Por defecto",
-	"Default (Open AI)": "",
-	"Default (SentenceTransformers)": "Por defecto (SentenceTransformers)",
+	"Default (Open AI)": "Predeterminado (Open AI)",
+	"Default (SentenceTransformers)": "Predeterminado (SentenceTransformers)",
 	"Default Model": "Modelo predeterminado",
 	"Default model updated": "El modelo por defecto ha sido actualizado",
 	"Default Prompt Suggestions": "Sugerencias de mensajes por defecto",
@@ -187,17 +187,17 @@
 	"Delete chat": "Borrar chat",
 	"Delete Chat": "Borrar Chat",
 	"Delete chat?": "Borrar el chat?",
-	"Delete Doc": "",
-	"Delete function?": "",
-	"Delete prompt?": "",
+	"Delete Doc": "Borrar Doc",
+	"Delete function?": "Borrar la función?",
+	"Delete prompt?": "Borrar el prompt?",
 	"delete this link": "Borrar este enlace",
-	"Delete tool?": "",
+	"Delete tool?": "Borrar la herramienta",
 	"Delete User": "Borrar Usuario",
 	"Deleted {{deleteModelTag}}": "Se borró {{deleteModelTag}}",
 	"Deleted {{name}}": "Eliminado {{nombre}}",
 	"Description": "Descripción",
 	"Didn't fully follow instructions": "No siguió las instrucciones",
-	"Disabled": "",
+	"Disabled": "Desactivado",
 	"Discover a function": "Descubre una función",
 	"Discover a model": "Descubrir un modelo",
 	"Discover a prompt": "Descubre un Prompt",
@@ -209,16 +209,16 @@
 	"Dismissible": "Desestimable",
 	"Display Emoji in Call": "Muestra Emoji en llamada",
 	"Display the username instead of You in the Chat": "Mostrar el nombre de usuario en lugar de Usted en el chat",
-	"Do not install functions from sources you do not fully trust.": "",
-	"Do not install tools from sources you do not fully trust.": "",
+	"Do not install functions from sources you do not fully trust.": "No instale funciones desde fuentes que no confíe totalmente.",
+	"Do not install tools from sources you do not fully trust.": "No instale herramientas desde fuentes que no confíe totalmente.",
 	"Document": "Documento",
 	"Documentation": "Documentación",
 	"Documents": "Documentos",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "no realiza ninguna conexión externa y sus datos permanecen seguros en su servidor alojado localmente.",
 	"Don't Allow": "No Permitir",
 	"Don't have an account?": "¿No tienes una cuenta?",
-	"don't install random functions from sources you don't trust.": "",
-	"don't install random tools from sources you don't trust.": "",
+	"don't install random functions from sources you don't trust.": "no instale funciones aleatorias desde fuentes que no confíe.",
+	"don't install random tools from sources you don't trust.": "no instale herramientas aleatorias desde fuentes que no confíe.",
 	"Don't like the style": "No te gusta el estilo?",
 	"Done": "Hecho",
 	"Download": "Descargar",
@@ -237,18 +237,18 @@
 	"Embedding Model Engine": "Motor de Modelo de Embedding",
 	"Embedding model set to \"{{embedding_model}}\"": "Modelo de Embedding configurado a \"{{embedding_model}}\"",
 	"Enable Community Sharing": "Habilitar el uso compartido de la comunidad",
-	"Enable Message Rating": "",
+	"Enable Message Rating": "Habilitar la calificación de los mensajes",
 	"Enable New Sign Ups": "Habilitar Nuevos Registros",
 	"Enable Web Search": "Habilitar la búsqueda web",
-	"Enable Web Search Query Generation": "",
-	"Enabled": "",
-	"Engine": "",
+	"Enable Web Search Query Generation": "Habilitar generación de consultas web",
+	"Enabled": "Activado",
+	"Engine": "Motor",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Asegúrese de que su archivo CSV incluya 4 columnas en este orden: Nombre, Correo Electrónico, Contraseña, Rol.",
 	"Enter {{role}} message here": "Ingrese el mensaje {{role}} aquí",
 	"Enter a detail about yourself for your LLMs to recall": "Ingrese un detalle sobre usted para que sus LLMs recuerden",
-	"Enter api auth string (e.g. username:password)": "",
+	"Enter api auth string (e.g. username:password)": "Ingrese la cadena de autorización de api (p.ej., nombre:contraseña)",
 	"Enter Brave Search API Key": "Ingresa la clave de API de Brave Search",
-	"Enter CFG Scale (e.g. 7.0)": "",
+	"Enter CFG Scale (e.g. 7.0)": "Ingresa la escala de CFG (p.ej., 7.0)",
 	"Enter Chunk Overlap": "Ingresar superposición de fragmentos",
 	"Enter Chunk Size": "Ingrese el tamaño del fragmento",
 	"Enter Github Raw URL": "Ingresa la URL sin procesar de Github",
@@ -256,28 +256,28 @@
 	"Enter Google PSE Engine Id": "Introduzca el ID del motor PSE de Google",
 	"Enter Image Size (e.g. 512x512)": "Ingrese el tamaño de la imagen (p.ej. 512x512)",
 	"Enter language codes": "Ingrese códigos de idioma",
-	"Enter Model ID": "",
+	"Enter Model ID": "Ingresa el ID del modelo",
 	"Enter model tag (e.g. {{modelTag}})": "Ingrese la etiqueta del modelo (p.ej. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Ingrese el número de pasos (p.ej., 50)",
-	"Enter Sampler (e.g. Euler a)": "",
-	"Enter Scheduler (e.g. Karras)": "",
+	"Enter Sampler (e.g. Euler a)": "Ingrese el sampler (p.ej., Euler a)",
+	"Enter Scheduler (e.g. Karras)": "Ingrese el planificador (p.ej., Karras)",
 	"Enter Score": "Ingrese la puntuación",
-	"Enter SearchApi API Key": "",
-	"Enter SearchApi Engine": "",
+	"Enter SearchApi API Key": "Ingrese la Clave API de SearchApi",
+	"Enter SearchApi Engine": "Ingrese el motor de SearchApi",
 	"Enter Searxng Query URL": "Introduzca la URL de consulta de Searxng",
 	"Enter Serper API Key": "Ingrese la clave API de Serper",
 	"Enter Serply API Key": "Ingrese la clave API de Serply",
 	"Enter Serpstack API Key": "Ingrese la clave API de Serpstack",
 	"Enter stop sequence": "Ingrese la secuencia de parada",
-	"Enter system prompt": "",
+	"Enter system prompt": "Ingrese el prompt del sistema",
 	"Enter Tavily API Key": "Ingrese la clave API de Tavily",
-	"Enter Tika Server URL": "",
+	"Enter Tika Server URL": "Ingrese la URL del servidor Tika",
 	"Enter Top K": "Ingrese el Top K",
 	"Enter URL (e.g. http://127.0.0.1:7860/)": "Ingrese la URL (p.ej., http://127.0.0.1:7860/)",
 	"Enter URL (e.g. http://localhost:11434)": "Ingrese la URL (p.ej., http://localhost:11434)",
 	"Enter Your Email": "Ingrese su correo electrónico",
 	"Enter Your Full Name": "Ingrese su nombre completo",
-	"Enter your message": "",
+	"Enter your message": "Ingrese su mensaje",
 	"Enter Your Password": "Ingrese su contraseña",
 	"Enter Your Role": "Ingrese su rol",
 	"Error": "Error",
@@ -294,22 +294,23 @@
 	"Export Prompts": "Exportar Prompts",
 	"Export Tools": "Exportar Herramientas",
 	"External Models": "Modelos Externos",
-	"Failed to create API Key.": "No se pudo crear la clave API.",
+	"Failed to add file.": "",
+    "Failed to create API Key.": "No se pudo crear la clave API.",
 	"Failed to read clipboard contents": "No se pudo leer el contenido del portapapeles",
 	"Failed to update settings": "Falla al actualizar los ajustes",
-	"Failed to upload file.": "",
+	"Failed to upload file.": "Falla al subir el archivo.",
 	"February": "Febrero",
 	"Feel free to add specific details": "Libre de agregar detalles específicos",
 	"File": "Archivo",
-	"File added successfully.": "",
-	"File content updated successfully.": "",
+	"File added successfully.": "Archivo agregado correctamente.",
+	"File content updated successfully.": "Contenido del archivo actualizado correctamente.",
 	"File Mode": "Modo de archivo",
 	"File not found.": "Archivo no encontrado.",
-	"File removed successfully.": "",
-	"File size should not exceed {{maxSize}} MB.": "",
-	"Files": "",
-	"Filter is now globally disabled": "",
-	"Filter is now globally enabled": "",
+	"File removed successfully.": "Archivo eliminado correctamente.",
+	"File size should not exceed {{maxSize}} MB.": "Tamaño del archivo no debe exceder {{maxSize}} MB.",
+	"Files": "Archivos",
+	"Filter is now globally disabled": "El filtro ahora está desactivado globalmente",
+	"Filter is now globally enabled": "El filtro ahora está habilitado globalmente",
 	"Filters": "Filtros",
 	"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",
@@ -320,28 +321,28 @@
 	"Frequency Penalty": "Penalización de frecuencia",
 	"Function created successfully": "Función creada exitosamente",
 	"Function deleted successfully": "Función borrada exitosamente",
-	"Function Description (e.g. A filter to remove profanity from text)": "",
-	"Function ID (e.g. my_filter)": "",
-	"Function is now globally disabled": "",
-	"Function is now globally enabled": "",
-	"Function Name (e.g. My Filter)": "",
+	"Function Description (e.g. A filter to remove profanity from text)": "Descripción de la función (por ejemplo, un filtro para eliminar lo profano del texto)",
+	"Function ID (e.g. my_filter)": "ID de la función (por ejemplo, mi_filtro)",
+	"Function is now globally disabled": "La función ahora está desactivada globalmente",
+	"Function is now globally enabled": "La función está habilitada globalmente",
+	"Function Name (e.g. My Filter)": "Nombre de la función (por ejemplo, Mi filtro)",
 	"Function updated successfully": "Función actualizada exitosamente",
 	"Functions": "Funciones",
-	"Functions allow arbitrary code execution": "",
-	"Functions allow arbitrary code execution.": "",
+	"Functions allow arbitrary code execution": "Funciones habilitan la ejecución de código arbitrario",
+	"Functions allow arbitrary code execution.": "Funciones habilitan la ejecución de código arbitrario.",
 	"Functions imported successfully": "Funciones importadas exitosamente",
 	"General": "General",
 	"General Settings": "Opciones Generales",
 	"Generate Image": "Generar imagen",
 	"Generating search query": "Generación de consultas de búsqueda",
 	"Generation Info": "Información de Generación",
-	"Get up and running with": "",
-	"Global": "",
+	"Get up and running with": "Levanta y empieza con",
+	"Global": "Global",
 	"Good Response": "Buena Respuesta",
 	"Google PSE API Key": "Clave API de Google PSE",
 	"Google PSE Engine Id": "ID del motor PSE de Google",
 	"h:mm a": "h:mm a",
-	"Haptic Feedback": "",
+	"Haptic Feedback": "Retroalimentación háptica",
 	"has no conversations.": "no tiene conversaciones.",
 	"Hello, {{name}}": "Hola, {{name}}",
 	"Help": "Ayuda",
@@ -349,7 +350,7 @@
 	"Hide Model": "Esconder Modelo",
 	"How can I help you today?": "¿Cómo puedo ayudarte hoy?",
 	"Hybrid Search": "Búsqueda Híbrida",
-	"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.": "",
+	"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.": "Aseguro que he leído y entiendo las implicaciones de mi acción. Estoy consciente de los riesgos asociados con la ejecución de código arbitrario y he verificado la confianza de la fuente.",
 	"Image Generation (Experimental)": "Generación de imágenes (experimental)",
 	"Image Generation Engine": "Motor de generación de imágenes",
 	"Image Settings": "Ajustes de la Imágen",
@@ -361,7 +362,7 @@
 	"Import Models": "Importar modelos",
 	"Import Prompts": "Importar Prompts",
 	"Import Tools": "Importar Herramientas",
-	"Include `--api-auth` flag when running stable-diffusion-webui": "",
+	"Include `--api-auth` flag when running stable-diffusion-webui": "Incluir el indicador `--api-auth` al ejecutar stable-diffusion-webui",
 	"Include `--api` flag when running stable-diffusion-webui": "Incluir el indicador `--api` al ejecutar stable-diffusion-webui",
 	"Info": "Información",
 	"Input commands": "Ingresar comandos",
@@ -380,17 +381,17 @@
 	"Keep Alive": "Mantener Vivo",
 	"Keyboard shortcuts": "Atajos de teclado",
 	"Knowledge": "Conocimiento",
-	"Knowledge created successfully.": "",
-	"Knowledge deleted successfully.": "",
-	"Knowledge reset successfully.": "",
-	"Knowledge updated successfully": "",
-	"Landing Page Mode": "",
+	"Knowledge created successfully.": "Conocimiento creado exitosamente.",
+	"Knowledge deleted successfully.": "Conocimiento eliminado exitosamente.",
+	"Knowledge reset successfully.": "Conocimiento restablecido exitosamente.",
+	"Knowledge updated successfully": "Conocimiento actualizado exitosamente.",
+	"Landing Page Mode": "Modo de Página de Inicio",
 	"Language": "Lenguaje",
-	"large language models, locally.": "",
+	"large language models, locally.": "modelos de lenguaje grande, localmente",
 	"Last Active": "Última Actividad",
 	"Last Modified": "Modificado por última vez",
-	"Leave empty for unlimited": "",
-	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave empty for unlimited": "Deje vacío para ilimitado",
+	"Leave empty to use the default prompt, or enter a custom prompt": "Deje vacío para usar el propmt predeterminado, o ingrese un propmt personalizado",
 	"Light": "Claro",
 	"Listening...": "Escuchando...",
 	"LLMs can make mistakes. Verify important information.": "Los LLM pueden cometer errores. Verifica la información importante.",
@@ -398,15 +399,15 @@
 	"LTR": "LTR",
 	"Made by OpenWebUI Community": "Hecho por la comunidad de OpenWebUI",
 	"Make sure to enclose them with": "Asegúrese de adjuntarlos con",
-	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
+	"Make sure to export a workflow.json file as API format from ComfyUI.": "Asegúrese de exportar un archivo workflow.json en formato API desde ComfyUI.",
 	"Manage": "Gestionar",
 	"Manage Models": "Administrar Modelos",
 	"Manage Ollama Models": "Administrar Modelos Ollama",
 	"Manage Pipelines": "Administrar Pipelines",
 	"March": "Marzo",
 	"Max Tokens (num_predict)": "Máximo de fichas (num_predict)",
-	"Max Upload Count": "",
-	"Max Upload Size": "",
+	"Max Upload Count": "Cantidad máxima de cargas",
+	"Max Upload Size": "Tamaño máximo de Cargas",
 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Se pueden descargar un máximo de 3 modelos simultáneamente. Por favor, inténtelo de nuevo más tarde.",
 	"May": "Mayo",
 	"Memories accessible by LLMs will be shown here.": "Las memorias accesibles por los LLMs se mostrarán aquí.",
@@ -415,7 +416,7 @@
 	"Memory cleared successfully": "Memoria liberada correctamente",
 	"Memory deleted successfully": "Memoria borrada correctamente",
 	"Memory updated successfully": "Memoria actualizada correctamente",
-	"Merge Responses": "",
+	"Merge Responses": "Fusionar Respuestas",
 	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Los mensajes que envíe después de crear su enlace no se compartirán. Los usuarios con el enlace podrán ver el chat compartido.",
 	"Min P": "",
 	"Minimum Score": "Puntuación mínima",
@@ -430,8 +431,8 @@
 	"Model {{modelId}} not found": "El modelo {{modelId}} no fue encontrado",
 	"Model {{modelName}} is not vision capable": "El modelo {{modelName}} no es capaz de ver",
 	"Model {{name}} is now {{status}}": "El modelo {{name}} ahora es {{status}}",
-	"Model {{name}} is now at the top": "",
-	"Model accepts image inputs": "",
+	"Model {{name}} is now at the top": "El modelo {{name}} está ahora en el tope",
+	"Model accepts image inputs": "El modelo acepta entradas de imagenes",
 	"Model created successfully!": "Modelo creado correctamente!",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Se detectó la ruta del sistema de archivos del modelo. Se requiere el nombre corto del modelo para la actualización, no se puede continuar.",
 	"Model ID": "ID del modelo",
@@ -443,16 +444,17 @@
 	"Modelfile Content": "Contenido del Modelfile",
 	"Models": "Modelos",
 	"More": "Más",
-	"Move to Top": "",
+	"Move to Top": "Mueve al tope",
 	"Name": "Nombre",
 	"Name Tag": "Nombre de etiqueta",
 	"Name your model": "Asigne un nombre a su modelo",
 	"New Chat": "Nuevo Chat",
 	"New Password": "Nueva Contraseña",
-	"No content to speak": "No hay contenido para hablar",
+	"No content found": "",
+    "No content to speak": "No hay contenido para hablar",
 	"No file selected": "Ningún archivo fué seleccionado",
-	"No HTML, CSS, or JavaScript content found.": "",
-	"No knowledge found": "",
+	"No HTML, CSS, or JavaScript content found.": "No se encontró contenido HTML, CSS, o JavaScript.",
+	"No knowledge found": "No se encontró ningún conocimiento",
 	"No results found": "No se han encontrado resultados",
 	"No search query generated": "No se ha generado ninguna consulta de búsqueda",
 	"No source available": "No hay fuente disponible",
@@ -477,15 +479,15 @@
 	"On": "Activado",
 	"Only": "Solamente",
 	"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.",
-	"Only collections can be edited, create a new knowledge base to edit/add documents.": "",
+	"Only collections can be edited, create a new knowledge base to edit/add documents.": "Solo se pueden editar las colecciones, crear una nueva base de conocimientos para editar / añadir documentos",
 	"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! Hubo un error en la respuesta anterior. Intente de nuevo o póngase en contacto con el administrador.",
 	"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 file": "",
-	"Open in full screen": "",
+	"Open file": "Abrir archivo",
+	"Open in full screen": "Abrir en pantalla completa",
 	"Open new chat": "Abrir nuevo chat",
-	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
+	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "La versión de Open WebUI (v{{OPEN_WEBUI_VERSION}}) es inferior a la versión requerida (v{{REQUIRED_VERSION}})",
 	"OpenAI": "OpenAI",
 	"OpenAI API": "OpenAI API",
 	"OpenAI API Config": "OpenAI API Config",
@@ -493,9 +495,9 @@
 	"OpenAI URL/Key required.": "URL/Clave de OpenAI es requerida.",
 	"or": "o",
 	"Other": "Otro",
-	"Output format": "",
-	"Overview": "",
-	"page": "",
+	"Output format": "Formato de salida",
+	"Overview": "Vista general",
+	"page": "página",
 	"Password": "Contraseña",
 	"PDF document (.pdf)": "PDF document (.pdf)",
 	"PDF Extract Images (OCR)": "Extraer imágenes de PDF (OCR)",
@@ -504,8 +506,8 @@
 	"Permission denied when accessing microphone": "Permiso denegado al acceder a la micrófono",
 	"Permission denied when accessing microphone: {{error}}": "Permiso denegado al acceder al micrófono: {{error}}",
 	"Personalization": "Personalización",
-	"Pin": "",
-	"Pinned": "",
+	"Pin": "Fijar",
+	"Pinned": "Fijado",
 	"Pipeline deleted successfully": "Pipeline borrada exitosamente",
 	"Pipeline downloaded successfully": "Pipeline descargada exitosamente",
 	"Pipelines": "Pipelines",
@@ -513,9 +515,9 @@
 	"Pipelines Valves": "Tuberías Válvulas",
 	"Plain text (.txt)": "Texto plano (.txt)",
 	"Playground": "Patio de juegos",
-	"Please carefully review the following warnings:": "",
-	"Please fill in all fields.": "",
-	"Please select a reason": "",
+	"Please carefully review the following warnings:": "Por favor revise con cuidado los siguientes avisos:",
+	"Please fill in all fields.": "Por favor llene todos los campos.",
+	"Please select a reason": "Por favor seleccione una razón",
 	"Positive attitude": "Actitud positiva",
 	"Previous 30 days": "Últimos 30 días",
 	"Previous 7 days": "Últimos 7 días",
@@ -548,76 +550,76 @@
 	"Reset Vector Storage": "Restablecer almacenamiento vectorial",
 	"Response AutoCopy to Clipboard": "Copiar respuesta automáticamente al portapapeles",
 	"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Las notificaciones de respuesta no pueden activarse debido a que los permisos del sitio web han sido denegados. Por favor, visite las configuraciones de su navegador para otorgar el acceso necesario.",
-	"Response splitting": "",
+	"Response splitting": "División de respuestas",
 	"Role": "Rol",
 	"Rosé Pine": "Rosé Pine",
 	"Rosé Pine Dawn": "Rosé Pine Dawn",
 	"RTL": "RTL",
-	"Run": "",
-	"Run Llama 2, Code Llama, and other models. Customize and create your own.": "",
+	"Run": "Ejecutar",
+	"Run Llama 2, Code Llama, and other models. Customize and create your own.": "Correr Llama 2, Code Llama y otros modelos. Personalice y cree sus propios modelos.",
 	"Running": "Ejecutando",
 	"Save": "Guardar",
 	"Save & Create": "Guardar y Crear",
 	"Save & Update": "Guardar y Actualizar",
-	"Save As Copy": "",
-	"Save Tag": "",
-	"Saved": "",
+	"Save As Copy": "Guardar como copia",
+	"Save Tag": "Guardar etiqueta",
+	"Saved": "Guardado",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Ya no se admite guardar registros de chat directamente en el almacenamiento de su navegador. Tómese un momento para descargar y eliminar sus registros de chat haciendo clic en el botón a continuación. No te preocupes, puedes volver a importar fácilmente tus registros de chat al backend a través de",
-	"Scroll to bottom when switching between branches": "",
+	"Scroll to bottom when switching between branches": "Moverse a la parte inferior cuando se cambia entre ramas",
 	"Search": "Buscar",
 	"Search a model": "Buscar un modelo",
 	"Search Chats": "Chats de búsqueda",
-	"Search Collection": "",
+	"Search Collection": "Buscar Colección",
 	"Search Documents": "Buscar Documentos",
-	"Search Functions": "Funciones de Búsqueda",
-	"Search Knowledge": "",
-	"Search Models": "Modelos de búsqueda",
+	"Search Functions": "Buscar Funciones",
+	"Search Knowledge": "Buscar Conocimiento",
+	"Search Models": "Buscar Modelos",
 	"Search Prompts": "Buscar Prompts",
 	"Search Query Generation Prompt": "Búsqueda de consulta de generación de prompts",
 	"Search Result Count": "Recuento de resultados de búsqueda",
 	"Search Tools": "Búsqueda de herramientas",
-	"SearchApi API Key": "",
-	"SearchApi Engine": "",
+	"SearchApi API Key": "Clave API de SearchApi",
+	"SearchApi Engine": "Motor de SearchApi",
 	"Searched {{count}} sites_one": "Buscado {{count}} sites_one",
 	"Searched {{count}} sites_many": "Buscado {{count}} sites_many",
 	"Searched {{count}} sites_other": "Buscó {{count}} sites_other",
 	"Searching \"{{searchQuery}}\"": "Buscando \"{{searchQuery}}\"",
-	"Searching Knowledge for \"{{searchQuery}}\"": "",
+	"Searching Knowledge for \"{{searchQuery}}\"": "Buscando Conocimiento para \"{{searchQuery}}\"",
 	"Searxng Query URL": "Searxng URL de consulta",
 	"See readme.md for instructions": "Vea el readme.md para instrucciones",
 	"See what's new": "Ver las novedades",
 	"Seed": "Seed",
 	"Select a base model": "Seleccionar un modelo base",
 	"Select a engine": "Busca un motor",
-	"Select a function": "Busca una función",
+	"Select a file to view or drag and drop a file to upload": "",
+    "Select a function": "Busca una función",
 	"Select a model": "Selecciona un modelo",
 	"Select a pipeline": "Selección de una Pipeline",
 	"Select a pipeline url": "Selección de una dirección URL de Pipeline",
 	"Select a tool": "Busca una herramienta",
 	"Select an Ollama instance": "Seleccione una instancia de Ollama",
-	"Select Engine": "",
-	"Select Knowledge": "",
+	"Select Engine": "Selecciona Motor",
+	"Select Knowledge": "Selecciona Conocimiento",
 	"Select model": "Selecciona un modelo",
 	"Select only one model to call": "Selecciona sólo un modelo para llamar",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Los modelos seleccionados no admiten entradas de imagen",
 	"Send": "Enviar",
 	"Send a Message": "Enviar un Mensaje",
 	"Send message": "Enviar Mensaje",
-	"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
+	"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "Envia `stream_options: { include_usage: true }` en la solicitud.\nLos proveedores admitidos devolverán información de uso del token en la respuesta cuando se establezca.",
 	"September": "Septiembre",
 	"Serper API Key": "Clave API de Serper",
 	"Serply API Key": "Clave API de Serply",
 	"Serpstack API Key": "Clave API de Serpstack",
 	"Server connection verified": "Conexión del servidor verificada",
 	"Set as default": "Establecer por defecto",
-	"Set CFG Scale": "",
+	"Set CFG Scale": "Establecer la escala CFG",
 	"Set Default Model": "Establecer modelo predeterminado",
 	"Set embedding model (e.g. {{model}})": "Establecer modelo de embedding (ej. {{model}})",
 	"Set Image Size": "Establecer tamaño de imagen",
 	"Set reranking model (e.g. {{model}})": "Establecer modelo de reranking (ej. {{model}})",
-	"Set Sampler": "",
-	"Set Scheduler": "",
+	"Set Sampler": "Establecer Sampler",
+	"Set Scheduler": "Establecer Programador",
 	"Set Steps": "Establecer Pasos",
 	"Set Task Model": "Establecer modelo de tarea",
 	"Set Voice": "Establecer la voz",
@@ -634,13 +636,13 @@
 	"Show your support!": "¡Muestra tu apoyo!",
 	"Showcased creativity": "Creatividad mostrada",
 	"Sign in": "Iniciar sesión",
-	"Sign in to {{WEBUI_NAME}}": "",
+	"Sign in to {{WEBUI_NAME}}": "Iniciar sesión en {{WEBUI_NAME}}",
 	"Sign Out": "Cerrar sesión",
 	"Sign up": "Crear una cuenta",
-	"Sign up to {{WEBUI_NAME}}": "",
-	"Signing in to {{WEBUI_NAME}}": "",
+	"Sign up to {{WEBUI_NAME}}": "Crear una cuenta en {{WEBUI_NAME}}",
+	"Signing in to {{WEBUI_NAME}}": "Iniciando sesión en {{WEBUI_NAME}}",
 	"Source": "Fuente",
-	"Speech Playback Speed": "",
+	"Speech Playback Speed": "Velocidad de reproducción de voz",
 	"Speech recognition error: {{error}}": "Error de reconocimiento de voz: {{error}}",
 	"Speech-to-Text Engine": "Motor de voz a texto",
 	"Stop Sequence": "Detener secuencia",
@@ -652,9 +654,9 @@
 	"Success": "Éxito",
 	"Successfully updated.": "Actualizado exitosamente.",
 	"Suggested": "Sugerido",
-	"Support": "",
-	"Support this plugin:": "",
-	"Sync directory": "",
+	"Support": "Soporte",
+	"Support this plugin:": "Brinda soporte a este plugin:",
+	"Sync directory": "Sincroniza directorio",
 	"System": "Sistema",
 	"System Prompt": "Prompt del sistema",
 	"Tags": "Etiquetas",
@@ -663,26 +665,26 @@
 	"Tell us more:": "Dinos más:",
 	"Temperature": "Temperatura",
 	"Template": "Plantilla",
-	"Temporary Chat": "",
+	"Temporary Chat": "Chat temporal",
 	"Text Completion": "Finalización de texto",
 	"Text-to-Speech Engine": "Motor de texto a voz",
 	"Tfs Z": "Tfs Z",
 	"Thanks for your feedback!": "¡Gracias por tu retroalimentación!",
-	"The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "",
-	"The maximum file size in MB. If the file size exceeds this limit, the file will not be uploaded.": "",
-	"The maximum number of files that can be used at once in chat. If the number of files exceeds this limit, the files will not be uploaded.": "",
+	"The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "Los desarrolladores de este plugin son apasionados voluntarios de la comunidad. Si encuentras este plugin útil, por favor considere contribuir a su desarrollo.",
+	"The maximum file size in MB. If the file size exceeds this limit, the file will not be uploaded.": "El tamaño máximo del archivo en MB. Si el tamaño del archivo supera este límite, el archivo no se subirá.",
+	"The maximum number of files that can be used at once in chat. If the number of files exceeds this limit, the files will not be uploaded.": "El número máximo de archivos que se pueden utilizar a la vez en chat. Si este límite es superado, los archivos no se subirán.",
 	"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...": "Pensando...",
 	"This action cannot be undone. Do you wish to continue?": "Esta acción no se puede deshacer. ¿Desea continuar?",
 	"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.": "Esta es una característica experimental que puede no funcionar como se esperaba y está sujeto a cambios en cualquier momento.",
-	"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
+	"This option will delete all existing files in the collection and replace them with newly uploaded files.": " Esta opción eliminará todos los archivos existentes en la colección y los reemplazará con nuevos archivos subidos.",
 	"This will delete": "Esto eliminará",
-	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
+	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Esto reseteará la base de conocimientos y sincronizará todos los archivos. ¿Desea continuar?",
 	"Thorough explanation": "Explicación exhaustiva",
-	"Tika": "",
-	"Tika Server URL required.": "",
+	"Tika": "Tika",
+	"Tika Server URL required.": "URL del servidor de Tika",
 	"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",
 	"Title (e.g. Tell me a fun fact)": "Título (por ejemplo, cuéntame una curiosidad)",
@@ -692,9 +694,9 @@
 	"To access the available model names for downloading,": "Para acceder a los nombres de modelos disponibles para descargar,",
 	"To access the GGUF models available for downloading,": "Para acceder a los modelos GGUF disponibles para descargar,",
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Para acceder al interfaz de usuario web, por favor contacte al administrador. Los administradores pueden administrar los estados de los usuarios desde el panel de administración.",
-	"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "",
+	"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Para adjuntar la base de conocimientos aquí, agreguelas al área de trabajo \"Conocimiento\" primero.",
 	"to chat input.": "a la entrada del chat.",
-	"To select actions here, add them to the \"Functions\" workspace first.": "",
+	"To select actions here, add them to the \"Functions\" workspace first.": "Para seleccionar acciones aquí, agreguelas al área de trabajo \"Funciones\" primero.",
 	"To select filters here, add them to the \"Functions\" workspace first.": "Para seleccionar filtros aquí, agreguelos al área de trabajo \"Funciones\" primero.",
 	"To select toolkits here, add them to the \"Tools\" workspace first.": "Para seleccionar herramientas aquí, agreguelas al área de trabajo \"Herramientas\" primero.",
 	"Today": "Hoy",
@@ -705,13 +707,13 @@
 	"Tool deleted successfully": "Herramienta eliminada con éxito",
 	"Tool imported successfully": "Herramienta importada con éxito",
 	"Tool updated successfully": "Herramienta actualizada con éxito",
-	"Toolkit Description (e.g. A toolkit for performing various operations)": "",
-	"Toolkit ID (e.g. my_toolkit)": "",
-	"Toolkit Name (e.g. My ToolKit)": "",
+	"Toolkit Description (e.g. A toolkit for performing various operations)": "Descripción del Kit de herramientas (por ejemplo, una herramienta para realizar diversas operaciones)",
+	"Toolkit ID (e.g. my_toolkit)": "ID del Kit de Herramientas (por ejemplo, mi_herramienta)",
+	"Toolkit Name (e.g. My ToolKit)": "Nombre del Kit de Herramientas (por ejemplo, mi kit de herramientas)",
 	"Tools": "Herramientas",
-	"Tools are a function calling system with arbitrary code execution": "",
-	"Tools have a function calling system that allows arbitrary code execution": "",
-	"Tools have a function calling system that allows arbitrary code execution.": "",
+	"Tools are a function calling system with arbitrary code execution": "Las herramientas son un sistema de llamada de funciones con código arbitrario",
+	"Tools have a function calling system that allows arbitrary code execution": "Las herramientas tienen un sistema de llamadas de funciones que permite la ejecución de código arbitrario",
+	"Tools have a function calling system that allows arbitrary code execution.": "Las herramientas tienen un sistema de llamada de funciones que permite la ejecución de código arbitrario.",
 	"Top K": "Top K",
 	"Top P": "Top P",
 	"Trouble accessing Ollama?": "¿Problemas para acceder a Ollama?",
@@ -722,21 +724,22 @@
 	"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}}.",
 	"UI": "UI",
-	"Unpin": "",
+	"Unpin": "Desanclar",
 	"Update": "Actualizar",
 	"Update and Copy Link": "Actualizar y copiar enlace",
-	"Update for the latest features and improvements.": "",
+	"Update for the latest features and improvements.": "Actualize para las últimas características e mejoras.",
 	"Update password": "Actualizar contraseña",
+	"Updated": "",
 	"Updated at": "Actualizado en",
 	"Upload": "Subir",
 	"Upload a GGUF model": "Subir un modelo GGUF",
-	"Upload directory": "",
-	"Upload files": "",
+	"Upload directory": "Directorio de carga",
+	"Upload files": "Subir archivos",
 	"Upload Files": "Subir archivos",
 	"Upload Pipeline": "Subir Pipeline",
 	"Upload Progress": "Progreso de carga",
 	"URL Mode": "Modo de URL",
-	"Use '#' in the prompt input to load and include your knowledge.": "",
+	"Use '#' in the prompt input to load and include your knowledge.": "Utilize '#' en el prompt para cargar y incluir su conocimiento.",
 	"Use '#' in the prompt input to load and select your documents.": "Utilice '#' en el prompt para cargar y seleccionar sus documentos.",
 	"Use Gravatar": "Usar Gravatar",
 	"Use Initials": "Usar Iniciales",
@@ -754,10 +757,10 @@
 	"variable": "variable",
 	"variable to have them replaced with clipboard content.": "variable para reemplazarlos con el contenido del portapapeles.",
 	"Version": "Versión",
-	"Version {{selectedVersion}} of {{totalVersions}}": "",
+	"Version {{selectedVersion}} of {{totalVersions}}": "Versión {{selectedVersion}} de {{totalVersions}}",
 	"Voice": "Voz",
 	"Warning": "Advertencia",
-	"Warning:": "",
+	"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",
 	"Web API": "API Web",
@@ -783,7 +786,7 @@
 	"You're a helpful assistant.": "Usted es un asistente útil.",
 	"You're now logged in.": "Usted ahora está conectado.",
 	"Your account status is currently pending activation.": "El estado de su cuenta actualmente se encuentra pendiente de activación.",
-	"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "",
+	"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "Su contribución completa irá directamente a el desarrollador del plugin; Open WebUI no toma ningun porcentaje. Sin embargo, la plataforma de financiación elegida podría tener sus propias tarifas.",
 	"Youtube": "Youtube",
 	"Youtube Loader Settings": "Configuración del cargador de Youtube"
 }

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "اکسپورت از پرامپت\u200cها",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "ایجاد کلید API با خطا مواجه شد.",
 	"Failed to read clipboard contents": "خواندن محتوای کلیپ بورد ناموفق بود",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "نام مدل خود را",
 	"New Chat": "گپ جدید",
 	"New Password": "رمز عبور جدید",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "Seed",
 	"Select a base model": "انتخاب یک مدل پایه",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "انتخاب یک مدل",
 	"Select a pipeline": "انتخاب یک خط لوله",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "انتخاب یک مدل",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "مدل) های (انتخاب شده ورودیهای تصویر را پشتیبانی نمیکند",
 	"Send": "ارسال",
 	"Send a Message": "ارسال یک پیام",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "به روزرسانی و کپی لینک",
 	"Update for the latest features and improvements.": "",
 	"Update password": "به روزرسانی رمزعبور",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "آپلود یک مدل GGUF",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Vie kehotteet",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "API-avaimen luonti epäonnistui.",
 	"Failed to read clipboard contents": "Leikepöydän sisällön lukeminen epäonnistui",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "Mallin nimeäminen",
 	"New Chat": "Uusi keskustelu",
 	"New Password": "Uusi salasana",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "Siemen",
 	"Select a base model": "Valitse perusmalli",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "Valitse malli",
 	"Select a pipeline": "Valitse putki",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "Valitse malli",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Valitut mallit eivät tue kuvasyötteitä",
 	"Send": "Lähetä",
 	"Send a Message": "Lähetä viesti",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "Päivitä ja kopioi linkki",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Päivitä salasana",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "Lataa GGUF-malli",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Exporter les Prompts",
 	"Export Tools": "Outils d'exportation",
 	"External Models": "Modèles externes",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Échec de la création de la clé API.",
 	"Failed to read clipboard contents": "Échec de la lecture du contenu du presse-papiers",
 	"Failed to update settings": "Échec de la mise à jour des paramètres",
@@ -449,6 +450,7 @@
 	"Name your model": "Nommez votre modèle",
 	"New Chat": "Nouvelle conversation",
 	"New Password": "Nouveau mot de passe",
+	"No content found": "",
 	"No content to speak": "Rien à signaler",
 	"No file selected": "Aucun fichier sélectionné",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -589,6 +591,7 @@
 	"Seed": "Graine",
 	"Select a base model": "Sélectionnez un modèle de base",
 	"Select a engine": "Sélectionnez un moteur",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Sélectionnez une fonction",
 	"Select a model": "Sélectionnez un modèle",
 	"Select a pipeline": "Sélectionnez un pipeline",
@@ -599,7 +602,6 @@
 	"Select Knowledge": "",
 	"Select model": "Sélectionnez un modèle",
 	"Select only one model to call": "Sélectionnez seulement un modèle pour appeler",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Les modèle(s) sélectionné(s) ne prennent pas en charge les entrées d'images",
 	"Send": "Envoyer",
 	"Send a Message": "Envoyer un message",
@@ -727,6 +729,7 @@
 	"Update and Copy Link": "Mettre à jour et copier le lien",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Mettre à jour le mot de passe",
+	"Updated": "",
 	"Updated at": "Mise à jour le",
 	"Upload": "Télécharger",
 	"Upload a GGUF model": "Téléverser un modèle GGUF",

+ 63 - 60
src/lib/i18n/locales/fr-FR/translation.json

@@ -9,7 +9,7 @@
 	"{{user}}'s Chats": "Conversations de {{user}}",
 	"{{webUIName}} Backend Required": "Backend {{webUIName}} requis",
 	"*Prompt node ID(s) are required for image generation": "*Les ID de noeud du prompt sont nécessaires pour la génération d’images",
-	"A new version (v{{LATEST_VERSION}}) is now available.": "",
+	"A new version (v{{LATEST_VERSION}}) is now available.": "Une nouvelle version (v{{LATEST_VERSION}}) est disponible.",
 	"A task model is used when performing tasks such as generating titles for chats and web search queries": "Un modèle de tâche est utilisé lors de l’exécution de tâches telles que la génération de titres pour les conversations et les requêtes de recherche sur le web.",
 	"a user": "un utilisateur",
 	"About": "À propos",
@@ -23,8 +23,8 @@
 	"Add a short description about what this model does": "Ajoutez une brève description de ce que fait ce modèle.",
 	"Add a short title for this prompt": "Ajoutez un bref titre pour cette prompt.",
 	"Add a tag": "Ajouter une étiquette",
-	"Add Content": "",
-	"Add content here": "",
+	"Add Content": "Ajouter du contenu",
+	"Add content here": "Ajoutez du contenu ici",
 	"Add custom prompt": "Ajouter une prompt personnalisée",
 	"Add Docs": "Ajouter de la documentation",
 	"Add Files": "Ajouter des fichiers",
@@ -33,7 +33,7 @@
 	"Add Model": "Ajouter un modèle",
 	"Add Tag": "Ajouter une étiquette",
 	"Add Tags": "Ajouter des étiquettes",
-	"Add text content": "",
+	"Add text content": "Ajouter du contenu texte",
 	"Add User": "Ajouter un utilisateur",
 	"Adjusting these settings will apply changes universally to all users.": "L'ajustement de ces paramètres appliquera universellement les changements à tous les utilisateurs.",
 	"admin": "administrateur",
@@ -68,8 +68,8 @@
 	"Archived Chats": "Conversations archivées",
 	"are allowed - Activate this command by typing": "sont autorisés - Activer cette commande en tapant",
 	"Are you sure?": "Êtes-vous certain ?",
-	"Artifacts": "",
-	"Ask a question": "",
+	"Artifacts": "Artéfacts",
+	"Ask a question": "Posez votre question",
 	"Attach file": "Joindre un document",
 	"Attention to detail": "Attention aux détails",
 	"Audio": "Audio",
@@ -79,10 +79,10 @@
 	"AUTOMATIC1111 Api Auth String": "AUTOMATIC1111 Chaîne d'authentification de l'API",
 	"AUTOMATIC1111 Base URL": "URL de base AUTOMATIC1111",
 	"AUTOMATIC1111 Base URL is required.": "L'URL de base {AUTOMATIC1111} est requise.",
-	"Available list": "",
+	"Available list": "Liste disponible",
 	"available!": "disponible !",
-	"Azure AI Speech": "",
-	"Azure Region": "",
+	"Azure AI Speech": "Azure AI Speech",
+	"Azure Region": "Région Azure",
 	"Back": "Retour en arrière",
 	"Bad Response": "Mauvaise réponse",
 	"Banners": "Bannières",
@@ -103,7 +103,7 @@
 	"Chat Bubble UI": "Bulles de chat",
 	"Chat Controls": "Contrôles du chat",
 	"Chat direction": "Direction du chat",
-	"Chat Overview": "",
+	"Chat Overview": "Aperçu du chat",
 	"Chats": "Conversations",
 	"Check Again": "Vérifiez à nouveau.",
 	"Check for updates": "Vérifier les mises à jour disponibles",
@@ -158,7 +158,7 @@
 	"Copying to clipboard was successful!": "La copie dans le presse-papiers a réussi !",
 	"Create a model": "Créer un modèle",
 	"Create Account": "Créer un compte",
-	"Create Knowledge": "",
+	"Create Knowledge": "Créer une connaissance",
 	"Create new key": "Créer une nouvelle clé",
 	"Create new secret key": "Créer une nouvelle clé secrète",
 	"Created at": "Créé à",
@@ -248,7 +248,7 @@
 	"Enter a detail about yourself for your LLMs to recall": "Saisissez un détail sur vous-même que vos LLMs pourront se rappeler",
 	"Enter api auth string (e.g. username:password)": "Entrez la chaîne d'authentification de l'API (par ex. nom d'utilisateur:mot de passe)",
 	"Enter Brave Search API Key": "Entrez la clé API Brave Search",
-	"Enter CFG Scale (e.g. 7.0)": "",
+	"Enter CFG Scale (e.g. 7.0)": "Entrez l'échelle CFG (par ex. 7.0)",
 	"Enter Chunk Overlap": "Entrez le chevauchement des chunks",
 	"Enter Chunk Size": "Entrez la taille des chunks",
 	"Enter Github Raw URL": "Entrez l'URL brute de GitHub",
@@ -259,8 +259,8 @@
 	"Enter Model ID": "Entrez l'ID du modèle",
 	"Enter model tag (e.g. {{modelTag}})": "Entrez l'étiquette du modèle (par ex. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Entrez le nombre d'étapes (par ex. 50)",
-	"Enter Sampler (e.g. Euler a)": "",
-	"Enter Scheduler (e.g. Karras)": "",
+	"Enter Sampler (e.g. Euler a)": "Entrez le sampler (par ex. Euler a)",
+	"Enter Scheduler (e.g. Karras)": "Entrez le planificateur (par ex. Karras)",
 	"Enter Score": "Entrez votre score",
 	"Enter SearchApi API Key": "Entrez la clé API SearchApi",
 	"Enter SearchApi Engine": "Entrez le moteur de recherche SearchApi",
@@ -294,18 +294,19 @@
 	"Export Prompts": "Exporter des prompts",
 	"Export Tools": "Outils d'exportation",
 	"External Models": "Modèles externes",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Échec de la création de la clé API.",
 	"Failed to read clipboard contents": "Échec de la lecture du contenu du presse-papiers",
 	"Failed to update settings": "Échec de la mise à jour des paramètres",
-	"Failed to upload file.": "",
+	"Failed to upload file.": "Échec du téléchargement du fichier.",
 	"February": "Février",
 	"Feel free to add specific details": "N'hésitez pas à ajouter des détails spécifiques",
 	"File": "Fichier",
-	"File added successfully.": "",
-	"File content updated successfully.": "",
+	"File added successfully.": "Fichier ajouté avec succès.",
+	"File content updated successfully.": "Contenu du fichier mis à jour avec succès.",
 	"File Mode": "Mode fichier",
 	"File not found.": "Fichier introuvable.",
-	"File removed successfully.": "",
+	"File removed successfully.": "Fichier supprimé avec succès.",
 	"File size should not exceed {{maxSize}} MB.": "La taille du fichier ne doit pas dépasser {{maxSize}} Mo.",
 	"Files": "Fichiers",
 	"Filter is now globally disabled": "Le filtre est maintenant désactivé globalement",
@@ -379,12 +380,12 @@
 	"JWT Token": "Jeton JWT",
 	"Keep Alive": "Temps de maintien connecté",
 	"Keyboard shortcuts": "Raccourcis clavier",
-	"Knowledge": "Connaissance",
-	"Knowledge created successfully.": "",
-	"Knowledge deleted successfully.": "",
-	"Knowledge reset successfully.": "",
-	"Knowledge updated successfully": "",
-	"Landing Page Mode": "",
+	"Knowledge": "Connaissances",
+	"Knowledge created successfully.": "Connaissance créée avec succès.",
+	"Knowledge deleted successfully.": "Connaissance supprimée avec succès.",
+	"Knowledge reset successfully.": "Connaissance réinitialisée avec succès.",
+	"Knowledge updated successfully": "Connaissance mise à jour avec succès",
+	"Landing Page Mode": "Mode de la page d'accueil",
 	"Language": "Langue",
 	"large language models, locally.": "grand modèle de langage, localement.",
 	"Last Active": "Dernière activité",
@@ -430,8 +431,8 @@
 	"Model {{modelId}} not found": "Modèle {{modelId}} introuvable",
 	"Model {{modelName}} is not vision capable": "Le modèle {{modelName}} n'a pas de capacités visuelles",
 	"Model {{name}} is now {{status}}": "Le modèle {{name}} est désormais {{status}}.",
-	"Model {{name}} is now at the top": "",
-	"Model accepts image inputs": "",
+	"Model {{name}} is now at the top": "Le modèle {{name}} est désormais en haut",
+	"Model accepts image inputs": "Le modèle accepte les images en entrée",
 	"Model created successfully!": "Le modèle a été créé avec succès !",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Chemin du système de fichiers de modèle détecté. Le nom court du modèle est requis pour la mise à jour, l'opération ne peut pas être poursuivie.",
 	"Model ID": "ID du modèle",
@@ -443,16 +444,17 @@
 	"Modelfile Content": "Contenu du Fichier de Modèle",
 	"Models": "Modèles",
 	"More": "Plus de",
-	"Move to Top": "",
+	"Move to Top": "Déplacer en haut",
 	"Name": "Nom d'utilisateur",
 	"Name Tag": "Nom de l'étiquette",
 	"Name your model": "Nommez votre modèle",
 	"New Chat": "Nouvelle conversation",
 	"New Password": "Nouveau mot de passe",
+	"No content found": "",
 	"No content to speak": "Rien à signaler",
 	"No file selected": "Aucun fichier sélectionné",
-	"No HTML, CSS, or JavaScript content found.": "",
-	"No knowledge found": "",
+	"No HTML, CSS, or JavaScript content found.": "Aucun contenu HTML, CSS ou JavaScript trouvé.",
+	"No knowledge found": "Aucune connaissance trouvée",
 	"No results found": "Aucun résultat trouvé",
 	"No search query generated": "Aucune requête de recherche générée",
 	"No source available": "Aucune source n'est disponible",
@@ -477,13 +479,13 @@
 	"On": "Activé",
 	"Only": "Seulement",
 	"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.",
-	"Only collections can be edited, create a new knowledge base to edit/add documents.": "",
+	"Only collections can be edited, create a new knowledge base to edit/add documents.": "Seules les collections peuvent être modifiées, créez une nouvelle base de connaissance pour modifier/ajouter des documents.",
 	"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 ! Un instant ! Vos fichiers sont toujours en train d'être traités. Nous les perfectionnons pour vous. Veuillez patienter, 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. Veuillez vérifier à nouveau et réessayer.",
 	"Oops! There was an error in the previous response. Please try again or contact admin.": "Oops ! Il y a eu une erreur dans la réponse précédente. Veuillez réessayer ou contacter l'administrateur.",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oups\u00a0! Vous utilisez une méthode non prise en charge (frontend uniquement). Veuillez servir l'interface Web à partir du backend.",
-	"Open file": "",
-	"Open in full screen": "",
+	"Open file": "Ouvrir le fichier",
+	"Open in full screen": "Ouvrir en plein écran",
 	"Open new chat": "Ouvrir une nouvelle conversation",
 	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "La version Open WebUI (v{{OPEN_WEBUI_VERSION}}) est inférieure à la version requise (v{{REQUIRED_VERSION}})",
 	"OpenAI": "OpenAI",
@@ -493,9 +495,9 @@
 	"OpenAI URL/Key required.": "URL/Clé OpenAI requise.",
 	"or": "ou",
 	"Other": "Autre",
-	"Output format": "",
-	"Overview": "",
-	"page": "",
+	"Output format": "Format de sortie",
+	"Overview": "Aperçu",
+	"page": "page",
 	"Password": "Mot de passe",
 	"PDF document (.pdf)": "Document au format PDF  (.pdf)",
 	"PDF Extract Images (OCR)": "Extraction d'images PDF (OCR)",
@@ -514,8 +516,8 @@
 	"Plain text (.txt)": "Texte simple (.txt)",
 	"Playground": "Playground",
 	"Please carefully review the following warnings:": "Veuillez lire attentivement les avertissements suivants :",
-	"Please fill in all fields.": "",
-	"Please select a reason": "",
+	"Please fill in all fields.": "Veuillez remplir tous les champs.",
+	"Please select a reason": "Veuillez sélectionner une raison",
 	"Positive attitude": "Attitude positive",
 	"Previous 30 days": "30 derniers jours",
 	"Previous 7 days": "7 derniers jours",
@@ -559,18 +561,18 @@
 	"Save": "Enregistrer",
 	"Save & Create": "Enregistrer & Créer",
 	"Save & Update": "Enregistrer & Mettre à jour",
-	"Save As Copy": "",
+	"Save As Copy": "Enregistrer comme copie",
 	"Save Tag": "Enregistrer l'étiquette",
-	"Saved": "",
+	"Saved": "Enregistré",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "La sauvegarde des journaux de conversation directement dans le stockage de votre navigateur n'est plus prise en charge. Veuillez prendre un instant pour télécharger et supprimer vos journaux de conversation en cliquant sur le bouton ci-dessous. Ne vous inquiétez pas, vous pouvez facilement réimporter vos journaux de conversation dans le backend via",
 	"Scroll to bottom when switching between branches": "Défiler vers le bas lors du passage d'une branche à l'autre",
 	"Search": "Recherche",
 	"Search a model": "Rechercher un modèle",
 	"Search Chats": "Rechercher des conversations",
-	"Search Collection": "",
+	"Search Collection": "Rechercher une collection",
 	"Search Documents": "Rechercher des documents",
 	"Search Functions": "Rechercher des fonctions",
-	"Search Knowledge": "",
+	"Search Knowledge": "Rechercher des connaissances",
 	"Search Models": "Rechercher des modèles",
 	"Search Prompts": "Rechercher des prompts",
 	"Search Query Generation Prompt": "Génération d'interrogation de recherche",
@@ -589,6 +591,7 @@
 	"Seed": "Graine",
 	"Select a base model": "Sélectionnez un modèle de base",
 	"Select a engine": "Sélectionnez un moteur",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Sélectionnez une fonction",
 	"Select a model": "Sélectionnez un modèle",
 	"Select a pipeline": "Sélectionnez un pipeline",
@@ -596,10 +599,9 @@
 	"Select a tool": "Sélectionnez un outil",
 	"Select an Ollama instance": "Sélectionnez une instance Ollama",
 	"Select Engine": "Sélectionnez le moteur",
-	"Select Knowledge": "",
+	"Select Knowledge": "Sélectionnez une connaissance",
 	"Select model": "Sélectionnez un modèle",
 	"Select only one model to call": "Sélectionnez seulement un modèle pour appeler",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Les modèle(s) sélectionné(s) ne prennent pas en charge les entrées d'images",
 	"Send": "Envoyer",
 	"Send a Message": "Envoyer un message",
@@ -611,13 +613,13 @@
 	"Serpstack API Key": "Clé API Serpstack",
 	"Server connection verified": "Connexion au serveur vérifiée",
 	"Set as default": "Définir comme valeur par défaut",
-	"Set CFG Scale": "",
+	"Set CFG Scale": "Définir la CFG",
 	"Set Default Model": "Définir le modèle par défaut",
 	"Set embedding model (e.g. {{model}})": "Définir le modèle d'embedding (par ex. {{model}})",
 	"Set Image Size": "Définir la taille de l'image",
 	"Set reranking model (e.g. {{model}})": "Définir le modèle de ré-ranking (par ex. {{model}})",
-	"Set Sampler": "",
-	"Set Scheduler": "",
+	"Set Sampler": "Définir le sampler",
+	"Set Scheduler": "Définir le planificateur",
 	"Set Steps": "Définir le nombre d'étapes",
 	"Set Task Model": "Définir le modèle de tâche",
 	"Set Voice": "Choisir la voix",
@@ -634,17 +636,17 @@
 	"Show your support!": "Montre ton soutien !",
 	"Showcased creativity": "Créativité mise en avant",
 	"Sign in": "S'identifier",
-	"Sign in to {{WEBUI_NAME}}": "",
+	"Sign in to {{WEBUI_NAME}}": "Connectez-vous à {{WEBUI_NAME}}",
 	"Sign Out": "Déconnexion",
 	"Sign up": "Inscrivez-vous",
-	"Sign up to {{WEBUI_NAME}}": "",
-	"Signing in to {{WEBUI_NAME}}": "",
+	"Sign up to {{WEBUI_NAME}}": "Inscrivez-vous à {{WEBUI_NAME}}",
+	"Signing in to {{WEBUI_NAME}}": "Connexion à {{WEBUI_NAME}}",
 	"Source": "Source",
-	"Speech Playback Speed": "",
+	"Speech Playback Speed": "Vitesse de lecture de la parole",
 	"Speech recognition error: {{error}}": "Erreur de reconnaissance vocale\u00a0: {{error}}",
 	"Speech-to-Text Engine": "Moteur de reconnaissance vocale",
 	"Stop Sequence": "Séquence d'arrêt",
-	"Stream Chat Response": "",
+	"Stream Chat Response": "Streamer la réponse de la conversation",
 	"STT Model": "Modèle de Speech-to-Text",
 	"STT Settings": "Paramètres de Speech-to-Text",
 	"Submit": "Soumettre",
@@ -654,7 +656,7 @@
 	"Suggested": "Sugéré",
 	"Support": "Supporter",
 	"Support this plugin:": "Supporter ce module",
-	"Sync directory": "",
+	"Sync directory": "Synchroniser le répertoire",
 	"System": "Système",
 	"System Prompt": "Prompt du système",
 	"Tags": "Étiquettes",
@@ -677,9 +679,9 @@
 	"This action cannot be undone. Do you wish to continue?": "Cette action ne peut pas être annulée. Souhaitez-vous continuer ?",
 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Cela garantit que vos conversations précieuses soient sauvegardé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.": "Il s'agit d'une fonctionnalité expérimentale, elle peut ne pas fonctionner comme prévu et est sujette à modification à tout moment.",
-	"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
+	"This option will delete all existing files in the collection and replace them with newly uploaded files.": "Cette option supprimera tous les fichiers existants dans la collection et les remplacera par les fichiers nouvellement téléchargés.",
 	"This will delete": "Cela supprimera",
-	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
+	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Cela réinitialisera la base de connaissances et synchronisera tous les fichiers. Souhaitez-vous continuer ?",
 	"Thorough explanation": "Explication approfondie",
 	"Tika": "Tika",
 	"Tika Server URL required.": "URL du serveur Tika requise.",
@@ -692,7 +694,7 @@
 	"To access the available model names for downloading,": "Pour accéder aux noms des modèles disponibles,",
 	"To access the GGUF models available for downloading,": "Pour accéder aux modèles GGUF disponibles,",
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Pour accéder à l'interface Web, veuillez contacter l'administrateur. Les administrateurs peuvent gérer les statuts des utilisateurs depuis le panneau d'administration.",
-	"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "",
+	"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Pour attacher une base de connaissances ici, ajoutez-les d'abord à l'espace de travail « Connaissances ».",
 	"to chat input.": "Vers la zone de chat.",
 	"To select actions here, add them to the \"Functions\" workspace first.": "Pour sélectionner des actions ici, ajoutez-les d'abord à l'espace de travail « Fonctions ».",
 	"To select filters here, add them to the \"Functions\" workspace first.": "Pour sélectionner des filtres ici, ajoutez-les d'abord à l'espace de travail « Fonctions ». ",
@@ -725,18 +727,19 @@
 	"Unpin": "Désépingler",
 	"Update": "Mise à jour",
 	"Update and Copy Link": "Mettre à jour et copier le lien",
-	"Update for the latest features and improvements.": "",
+	"Update for the latest features and improvements.": "Mettez à jour pour bénéficier des dernières fonctionnalités et améliorations.",
 	"Update password": "Mettre à jour le mot de passe",
+	"Updated": "",
 	"Updated at": "Mise à jour le",
 	"Upload": "Télécharger",
 	"Upload a GGUF model": "Téléverser un modèle GGUF",
-	"Upload directory": "",
-	"Upload files": "",
+	"Upload directory": "Répertoire de téléchargement",
+	"Upload files": "Télécharger des fichiers",
 	"Upload Files": "Télécharger des fichiers",
 	"Upload Pipeline": "Pipeline de téléchargement",
 	"Upload Progress": "Progression de l'envoi",
 	"URL Mode": "Mode d'URL",
-	"Use '#' in the prompt input to load and include your knowledge.": "",
+	"Use '#' in the prompt input to load and include your knowledge.": "Utilisez '#' dans la zone de saisie du prompt pour charger et inclure vos connaissances.",
 	"Use '#' in the prompt input to load and select your documents.": "Utilisez '#' dans la zone de saisie du prompt pour charger et sélectionner vos documents.",
 	"Use Gravatar": "Utilisez Gravatar",
 	"Use Initials": "Utiliser les initiales",
@@ -754,7 +757,7 @@
 	"variable": "variable",
 	"variable to have them replaced with clipboard content.": "variable pour qu'elles soient remplacées par le contenu du presse-papiers.",
 	"Version": "version:",
-	"Version {{selectedVersion}} of {{totalVersions}}": "",
+	"Version {{selectedVersion}} of {{totalVersions}}": "Version {{selectedVersion}} de {{totalVersions}}",
 	"Voice": "Voix",
 	"Warning": "Avertissement !",
 	"Warning:": "Avertissement :",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "ייצוא פקודות",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "יצירת מפתח API נכשלה.",
 	"Failed to read clipboard contents": "קריאת תוכן הלוח נכשלה",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "תן שם לדגם שלך",
 	"New Chat": "צ'אט חדש",
 	"New Password": "סיסמה חדשה",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -589,6 +591,7 @@
 	"Seed": "זרע",
 	"Select a base model": "בחירת מודל בסיס",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "בחר מודל",
 	"Select a pipeline": "בחר קו צינור",
@@ -599,7 +602,6 @@
 	"Select Knowledge": "",
 	"Select model": "בחר מודל",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "דגמים נבחרים אינם תומכים בקלט תמונה",
 	"Send": "שלח",
 	"Send a Message": "שלח הודעה",
@@ -727,6 +729,7 @@
 	"Update and Copy Link": "עדכן ושכפל קישור",
 	"Update for the latest features and improvements.": "",
 	"Update password": "עדכן סיסמה",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "העלה מודל GGUF",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "प्रॉम्प्ट निर्यात करें",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "एपीआई कुंजी बनाने में विफल.",
 	"Failed to read clipboard contents": "क्लिपबोर्ड सामग्री पढ़ने में विफल",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "अपने मॉडल को नाम दें",
 	"New Chat": "नई चैट",
 	"New Password": "नया पासवर्ड",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "सीड्\u200c",
 	"Select a base model": "एक आधार मॉडल का चयन करें",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "एक मॉडल चुनें",
 	"Select a pipeline": "एक पाइपलाइन का चयन करें",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "मॉडल चुनें",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "चयनित मॉडल छवि इनपुट का समर्थन नहीं करते हैं",
 	"Send": "भेज",
 	"Send a Message": "एक संदेश भेजो",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "अपडेट करें और लिंक कॉपी करें",
 	"Update for the latest features and improvements.": "",
 	"Update password": "पासवर्ड अपडेट करें",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "GGUF मॉडल अपलोड करें",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Izvoz prompta",
 	"Export Tools": "Izvoz alata",
 	"External Models": "Vanjski modeli",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Neuspješno stvaranje API ključa.",
 	"Failed to read clipboard contents": "Neuspješno čitanje sadržaja međuspremnika",
 	"Failed to update settings": "Greška kod ažuriranja postavki",
@@ -449,6 +450,7 @@
 	"Name your model": "Dodijelite naziv modelu",
 	"New Chat": "Novi razgovor",
 	"New Password": "Nova lozinka",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -589,6 +591,7 @@
 	"Seed": "Sjeme",
 	"Select a base model": "Odabir osnovnog modela",
 	"Select a engine": "Odaberite pogon",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "Odaberite model",
 	"Select a pipeline": "Odabir kanala",
@@ -599,7 +602,6 @@
 	"Select Knowledge": "",
 	"Select model": "Odaberite model",
 	"Select only one model to call": "Odaberite samo jedan model za poziv",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Odabrani modeli ne podržavaju unose slika",
 	"Send": "Pošalji",
 	"Send a Message": "Pošaljite poruku",
@@ -727,6 +729,7 @@
 	"Update and Copy Link": "Ažuriraj i kopiraj vezu",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Ažuriraj lozinku",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "Učitaj GGUF model",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Perintah Ekspor",
 	"Export Tools": "Alat Ekspor",
 	"External Models": "Model Eksternal",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Gagal membuat API Key.",
 	"Failed to read clipboard contents": "Gagal membaca konten papan klip",
 	"Failed to update settings": "Gagal memperbarui pengaturan",
@@ -449,6 +450,7 @@
 	"Name your model": "Beri nama model Anda",
 	"New Chat": "Obrolan Baru",
 	"New Password": "Kata Sandi Baru",
+	"No content found": "",
 	"No content to speak": "Tidak ada konten untuk dibicarakan",
 	"No file selected": "Tidak ada file yang dipilih",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "Benih",
 	"Select a base model": "Pilih model dasar",
 	"Select a engine": "Pilih mesin",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Memilih fungsi",
 	"Select a model": "Pilih model",
 	"Select a pipeline": "Pilih saluran pipa",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "Pilih model",
 	"Select only one model to call": "Pilih hanya satu model untuk dipanggil",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Model yang dipilih tidak mendukung input gambar",
 	"Send": "Kirim",
 	"Send a Message": "Kirim Pesan",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "Perbarui dan Salin Tautan",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Perbarui kata sandi",
+	"Updated": "",
 	"Updated at": "Diperbarui di",
 	"Upload": "Unggah",
 	"Upload a GGUF model": "Unggah model GGUF",

+ 4 - 1
src/lib/i18n/locales/ie-GA/translation.json

@@ -294,6 +294,7 @@
 	"Export Prompts": "Leideanna Easpórtála",
 	"Export Tools": "Uirlisí Easpór",
 	"External Models": "Samhlacha Seachtracha",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Theip ar an eochair API a chruthú.",
 	"Failed to read clipboard contents": "Theip ar ábhar gearrthaisce a lé",
 	"Failed to update settings": "Theip ar shocruithe a nuashonrú",
@@ -449,6 +450,7 @@
 	"Name your model": "Ainmnigh do mhúnla",
 	"New Chat": "Comhrá Nua",
 	"New Password": "Pasfhocal Nua",
+	"No content found": "",
 	"No content to speak": "Níl aon ábhar le labhairt",
 	"No file selected": "Níl aon chomhad roghnaithe",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "Síol",
 	"Select a base model": "Roghnaigh bunmhúnla",
 	"Select a engine": "Roghnaigh inneall",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Roghnaigh feidhm",
 	"Select a model": "Roghnaigh samhail",
 	"Select a pipeline": "Roghnaigh píblíne",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "Roghnaigh samhail",
 	"Select only one model to call": "Roghnaigh ach samhail amháin le glaoch",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Ní thacaíonn samhail(í) roghnaithe le hionchur íomhá",
 	"Send": "Seol",
 	"Send a Message": "Seol Teachtaireacht",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "Nuashonraigh agus Cóipeáil Nasc",
 	"Update for the latest features and improvements.": "Nuashonrú le haghaidh na gnéithe agus na feabhsuithe is déanaí.",
 	"Update password": "Nuashonrú pasfhocal",
+	"Updated": "",
 	"Updated at": "Nuashonraithe ag",
 	"Upload": "Uaslódáil",
 	"Upload a GGUF model": "Uaslódáil samhail GGUF",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Esporta prompt",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Impossibile creare la chiave API.",
 	"Failed to read clipboard contents": "Impossibile leggere il contenuto degli appunti",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "Assegna un nome al tuo modello",
 	"New Chat": "Nuova chat",
 	"New Password": "Nuova password",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -589,6 +591,7 @@
 	"Seed": "Seme",
 	"Select a base model": "Selezionare un modello di base",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "Seleziona un modello",
 	"Select a pipeline": "Selezionare una tubazione",
@@ -599,7 +602,6 @@
 	"Select Knowledge": "",
 	"Select model": "Seleziona modello",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "I modelli selezionati non supportano l'input di immagini",
 	"Send": "Invia",
 	"Send a Message": "Invia un messaggio",
@@ -727,6 +729,7 @@
 	"Update and Copy Link": "Aggiorna e copia link",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Aggiorna password",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "Carica un modello GGUF",

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

@@ -9,38 +9,38 @@
 	"{{user}}'s Chats": "{{user}} のチャット",
 	"{{webUIName}} Backend Required": "{{webUIName}} バックエンドが必要です",
 	"*Prompt node ID(s) are required for image generation": "",
-	"A new version (v{{LATEST_VERSION}}) is now available.": "",
+	"A new version (v{{LATEST_VERSION}}) is now available.": "新しいバージョンがReleaseされています。",
 	"A task model is used when performing tasks such as generating titles for chats and web search queries": "タスクモデルは、チャットやWeb検索クエリのタイトルの生成などのタスクを実行するときに使用されます",
 	"a user": "ユーザー",
 	"About": "概要",
 	"Account": "アカウント",
-	"Account Activation Pending": "",
+	"Account Activation Pending": "アカウント承認待ち",
 	"Accurate information": "情報の正確性",
-	"Actions": "",
-	"Active Users": "",
+	"Actions": "アクション",
+	"Active Users": "アクティブユーザー",
 	"Add": "追加",
 	"Add a model id": "モデル ID を追加する",
 	"Add a short description about what this model does": "このモデルの機能に関する簡単な説明を追加します",
-	"Add a short title for this prompt": "このプロンプトの短いタイトルを追加",
+	"Add a short title for this prompt": "このプロンプトへ名前を追加",
 	"Add a tag": "タグを追加",
-	"Add Content": "",
-	"Add content here": "",
+	"Add Content": "コンテンツを追加",
+	"Add content here": "ここへコンテンツを追加",
 	"Add custom prompt": "カスタムプロンプトを追加",
 	"Add Docs": "ドキュメントを追加",
 	"Add Files": "ファイルを追加",
 	"Add Memory": "メモリを追加",
 	"Add message": "メッセージを追加",
 	"Add Model": "モデルを追加",
-	"Add Tag": "",
+	"Add Tag": "タグを追加",
 	"Add Tags": "タグを追加",
-	"Add text content": "",
+	"Add text content": "コンテンツを追加",
 	"Add User": "ユーザーを追加",
-	"Adjusting these settings will apply changes universally to all users.": "これらの設定を調整すると、すべてのユーザーに普遍的に変更が適用されます。",
+	"Adjusting these settings will apply changes universally to all users.": "これらの設定を調整すると、すべてのユーザーに変更が適用されます。",
 	"admin": "管理者",
-	"Admin": "",
+	"Admin": "管理者",
 	"Admin Panel": "管理者パネル",
 	"Admin Settings": "管理者設定",
-	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "",
+	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "管理者は全てのツールにアクセス出来ます。ユーザーはワークスペースのモデル毎に割り当てて下さい。",
 	"Advanced Parameters": "詳細パラメーター",
 	"Advanced Params": "高度なパラメータ",
 	"all": "すべて",
@@ -48,11 +48,11 @@
 	"All Users": "すべてのユーザー",
 	"Allow": "許可",
 	"Allow Chat Deletion": "チャットの削除を許可",
-	"Allow Chat Editing": "",
-	"Allow non-local voices": "",
-	"Allow Temporary Chat": "",
-	"Allow User Location": "",
-	"Allow Voice Interruption in Call": "",
+	"Allow Chat Editing": "チャットの編集を許可",
+	"Allow non-local voices": "ローカル以外のボイスを許可",
+	"Allow Temporary Chat": "チャットの一時許可",
+	"Allow User Location": "ユーザーロケーションの許可",
+	"Allow Voice Interruption in Call": "音声の割り込みを許可",
 	"alphanumeric characters and hyphens": "英数字とハイフン",
 	"Already have an account?": "すでにアカウントをお持ちですか?",
 	"an assistant": "アシスタント",
@@ -69,41 +69,41 @@
 	"are allowed - Activate this command by typing": "が許可されています - 次のように入力してこのコマンドをアクティブ化します",
 	"Are you sure?": "よろしいですか?",
 	"Artifacts": "",
-	"Ask a question": "",
+	"Ask a question": "質問して下さい。",
 	"Attach file": "ファイルを添付する",
 	"Attention to detail": "詳細に注意する",
 	"Audio": "オーディオ",
 	"August": "8月",
 	"Auto-playback response": "応答の自動再生",
 	"Automatic1111": "",
-	"AUTOMATIC1111 Api Auth String": "",
+	"AUTOMATIC1111 Api Auth String": "AUTOMATIC1111のAuthを入力",
 	"AUTOMATIC1111 Base URL": "AUTOMATIC1111 ベース URL",
 	"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 ベース URL が必要です。",
-	"Available list": "",
+	"Available list": "利用可能リスト",
 	"available!": "利用可能!",
-	"Azure AI Speech": "",
-	"Azure Region": "",
+	"Azure AI Speech": "AzureAIスピーチ",
+	"Azure Region": "Azureリージョン",
 	"Back": "戻る",
 	"Bad Response": "応答が悪い",
 	"Banners": "バナー",
 	"Base Model (From)": "ベースモデル(From)",
-	"Batch Size (num_batch)": "",
+	"Batch Size (num_batch)": "バッチサイズ",
 	"before": "より前",
 	"Being lazy": "怠惰な",
 	"Brave Search API Key": "Brave Search APIキー",
 	"Bypass SSL verification for Websites": "SSL 検証をバイパスする",
-	"Call": "",
+	"Call": "コール",
 	"Call feature is not supported when using Web STT engine": "",
-	"Camera": "",
+	"Camera": "カメラ",
 	"Cancel": "キャンセル",
 	"Capabilities": "資格",
 	"Change Password": "パスワードを変更",
 	"Chat": "チャット",
-	"Chat Background Image": "",
+	"Chat Background Image": "チャットバックグラウンドイメージ",
 	"Chat Bubble UI": "チャットバブルUI",
-	"Chat Controls": "",
+	"Chat Controls": "チャットコントロール",
 	"Chat direction": "チャットの方向",
-	"Chat Overview": "",
+	"Chat Overview": "チャット概要",
 	"Chats": "チャット",
 	"Check Again": "再確認",
 	"Check for updates": "アップデートを確認",
@@ -113,44 +113,44 @@
 	"Chunk Params": "チャンクパラメーター",
 	"Chunk Size": "チャンクサイズ",
 	"Citation": "引用文",
-	"Clear memory": "",
+	"Clear memory": "メモリクリアー",
 	"Click here for help.": "ヘルプについてはここをクリックしてください。",
 	"Click here to": "ここをクリックして",
-	"Click here to download user import template file.": "",
+	"Click here to download user import template file.": "ユーザーテンプレートをインポートするにはここをクリック",
 	"Click here to select": "選択するにはここをクリックしてください",
 	"Click here to select a csv file.": "CSVファイルを選択するにはここをクリックしてください。",
-	"Click here to select a py file.": "",
+	"Click here to select a py file.": "Pythonスクリプトファイルを選択するにはここをクリック",
 	"Click here to select documents.": "ドキュメントを選択するにはここをクリックしてください。",
-	"Click here to upload a workflow.json file.": "",
+	"Click here to upload a workflow.json file.": "workflow.jsonファイルをアップロードするにはここをクリック。",
 	"click here.": "ここをクリックしてください。",
 	"Click on the user role button to change a user's role.": "ユーザーの役割を変更するには、ユーザー役割ボタンをクリックしてください。",
-	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "",
+	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "クリップボードへの書き込み許可がありません。ブラウザ設定を確認し許可してください。",
 	"Clone": "クローン",
 	"Close": "閉じる",
-	"Code formatted successfully": "",
+	"Code formatted successfully": "コードフォーマット完了",
 	"Collection": "コレクション",
 	"ComfyUI": "ComfyUI",
 	"ComfyUI Base URL": "ComfyUIベースURL",
 	"ComfyUI Base URL is required.": "ComfyUIベースURLが必要です。",
-	"ComfyUI Workflow": "",
-	"ComfyUI Workflow Nodes": "",
+	"ComfyUI Workflow": "ComfyUIワークフロー",
+	"ComfyUI Workflow Nodes": "ComfyUIワークフローノード",
 	"Command": "コマンド",
 	"Concurrent Requests": "コンカレント要求",
-	"Confirm": "",
+	"Confirm": "確認",
 	"Confirm Password": "パスワードを確認",
 	"Confirm your action": "",
 	"Connections": "接続",
-	"Contact Admin for WebUI Access": "",
+	"Contact Admin for WebUI Access": "WEBUIへの接続について管理者に問い合わせ下さい。",
 	"Content": "コンテンツ",
 	"Content Extraction": "",
 	"Context Length": "コンテキストの長さ",
 	"Continue Response": "続きの応答",
 	"Continue with {{provider}}": "",
 	"Control how message text is split for TTS requests. 'Punctuation' splits into sentences, 'paragraphs' splits into paragraphs, and 'none' keeps the message as a single string.": "",
-	"Controls": "",
-	"Copied": "",
-	"Copied shared chat URL to clipboard!": "共有チャットURLをクリップボードにコピーしました",
-	"Copied to clipboard": "",
+	"Controls": "コントロール",
+	"Copied": "コピー",
+	"Copied shared chat URL to clipboard!": "共有チャットURLをクリップボードにコピーしました!",
+	"Copied to clipboard": "クリップボードにコピーしました。",
 	"Copy": "コピー",
 	"Copy last code block": "最後のコードブロックをコピー",
 	"Copy last response": "最後の応答をコピー",
@@ -158,23 +158,23 @@
 	"Copying to clipboard was successful!": "クリップボードへのコピーが成功しました!",
 	"Create a model": "モデルを作成する",
 	"Create Account": "アカウントを作成",
-	"Create Knowledge": "",
+	"Create Knowledge": "RAG用データ作成",
 	"Create new key": "新しいキーを作成",
 	"Create new secret key": "新しいシークレットキーを作成",
 	"Created at": "作成日時",
 	"Created At": "作成日時",
 	"Created by": "",
-	"CSV Import": "",
+	"CSV Import": "CSVインポート",
 	"Current Model": "現在のモデル",
 	"Current Password": "現在のパスワード",
 	"Custom": "カスタム",
 	"Customize models for a specific purpose": "特定の目的に合わせてモデルをカスタマイズする",
 	"Dark": "ダーク",
-	"Dashboard": "",
+	"Dashboard": "ダッシュボード",
 	"Database": "データベース",
 	"December": "12月",
 	"Default": "デフォルト",
-	"Default (Open AI)": "",
+	"Default (Open AI)": "デフォルト(OpenAI)",
 	"Default (SentenceTransformers)": "デフォルト (SentenceTransformers)",
 	"Default Model": "デフォルトモデル",
 	"Default model updated": "デフォルトモデルが更新されました",
@@ -186,41 +186,41 @@
 	"Delete All Chats": "すべてのチャットを削除",
 	"Delete chat": "チャットを削除",
 	"Delete Chat": "チャットを削除",
-	"Delete chat?": "",
-	"Delete Doc": "",
-	"Delete function?": "",
-	"Delete prompt?": "",
+	"Delete chat?": "チャットを削除しますか?",
+	"Delete Doc": "ドキュメントを削除しますか?",
+	"Delete function?": "Functionを削除しますか?",
+	"Delete prompt?": "プロンプトを削除しますか?",
 	"delete this link": "このリンクを削除します",
-	"Delete tool?": "",
+	"Delete tool?": "ツールを削除しますか?",
 	"Delete User": "ユーザーを削除",
 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} を削除しました",
 	"Deleted {{name}}": "{{name}}を削除しました",
 	"Description": "説明",
 	"Didn't fully follow instructions": "説明に沿って操作していませんでした",
-	"Disabled": "",
+	"Disabled": "無効",
 	"Discover a function": "",
 	"Discover a model": "モデルを検出する",
-	"Discover a prompt": "プロンプトを見つける",
-	"Discover a tool": "",
-	"Discover, download, and explore custom functions": "",
+	"Discover a prompt": "プロンプトを探す",
+	"Discover a tool": "ツールを探す",
+	"Discover, download, and explore custom functions": "カスタムFunctionを探しダウンロードする",
 	"Discover, download, and explore custom prompts": "カスタムプロンプトを見つけて、ダウンロードして、探索",
-	"Discover, download, and explore custom tools": "",
+	"Discover, download, and explore custom tools": "カスタムツールを探しダウンロードする",
 	"Discover, download, and explore model presets": "モデルプリセットを見つけて、ダウンロードして、探索",
 	"Dismissible": "",
 	"Display Emoji in Call": "",
 	"Display the username instead of You in the Chat": "チャットで「あなた」の代わりにユーザー名を表示",
-	"Do not install functions from sources you do not fully trust.": "",
-	"Do not install tools from sources you do not fully trust.": "",
+	"Do not install functions from sources you do not fully trust.": "信楽出来ないソースからFunctionをインストールしないでください。",
+	"Do not install tools from sources you do not fully trust.": "信頼出来ないソースからツールをインストールしないでください。",
 	"Document": "ドキュメント",
-	"Documentation": "",
+	"Documentation": "ドキュメント",
 	"Documents": "ドキュメント",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "外部接続を行わず、データはローカルでホストされているサーバー上に安全に保持されます。",
 	"Don't Allow": "許可しない",
 	"Don't have an account?": "アカウントをお持ちではありませんか?",
-	"don't install random functions from sources you don't trust.": "",
-	"don't install random tools from sources you don't trust.": "",
+	"don't install random functions from sources you don't trust.": "信頼出来ないソースからランダムFunctionをインストールしないでください。",
+	"don't install random tools from sources you don't trust.": "信頼出来ないソースからランダムツールをインストールしないでください。",
 	"Don't like the style": "デザインが好きでない",
-	"Done": "",
+	"Done": "完了",
 	"Download": "ダウンロードをキャンセルしました",
 	"Download canceled": "ダウンロードをキャンセルしました",
 	"Download Database": "データベースをダウンロード",
@@ -232,21 +232,21 @@
 	"Edit User": "ユーザーを編集",
 	"ElevenLabs": "",
 	"Email": "メールアドレス",
-	"Embedding Batch Size": "",
+	"Embedding Batch Size": "埋め込みモデルバッチサイズ",
 	"Embedding Model": "埋め込みモデル",
 	"Embedding Model Engine": "埋め込みモデルエンジン",
 	"Embedding model set to \"{{embedding_model}}\"": "埋め込みモデルを\"{{embedding_model}}\"に設定しました",
 	"Enable Community Sharing": "コミュニティ共有の有効化",
-	"Enable Message Rating": "",
+	"Enable Message Rating": "メッセージRatingの有効化",
 	"Enable New Sign Ups": "新規登録を有効化",
 	"Enable Web Search": "Web 検索を有効にする",
-	"Enable Web Search Query Generation": "",
-	"Enabled": "",
-	"Engine": "",
+	"Enable Web Search Query Generation": "WEBサーチQueryの有効化",
+	"Enabled": "有効",
+	"Engine": "エンジン",
 	"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "CSVファイルに4つの列が含まれていることを確認してください: Name, Email, Password, Role.",
 	"Enter {{role}} message here": "{{role}} メッセージをここに入力してください",
 	"Enter a detail about yourself for your LLMs to recall": "LLM が記憶するために、自分についての詳細を入力してください",
-	"Enter api auth string (e.g. username:password)": "",
+	"Enter api auth string (e.g. username:password)": "API AuthStringを入力(e.g Username:Password)",
 	"Enter Brave Search API Key": "Brave Search APIキーの入力",
 	"Enter CFG Scale (e.g. 7.0)": "",
 	"Enter Chunk Overlap": "チャンクオーバーラップを入力してください",
@@ -256,76 +256,76 @@
 	"Enter Google PSE Engine Id": "Google PSE エンジン ID を入力します。",
 	"Enter Image Size (e.g. 512x512)": "画像サイズを入力してください (例: 512x512)",
 	"Enter language codes": "言語コードを入力してください",
-	"Enter Model ID": "",
+	"Enter Model ID": "モデルIDを入力してください。",
 	"Enter model tag (e.g. {{modelTag}})": "モデルタグを入力してください (例: {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "ステップ数を入力してください (例: 50)",
-	"Enter Sampler (e.g. Euler a)": "",
-	"Enter Scheduler (e.g. Karras)": "",
+	"Enter Sampler (e.g. Euler a)": "サンプラーを入力してください(e.g. Euler a)。",
+	"Enter Scheduler (e.g. Karras)": "スケジューラーを入力してください。(e.g. Karras)",
 	"Enter Score": "スコアを入力してください",
-	"Enter SearchApi API Key": "",
-	"Enter SearchApi Engine": "",
+	"Enter SearchApi API Key": "SearchApi API Keyを入力してください。",
+	"Enter SearchApi Engine": "SearchApi Engineを入力してください。",
 	"Enter Searxng Query URL": "SearxngクエリURLを入力",
 	"Enter Serper API Key": "Serper APIキーの入力",
-	"Enter Serply API Key": "",
+	"Enter Serply API Key": "Serply API Keyを入力してください。",
 	"Enter Serpstack API Key": "Serpstack APIキーの入力",
 	"Enter stop sequence": "ストップシーケンスを入力してください",
-	"Enter system prompt": "",
-	"Enter Tavily API Key": "",
-	"Enter Tika Server URL": "",
+	"Enter system prompt": "システムプロンプト入力",
+	"Enter Tavily API Key": "Tavily API Keyを入力してください。",
+	"Enter Tika Server URL": "Tika Server URLを入力してください。",
 	"Enter Top K": "トップ K を入力してください",
 	"Enter URL (e.g. http://127.0.0.1:7860/)": "URL を入力してください (例: http://127.0.0.1:7860/)",
 	"Enter URL (e.g. http://localhost:11434)": "URL を入力してください (例: http://localhost:11434)",
 	"Enter Your Email": "メールアドレスを入力してください",
 	"Enter Your Full Name": "フルネームを入力してください",
-	"Enter your message": "",
+	"Enter your message": "メッセージを入力してください。",
 	"Enter Your Password": "パスワードを入力してください",
 	"Enter Your Role": "ロールを入力してください",
 	"Error": "エラー",
-	"Experimental": "実験",
+	"Experimental": "実験",
 	"Export": "輸出",
 	"Export All Chats (All Users)": "すべてのチャットをエクスポート (すべてのユーザー)",
-	"Export chat (.json)": "",
+	"Export chat (.json)": "チャットをエクスポート(.json)",
 	"Export Chats": "チャットをエクスポート",
-	"Export Config to JSON File": "",
+	"Export Config to JSON File": "JSONファイルのエクスポートConfig",
 	"Export Documents Mapping": "ドキュメントマッピングをエクスポート",
-	"Export Functions": "",
+	"Export Functions": "Functionのエクスポート",
 	"Export LiteLLM config.yaml": "",
 	"Export Models": "モデルのエクスポート",
 	"Export Prompts": "プロンプトをエクスポート",
-	"Export Tools": "",
-	"External Models": "",
+	"Export Tools": "ツールのエクスポート",
+	"External Models": "外部モデル",
 	"Failed to create API Key.": "APIキーの作成に失敗しました。",
 	"Failed to read clipboard contents": "クリップボードの内容を読み取れませんでした",
-	"Failed to update settings": "",
-	"Failed to upload file.": "",
+	"Failed to update settings": "設定アップデート失敗",
+	"Failed to upload file.": "ファイルアップロード失敗",
 	"February": "2月",
 	"Feel free to add specific details": "詳細を追加してください",
-	"File": "",
-	"File added successfully.": "",
-	"File content updated successfully.": "",
+	"File": "ファイル",
+	"File added successfully.": "ファイル追加が成功しました。",
+	"File content updated successfully.": "ファイルコンテンツ追加が成功しました。",
 	"File Mode": "ファイルモード",
 	"File not found.": "ファイルが見つかりません。",
-	"File removed successfully.": "",
-	"File size should not exceed {{maxSize}} MB.": "",
-	"Files": "",
-	"Filter is now globally disabled": "",
-	"Filter is now globally enabled": "",
-	"Filters": "",
+	"File removed successfully.": "ファイル削除が成功しました。",
+	"File size should not exceed {{maxSize}} MB.": "ファイルサイズ最大値{{maxSize}} MB",
+	"Files": "ファイル",
+	"Filter is now globally disabled": "グローバルフィルタが無効です。",
+	"Filter is now globally enabled": "グローバルフィルタが有効です。",
+	"Filters": "フィルター",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "指紋のなりすましが検出されました: イニシャルをアバターとして使用できません。デフォルトのプロファイル画像にデフォルト設定されています。",
 	"Fluidly stream large external response chunks": "大規模な外部応答チャンクを流動的にストリーミングする",
 	"Focus chat input": "チャット入力をフォーカス",
 	"Followed instructions perfectly": "完全に指示に従った",
-	"Form": "",
+	"Form": "フォーム",
 	"Format your variables using square brackets like this:": "次のように角括弧を使用して変数をフォーマットします。",
-	"Frequency Penalty": "周波数ペナルティ",
-	"Function created successfully": "",
-	"Function deleted successfully": "",
-	"Function Description (e.g. A filter to remove profanity from text)": "",
+	"Frequency Penalty": "繰り返しペナルティ",
+	"Function created successfully": "Functonの作成が成功しました。",
+	"Function deleted successfully": "Functionの削除が成功しました。",
+	"Function Description (e.g. A filter to remove profanity from text)": "Function詳細",
 	"Function ID (e.g. my_filter)": "",
-	"Function is now globally disabled": "",
-	"Function is now globally enabled": "",
+	"Function is now globally disabled": "Functionはグローバルで無効です。",
+	"Function is now globally enabled": "Functionはグローバルで有効です。",
 	"Function Name (e.g. My Filter)": "",
-	"Function updated successfully": "",
+	"Function updated successfully": "Functionのアップデートが成功しました。",
 	"Functions": "",
 	"Functions allow arbitrary code execution": "",
 	"Functions allow arbitrary code execution.": "",
@@ -336,7 +336,7 @@
 	"Generating search query": "検索クエリの生成",
 	"Generation Info": "生成情報",
 	"Get up and running with": "",
-	"Global": "",
+	"Global": "グローバル",
 	"Good Response": "良い応答",
 	"Google PSE API Key": "Google PSE APIキー",
 	"Google PSE Engine Id": "Google PSE エンジン ID",
@@ -357,10 +357,10 @@
 	"Import Chats": "チャットをインポート",
 	"Import Config from JSON File": "",
 	"Import Documents Mapping": "ドキュメントマッピングをインポート",
-	"Import Functions": "",
+	"Import Functions": "Functionのインポート",
 	"Import Models": "モデルのインポート",
 	"Import Prompts": "プロンプトをインポート",
-	"Import Tools": "",
+	"Import Tools": "ツールのインポート",
 	"Include `--api-auth` flag when running stable-diffusion-webui": "",
 	"Include `--api` flag when running stable-diffusion-webui": "stable-diffusion-webuiを実行する際に`--api`フラグを含める",
 	"Info": "情報",
@@ -379,12 +379,12 @@
 	"JWT Token": "JWT トークン",
 	"Keep Alive": "キープアライブ",
 	"Keyboard shortcuts": "キーボードショートカット",
-	"Knowledge": "",
-	"Knowledge created successfully.": "",
-	"Knowledge deleted successfully.": "",
-	"Knowledge reset successfully.": "",
-	"Knowledge updated successfully": "",
-	"Landing Page Mode": "",
+	"Knowledge": "RAGファイル",
+	"Knowledge created successfully.": "RAGファイル識別タグ作成完了",
+	"Knowledge deleted successfully.": "RAGファイル識別タグ削除完了",
+	"Knowledge reset successfully.": "RAGファイルリセット",
+	"Knowledge updated successfully": "RAGファイルアップデート完了",
+	"Landing Page Mode": "ランディングページモード",
 	"Language": "言語",
 	"large language models, locally.": "",
 	"Last Active": "最終アクティブ",
@@ -411,10 +411,10 @@
 	"May": "5月",
 	"Memories accessible by LLMs will be shown here.": "LLM がアクセスできるメモリはここに表示されます。",
 	"Memory": "メモリ",
-	"Memory added successfully": "",
-	"Memory cleared successfully": "",
-	"Memory deleted successfully": "",
-	"Memory updated successfully": "",
+	"Memory added successfully": "メモリに追加されました。",
+	"Memory cleared successfully": "メモリをクリアしました。",
+	"Memory deleted successfully": "メモリを削除しました。",
+	"Memory updated successfully": "メモリアップデート成功",
 	"Merge Responses": "",
 	"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "リンクを作成した後、送信したメッセージは共有されません。URL を持つユーザーは共有チャットを閲覧できます。",
 	"Min P": "",
@@ -567,17 +567,17 @@
 	"Search": "検索",
 	"Search a model": "モデルを検索",
 	"Search Chats": "チャットの検索",
-	"Search Collection": "",
+	"Search Collection": "Collectionの検索",
 	"Search Documents": "ドキュメントを検索",
-	"Search Functions": "",
-	"Search Knowledge": "",
+	"Search Functions": "Functionの検索",
+	"Search Knowledge": "RAGファイルの検索",
 	"Search Models": "モデル検索",
 	"Search Prompts": "プロンプトを検索",
-	"Search Query Generation Prompt": "",
+	"Search Query Generation Prompt": "Query生成プロンプトの検索",
 	"Search Result Count": "検索結果数",
-	"Search Tools": "",
-	"SearchApi API Key": "",
-	"SearchApi Engine": "",
+	"Search Tools": "ツールの検索",
+	"SearchApi API Key": "SearchApiのAPIKey",
+	"SearchApi Engine": "SearchApiエンジン",
 	"Searched {{count}} sites_other": "{{count}} sites_other検索",
 	"Searching \"{{searchQuery}}\"": "",
 	"Searching Knowledge for \"{{searchQuery}}\"": "",
@@ -586,15 +586,15 @@
 	"See what's new": "新機能を見る",
 	"Seed": "シード",
 	"Select a base model": "基本モデルの選択",
-	"Select a engine": "",
-	"Select a function": "",
+	"Select a engine": "エンジンの選択",
+	"Select a function": "Functionの選択",
 	"Select a model": "モデルを選択",
 	"Select a pipeline": "パイプラインの選択",
 	"Select a pipeline url": "パイプラインの URL を選択する",
-	"Select a tool": "",
+	"Select a tool": "ツールの選択",
 	"Select an Ollama instance": "Ollama インスタンスを選択",
-	"Select Engine": "",
-	"Select Knowledge": "",
+	"Select Engine": "エンジンの選択",
+	"Select Knowledge": "RAGデータの選択",
 	"Select model": "モデルを選択",
 	"Select only one model to call": "",
 	"Select/Add Files": "",
@@ -659,7 +659,7 @@
 	"Tap to interrupt": "",
 	"Tavily API Key": "",
 	"Tell us more:": "もっと話してください:",
-	"Temperature": "温度",
+	"Temperature": "生成時予測幅(Tenperature)",
 	"Template": "テンプレート",
 	"Temporary Chat": "",
 	"Text Completion": "テキスト補完",
@@ -671,10 +671,10 @@
 	"The maximum number of files that can be used at once in chat. If the number of files exceeds this limit, the files will not be uploaded.": "",
 	"The score should be a value between 0.0 (0%) and 1.0 (100%).": "スコアは0.0(0%)から1.0(100%)の間の値にしてください。",
 	"Theme": "テーマ",
-	"Thinking...": "",
+	"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 is an experimental feature, it may not function as expected and is subject to change at any time.": "開発中の機能であり正常動作しない場合があります。",
 	"This option will delete all existing files in the collection and replace them with newly uploaded files.": "",
 	"This will delete": "",
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
@@ -713,9 +713,9 @@
 	"Top K": "トップ K",
 	"Top P": "トップ P",
 	"Trouble accessing Ollama?": "Ollama へのアクセスに問題がありますか?",
-	"TTS Model": "",
+	"TTS Model": "TTSモデル",
 	"TTS Settings": "TTS 設定",
-	"TTS Voice": "",
+	"TTS Voice": "TTSボイス",
 	"Type": "種類",
 	"Type Hugging Face Resolve (Download) URL": "Hugging Face Resolve (ダウンロード) URL を入力してください",
 	"Uh-oh! There was an issue connecting to {{provider}}.": "おっと! {{provider}} への接続に問題が発生しました。",
@@ -728,13 +728,13 @@
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "GGUF モデルをアップロード",
-	"Upload directory": "",
-	"Upload files": "",
+	"Upload directory": "アップロードディレクトリ",
+	"Upload files": "アップロードファイル",
 	"Upload Files": "ファイルのアップロード",
-	"Upload Pipeline": "",
+	"Upload Pipeline": "アップロードパイプライン",
 	"Upload Progress": "アップロードの進行状況",
 	"URL Mode": "URL モード",
-	"Use '#' in the prompt input to load and include your knowledge.": "",
+	"Use '#' in the prompt input to load and include your knowledge.": "#を入力するとRAGデータを参照することが出来ます。",
 	"Use '#' in the prompt input to load and select your documents.": "プロンプト入力で '#' を使用して、ドキュメントを読み込んで選択します。",
 	"Use Gravatar": "Gravatar を使用する",
 	"Use Initials": "初期値を使用する",
@@ -753,9 +753,9 @@
 	"variable to have them replaced with clipboard content.": "クリップボードの内容に置き換える変数。",
 	"Version": "バージョン",
 	"Version {{selectedVersion}} of {{totalVersions}}": "",
-	"Voice": "",
+	"Voice": "ボイス",
 	"Warning": "警告",
-	"Warning:": "",
+	"Warning:": "警告",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "警告: 埋め込みモデルを更新または変更した場合は、すべてのドキュメントを再インポートする必要があります。",
 	"Web": "ウェブ",
 	"Web API": "",
@@ -775,13 +775,13 @@
 	"You": "あなた",
 	"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "",
 	"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "",
-	"You cannot clone a base model": "基本モデルのクローン作成できない",
+	"You cannot clone a base model": "基本モデルのクローン作成できない",
 	"You have no archived conversations.": "これまでにアーカイブされた会話はありません。",
 	"You have shared this chat": "このチャットを共有しました",
-	"You're a helpful assistant.": "あなたは役に立つアシスタントです。",
+	"You're a helpful assistant.": "あなたは有能なアシスタントです。",
 	"You're now logged in.": "ログインしました。",
-	"Your account status is currently pending activation.": "",
+	"Your account status is currently pending activation.": "貴方のアカウント状態は現在登録認証待ちです。",
 	"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "",
 	"Youtube": "YouTube",
-	"Youtube Loader Settings": "Youtubeローダー設定"
+	"Youtube Loader Settings": "Youtubeローダー設定(日本語はja)"
 }

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "მოთხოვნების ექსპორტი",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "API ღილაკის შექმნა ვერ მოხერხდა.",
 	"Failed to read clipboard contents": "ბუფერში შიგთავსის წაკითხვა ვერ მოხერხდა",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "დაასახელეთ თქვენი მოდელი",
 	"New Chat": "ახალი მიმოწერა",
 	"New Password": "ახალი პაროლი",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "სიდი",
 	"Select a base model": "აირჩიეთ ბაზის მოდელი",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "მოდელის არჩევა",
 	"Select a pipeline": "აირჩიეთ მილსადენი",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "მოდელის არჩევა",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "შერჩეული მოდელი (ებ) ი არ უჭერს მხარს გამოსახულების შეყვანას",
 	"Send": "გაგზავნა",
 	"Send a Message": "შეტყობინების გაგზავნა",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "განახლება და ბმულის კოპირება",
 	"Update for the latest features and improvements.": "",
 	"Update password": "პაროლის განახლება",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "GGUF მოდელის ატვირთვა",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "프롬프트 내보내기",
 	"Export Tools": "도구 내보내기",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "API 키 생성에 실패했습니다.",
 	"Failed to read clipboard contents": "클립보드 내용을 읽는 데 실패하였습니다.",
 	"Failed to update settings": "설정 업데이트에 실패하였습니다.",
@@ -449,6 +450,7 @@
 	"Name your model": "모델 이름 지정",
 	"New Chat": "새 채팅",
 	"New Password": "새 비밀번호",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "시드",
 	"Select a base model": "기본 모델 선택",
 	"Select a engine": "엔진 선택",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "모델 선택",
 	"Select a pipeline": "파이프라인 선택",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "모델 선택",
 	"Select only one model to call": "콜을 위해서는 모델을 하나만 선택해야 합니다.",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "선택한 모델은 이미지 입력을 지원하지 않습니다.",
 	"Send": "보내기",
 	"Send a Message": "메시지 보내기",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "링크 업데이트 및 복사",
 	"Update for the latest features and improvements.": "",
 	"Update password": "비밀번호 업데이트",
+	"Updated": "",
 	"Updated at": "다음에 업데이트됨",
 	"Upload": "업로드",
 	"Upload a GGUF model": "GGUF 모델 업로드",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Eksportuoti užklausas",
 	"Export Tools": "Eksportuoti įrankius",
 	"External Models": "Išoriniai modeliai",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Nepavyko sukurti API rakto",
 	"Failed to read clipboard contents": "Nepavyko perskaityti kopijuoklės",
 	"Failed to update settings": "Nepavyko atnaujinti nustatymų",
@@ -449,6 +450,7 @@
 	"Name your model": "Pavadinkite savo modelį",
 	"New Chat": "Naujas pokalbis",
 	"New Password": "Naujas slaptažodis",
+	"No content found": "",
 	"No content to speak": "Nėra turinio kalbėjimui",
 	"No file selected": "Nėra pasirinktų dokumentų",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -590,6 +592,7 @@
 	"Seed": "Sėkla",
 	"Select a base model": "Pasirinkite bazinį modelį",
 	"Select a engine": "Pasirinkite variklį",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Pasirinkite funkciją",
 	"Select a model": "Pasirinkti modelį",
 	"Select a pipeline": "Pasirinkite procesą",
@@ -600,7 +603,6 @@
 	"Select Knowledge": "",
 	"Select model": "Pasirinkti modelį",
 	"Select only one model to call": "Pasirinkite vieną modelį",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Pasirinkti modeliai nepalaiko vaizdinių užklausų",
 	"Send": "Siųsti",
 	"Send a Message": "Siųsti žinutę",
@@ -728,6 +730,7 @@
 	"Update and Copy Link": "Atnaujinti ir kopijuoti nuorodą",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Atnaujinti slaptažodį",
+	"Updated": "",
 	"Updated at": "Atnaujinta",
 	"Upload": "Atnaujinti",
 	"Upload a GGUF model": "Parsisiųsti GGUF modelį",

+ 4 - 1
src/lib/i18n/locales/ms-MY/translation.json

@@ -294,6 +294,7 @@
 	"Export Prompts": "Eksport Gesaan",
 	"Export Tools": "Eksport Alat",
 	"External Models": "Model Luaran",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Gagal mencipta kekunci API",
 	"Failed to read clipboard contents": "Gagal membaca konten papan klip",
 	"Failed to update settings": "Gagal mengemaskini tetapan",
@@ -449,6 +450,7 @@
 	"Name your model": "Namakan Model Anda",
 	"New Chat": "Perbualan Baru",
 	"New Password": "Kata Laluan Baru",
+	"No content found": "",
 	"No content to speak": "Tiada kandungan untuk bercakap",
 	"No file selected": "Tiada fail dipilih",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "Benih",
 	"Select a base model": "Pilih model asas",
 	"Select a engine": "Pilih enjin",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Pilih fungsi",
 	"Select a model": "Pilih model",
 	"Select a pipeline": "Pilih 'pipeline'",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "Pilih model",
 	"Select only one model to call": "Pilih hanya satu model untuk dipanggil",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Model dipilih tidak menyokong input imej",
 	"Send": "Hantar",
 	"Send a Message": "Hantar Pesanan",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "Kemaskini dan salin pautan",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Kemaskini Kata Laluan",
+	"Updated": "",
 	"Updated at": "Dikemaskini pada",
 	"Upload": "Muatnaik",
 	"Upload a GGUF model": "Muatnaik model GGUF",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Eksporter prompts",
 	"Export Tools": "Eksporter verktøy",
 	"External Models": "Eksterne modeller",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Kunne ikke opprette API-nøkkel.",
 	"Failed to read clipboard contents": "Kunne ikke lese utklippstavleinnhold",
 	"Failed to update settings": "Kunne ikke oppdatere innstillinger",
@@ -449,6 +450,7 @@
 	"Name your model": "Gi modellen din et navn",
 	"New Chat": "Ny chat",
 	"New Password": "Nytt passord",
+	"No content found": "",
 	"No content to speak": "Mangler innhold for tale",
 	"No file selected": "Ingen fil valgt",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "Seed",
 	"Select a base model": "Velg en grunnmodell",
 	"Select a engine": "Velg en motor",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Velg en funksjon",
 	"Select a model": "Velg en modell",
 	"Select a pipeline": "Velg en pipeline",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "Velg modell",
 	"Select only one model to call": "Velg kun én modell å kalle",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Valgte modell(er) støtter ikke bildeforslag",
 	"Send": "Send",
 	"Send a Message": "Send en melding",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "Oppdater og kopier lenke",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Oppdater passord",
+	"Updated": "",
 	"Updated at": "Oppdatert",
 	"Upload": "Last opp",
 	"Upload a GGUF model": "Last opp en GGUF-modell",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Exporteer Prompts",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Kan API Key niet aanmaken.",
 	"Failed to read clipboard contents": "Kan klembord inhoud niet lezen",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "Geef uw model een naam",
 	"New Chat": "Nieuwe Chat",
 	"New Password": "Nieuw Wachtwoord",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "Seed",
 	"Select a base model": "Selecteer een basismodel",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "Selecteer een model",
 	"Select a pipeline": "Selecteer een pijplijn",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "Selecteer een model",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Geselecteerde modellen ondersteunen geen beeldinvoer",
 	"Send": "Verzenden",
 	"Send a Message": "Stuur een Bericht",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "Update en Kopieer Link",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Wijzig wachtwoord",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "Upload een GGUF model",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "ਪ੍ਰੰਪਟ ਨਿਰਯਾਤ ਕਰੋ",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "API ਕੁੰਜੀ ਬਣਾਉਣ ਵਿੱਚ ਅਸਫਲ।",
 	"Failed to read clipboard contents": "ਕਲਿੱਪਬੋਰਡ ਸਮੱਗਰੀ ਪੜ੍ਹਣ ਵਿੱਚ ਅਸਫਲ",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "ਆਪਣੇ ਮਾਡਲ ਦਾ ਨਾਮ ਦੱਸੋ",
 	"New Chat": "ਨਵੀਂ ਗੱਲਬਾਤ",
 	"New Password": "ਨਵਾਂ ਪਾਸਵਰਡ",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "ਬੀਜ",
 	"Select a base model": "ਆਧਾਰ ਮਾਡਲ ਚੁਣੋ",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "ਇੱਕ ਮਾਡਲ ਚੁਣੋ",
 	"Select a pipeline": "ਪਾਈਪਲਾਈਨ ਚੁਣੋ",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "ਮਾਡਲ ਚੁਣੋ",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "ਚੁਣੇ ਗਏ ਮਾਡਲ(ਆਂ) ਚਿੱਤਰ ਇਨਪੁੱਟਾਂ ਦਾ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੇ",
 	"Send": "ਭੇਜੋ",
 	"Send a Message": "ਇੱਕ ਸੁਨੇਹਾ ਭੇਜੋ",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "ਅੱਪਡੇਟ ਕਰੋ ਅਤੇ ਲਿੰਕ ਕਾਪੀ ਕਰੋ",
 	"Update for the latest features and improvements.": "",
 	"Update password": "ਪਾਸਵਰਡ ਅੱਪਡੇਟ ਕਰੋ",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "ਇੱਕ GGUF ਮਾਡਲ ਅਪਲੋਡ ਕਰੋ",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Eksportuj prompty",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Nie udało się utworzyć klucza API.",
 	"Failed to read clipboard contents": "Nie udało się odczytać zawartości schowka",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "Nazwij swój model",
 	"New Chat": "Nowy czat",
 	"New Password": "Nowe hasło",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -590,6 +592,7 @@
 	"Seed": "Seed",
 	"Select a base model": "Wybieranie modelu bazowego",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "Wybierz model",
 	"Select a pipeline": "Wybieranie potoku",
@@ -600,7 +603,6 @@
 	"Select Knowledge": "",
 	"Select model": "Wybierz model",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Wybrane modele nie obsługują danych wejściowych obrazu",
 	"Send": "Wyślij",
 	"Send a Message": "Wyślij Wiadomość",
@@ -728,6 +730,7 @@
 	"Update and Copy Link": "Uaktualnij i skopiuj link",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Aktualizacja hasła",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "Prześlij model GGUF",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Exportar Prompts",
 	"Export Tools": "Exportar Ferramentas",
 	"External Models": "Modelos Externos",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Falha ao criar a Chave API.",
 	"Failed to read clipboard contents": "Falha ao ler o conteúdo da área de transferência",
 	"Failed to update settings": "Falha ao atualizar as configurações",
@@ -449,6 +450,7 @@
 	"Name your model": "Nomeie seu modelo",
 	"New Chat": "Novo Chat",
 	"New Password": "Nova Senha",
+	"No content found": "",
 	"No content to speak": "Sem conteúdo para falar",
 	"No file selected": "Nenhum arquivo selecionado",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -589,6 +591,7 @@
 	"Seed": "Seed",
 	"Select a base model": "Selecione um modelo base",
 	"Select a engine": "Selecione um motor",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Selecione uma função",
 	"Select a model": "Selecione um modelo",
 	"Select a pipeline": "Selecione um pipeline",
@@ -599,7 +602,6 @@
 	"Select Knowledge": "",
 	"Select model": "Selecionar modelo",
 	"Select only one model to call": "Selecione apenas um modelo para chamar",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Modelo(s) selecionado(s) não suportam entradas de imagem",
 	"Send": "Enviar",
 	"Send a Message": "Enviar uma Mensagem",
@@ -727,6 +729,7 @@
 	"Update and Copy Link": "Atualizar e Copiar Link",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Atualizar senha",
+	"Updated": "",
 	"Updated at": "Atualizado em",
 	"Upload": "Fazer upload",
 	"Upload a GGUF model": "Fazer upload de um modelo GGUF",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Exportar Prompts",
 	"Export Tools": "",
 	"External Models": "Modelos Externos",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Falha ao criar a Chave da API.",
 	"Failed to read clipboard contents": "Falha ao ler o conteúdo da área de transferência",
 	"Failed to update settings": "Falha ao atualizar as definições",
@@ -449,6 +450,7 @@
 	"Name your model": "Atribua um nome ao seu modelo",
 	"New Chat": "Nova Conversa",
 	"New Password": "Nova Senha",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -589,6 +591,7 @@
 	"Seed": "Semente",
 	"Select a base model": "Selecione um modelo base",
 	"Select a engine": "Selecione um motor",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "Selecione um modelo",
 	"Select a pipeline": "Selecione um pipeline",
@@ -599,7 +602,6 @@
 	"Select Knowledge": "",
 	"Select model": "Selecione o modelo",
 	"Select only one model to call": "Selecione apenas um modelo para a chamada",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "O(s) modelo(s) selecionado(s) não suporta(m) entradas de imagem",
 	"Send": "Enviar",
 	"Send a Message": "Enviar uma Mensagem",
@@ -727,6 +729,7 @@
 	"Update and Copy Link": "Atualizar e Copiar Link",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Atualizar senha",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "Carregar um modelo GGUF",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Exportă Prompturile",
 	"Export Tools": "Exportă Instrumentele",
 	"External Models": "Modele Externe",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Crearea cheii API a eșuat.",
 	"Failed to read clipboard contents": "Citirea conținutului clipboard-ului a eșuat",
 	"Failed to update settings": "Actualizarea setărilor a eșuat",
@@ -449,6 +450,7 @@
 	"Name your model": "Denumirea modelului",
 	"New Chat": "Conversație Nouă",
 	"New Password": "Parolă Nouă",
+	"No content found": "",
 	"No content to speak": "Nu există conținut de vorbit",
 	"No file selected": "Nu a fost selectat niciun fișier",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -589,6 +591,7 @@
 	"Seed": "Sămânță",
 	"Select a base model": "Selectează un model de bază",
 	"Select a engine": "Selectează un motor",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Selectează o funcție",
 	"Select a model": "Selectează un model",
 	"Select a pipeline": "Selectează o conductă",
@@ -599,7 +602,6 @@
 	"Select Knowledge": "",
 	"Select model": "Selectează model",
 	"Select only one model to call": "Selectează doar un singur model pentru apel",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Modelul(e) selectat(e) nu suportă intrări de imagine",
 	"Send": "Trimite",
 	"Send a Message": "Trimite un Mesaj",
@@ -727,6 +729,7 @@
 	"Update and Copy Link": "Actualizează și Copiază Link-ul",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Actualizează parola",
+	"Updated": "",
 	"Updated at": "Actualizat la",
 	"Upload": "Încărcare",
 	"Upload a GGUF model": "Încarcă un model GGUF",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Экспортировать промпты",
 	"Export Tools": "Экспортировать инструменты",
 	"External Models": "Внешние модели",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Не удалось создать ключ API.",
 	"Failed to read clipboard contents": "Не удалось прочитать содержимое буфера обмена",
 	"Failed to update settings": "Не удалось обновить настройки",
@@ -449,6 +450,7 @@
 	"Name your model": "Присвойте модели имя",
 	"New Chat": "Новый чат",
 	"New Password": "Новый пароль",
+	"No content found": "",
 	"No content to speak": "Нечего говорить",
 	"No file selected": "Файлы не выбраны",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -590,6 +592,7 @@
 	"Seed": "Сид",
 	"Select a base model": "Выберите базовую модель",
 	"Select a engine": "Выберите движок",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Выберите функцию",
 	"Select a model": "Выберите модель",
 	"Select a pipeline": "Выберите конвейер",
@@ -600,7 +603,6 @@
 	"Select Knowledge": "",
 	"Select model": "Выберите модель",
 	"Select only one model to call": "Выберите только одну модель для вызова",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Выбранные модели не поддерживают ввод изображений",
 	"Send": "Отправить",
 	"Send a Message": "Отправить сообщение",
@@ -728,6 +730,7 @@
 	"Update and Copy Link": "Обновить и скопировать ссылку",
 	"Update for the latest features and improvements.": "Обновитесь для получения последних функций и улучшений.",
 	"Update password": "Обновить пароль",
+	"Updated": "",
 	"Updated at": "Обновлено",
 	"Upload": "Загрузить",
 	"Upload a GGUF model": "Загрузить модель GGUF",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Извези упите",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Неуспешно стварање API кључа.",
 	"Failed to read clipboard contents": "Неуспешно читање садржаја оставе",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "Наведи свој модел",
 	"New Chat": "Ново ћаскање",
 	"New Password": "Нова лозинка",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -589,6 +591,7 @@
 	"Seed": "Семе",
 	"Select a base model": "Избор основног модела",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "Изабери модел",
 	"Select a pipeline": "Избор цевовода",
@@ -599,7 +602,6 @@
 	"Select Knowledge": "",
 	"Select model": "Изабери модел",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Изабрани модели не подржавају уносе слика",
 	"Send": "Пошаљи",
 	"Send a Message": "Пошаљи поруку",
@@ -727,6 +729,7 @@
 	"Update and Copy Link": "Ажурирај и копирај везу",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Ажурирај лозинку",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "Отпреми GGUF модел",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Exportera instruktioner",
 	"Export Tools": "Exportera verktyg",
 	"External Models": "Externa modeller",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Misslyckades med att skapa API-nyckel.",
 	"Failed to read clipboard contents": "Misslyckades med att läsa urklippsinnehåll",
 	"Failed to update settings": "Misslyckades med att uppdatera inställningarna",
@@ -449,6 +450,7 @@
 	"Name your model": "Namnge din modell",
 	"New Chat": "Ny chatt",
 	"New Password": "Nytt lösenord",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "Seed",
 	"Select a base model": "Välj en basmodell",
 	"Select a engine": "Välj en motor",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "Välj en modell",
 	"Select a pipeline": "Välj en rörledning",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "Välj en modell",
 	"Select only one model to call": "Välj endast en modell att ringa",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Valda modeller stöder inte bildinmatningar",
 	"Send": "Skicka",
 	"Send a Message": "Skicka ett meddelande",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "Uppdatera och kopiera länk",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Uppdatera lösenord",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "Ladda upp en GGUF-modell",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "ส่งออกพรอมต์",
 	"Export Tools": "ส่งออกเครื่องมือ",
 	"External Models": "โมเดลภายนอก",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "สร้างคีย์ API ล้มเหลว",
 	"Failed to read clipboard contents": "อ่านเนื้อหาคลิปบอร์ดล้มเหลว",
 	"Failed to update settings": "อัปเดตการตั้งค่าล้มเหลว",
@@ -449,6 +450,7 @@
 	"Name your model": "ตั้งชื่อโมเดลของคุณ",
 	"New Chat": "แชทใหม่",
 	"New Password": "รหัสผ่านใหม่",
+	"No content found": "",
 	"No content to speak": "ไม่มีเนื้อหาที่จะพูด",
 	"No file selected": "ไม่ได้เลือกไฟล์",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "Seed",
 	"Select a base model": "เลือกโมเดลฐาน",
 	"Select a engine": "เลือกเอนจิน",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "เลือกฟังก์ชัน",
 	"Select a model": "เลือกโมเดล",
 	"Select a pipeline": "เลือกไปป์ไลน์",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "เลือกโมเดล",
 	"Select only one model to call": "เลือกเพียงโมเดลเดียวที่จะใช้",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "โมเดลที่เลือกไม่รองรับภาพ",
 	"Send": "ส่ง",
 	"Send a Message": "ส่งข้อความ",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "อัปเดตและคัดลอกลิงก์",
 	"Update for the latest features and improvements.": "",
 	"Update password": "อัปเดตรหัสผ่าน",
+	"Updated": "",
 	"Updated at": "อัปเดตเมื่อ",
 	"Upload": "อัปโหลด",
 	"Upload a GGUF model": "อัปโหลดโมเดล GGUF",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "",
 	"Export Tools": "",
 	"External Models": "",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "",
 	"Failed to read clipboard contents": "",
 	"Failed to update settings": "",
@@ -449,6 +450,7 @@
 	"Name your model": "",
 	"New Chat": "",
 	"New Password": "",
+	"No content found": "",
 	"No content to speak": "",
 	"No file selected": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "",
 	"Select a base model": "",
 	"Select a engine": "",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "",
 	"Select a model": "",
 	"Select a pipeline": "",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "",
 	"Select only one model to call": "",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "",
 	"Send": "",
 	"Send a Message": "",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "",
 	"Update for the latest features and improvements.": "",
 	"Update password": "",
+	"Updated": "",
 	"Updated at": "",
 	"Upload": "",
 	"Upload a GGUF model": "",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Promptları Dışa Aktar",
 	"Export Tools": "Araçları Dışa Aktar",
 	"External Models": "Modelleri Dışa Aktar",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "API Anahtarı oluşturulamadı.",
 	"Failed to read clipboard contents": "Pano içeriği okunamadı",
 	"Failed to update settings": "Ayarlar güncellenemedi",
@@ -449,6 +450,7 @@
 	"Name your model": "Modelinizi Adlandırın",
 	"New Chat": "Yeni Sohbet",
 	"New Password": "Yeni Parola",
+	"No content found": "",
 	"No content to speak": "Konuşacak içerik yok",
 	"No file selected": "Hiçbir dosya seçilmedi",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -588,6 +590,7 @@
 	"Seed": "Seed",
 	"Select a base model": "Bir temel model seç",
 	"Select a engine": "Bir motor seç",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Bir fonksiyon seç",
 	"Select a model": "Bir model seç",
 	"Select a pipeline": "Bir pipeline seç",
@@ -598,7 +601,6 @@
 	"Select Knowledge": "",
 	"Select model": "Model seç",
 	"Select only one model to call": "Arama için sadece bir model seç",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Seçilen model(ler) görüntü girişlerini desteklemiyor",
 	"Send": "Gönder",
 	"Send a Message": "Bir Mesaj Gönder",
@@ -726,6 +728,7 @@
 	"Update and Copy Link": "Güncelle ve Bağlantıyı Kopyala",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Parolayı Güncelle",
+	"Updated": "",
 	"Updated at": "Şu tarihte güncellendi:",
 	"Upload": "Yükle",
 	"Upload a GGUF model": "Bir GGUF modeli yükle",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Експортувати промти",
 	"Export Tools": "Експортувати інструменти",
 	"External Models": "Зовнішні моделі",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Не вдалося створити API ключ.",
 	"Failed to read clipboard contents": "Не вдалося прочитати вміст буфера обміну",
 	"Failed to update settings": "Не вдалося оновити налаштування",
@@ -449,6 +450,7 @@
 	"Name your model": "Назвіть свою модель",
 	"New Chat": "Новий чат",
 	"New Password": "Новий пароль",
+	"No content found": "",
 	"No content to speak": "Нема чого говорити",
 	"No file selected": "Файл не обрано",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -590,6 +592,7 @@
 	"Seed": "Сід",
 	"Select a base model": "Обрати базову модель",
 	"Select a engine": "Оберіть рушій",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Оберіть функцію",
 	"Select a model": "Оберіть модель",
 	"Select a pipeline": "Оберіть конвеєр",
@@ -600,7 +603,6 @@
 	"Select Knowledge": "",
 	"Select model": "Обрати модель",
 	"Select only one model to call": "Оберіть лише одну модель для виклику",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Вибрані модель(і) не підтримують вхідні зображення",
 	"Send": "Надіслати",
 	"Send a Message": "Надіслати повідомлення",
@@ -728,6 +730,7 @@
 	"Update and Copy Link": "Оновлення та копіювання посилання",
 	"Update for the latest features and improvements.": "Оновіть програми для нових функцій та покращень.",
 	"Update password": "Оновити пароль",
+	"Updated": "",
 	"Updated at": "Оновлено на",
 	"Upload": "Завантажити",
 	"Upload a GGUF model": "Завантажити GGUF модель",

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

@@ -294,6 +294,7 @@
 	"Export Prompts": "Tải các prompt về máy",
 	"Export Tools": "Tải Tools về máy",
 	"External Models": "Các model ngoài",
+	"Failed to add file.": "",
 	"Failed to create API Key.": "Lỗi khởi tạo API Key",
 	"Failed to read clipboard contents": "Không thể đọc nội dung clipboard",
 	"Failed to update settings": "Lỗi khi cập nhật các cài đặt",
@@ -449,6 +450,7 @@
 	"Name your model": "Tên model",
 	"New Chat": "Tạo chat mới",
 	"New Password": "Mật khẩu mới",
+	"No content found": "",
 	"No content to speak": "Không có nội dung để nói",
 	"No file selected": "Chưa có tệp nào được chọn",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -587,6 +589,7 @@
 	"Seed": "Seed",
 	"Select a base model": "Chọn một base model",
 	"Select a engine": "Chọn dịch vụ",
+	"Select a file to view or drag and drop a file to upload": "",
 	"Select a function": "Chọn function",
 	"Select a model": "Chọn mô hình",
 	"Select a pipeline": "Chọn một quy trình",
@@ -597,7 +600,6 @@
 	"Select Knowledge": "",
 	"Select model": "Chọn model",
 	"Select only one model to call": "Chọn model để gọi",
-	"Select/Add Files": "",
 	"Selected model(s) do not support image inputs": "Model được lựa chọn không hỗ trợ đầu vào là hình ảnh",
 	"Send": "Gửi",
 	"Send a Message": "Gửi yêu cầu",
@@ -725,6 +727,7 @@
 	"Update and Copy Link": "Cập nhật và sao chép link",
 	"Update for the latest features and improvements.": "",
 	"Update password": "Cập nhật mật khẩu",
+	"Updated": "",
 	"Updated at": "Cập nhật lúc",
 	"Upload": "",
 	"Upload a GGUF model": "Tải lên mô hình GGUF",

+ 15 - 12
src/lib/i18n/locales/zh-CN/translation.json

@@ -68,8 +68,8 @@
 	"Archived Chats": "已归档对话",
 	"are allowed - Activate this command by typing": "允许 - 通过输入来激活这个命令",
 	"Are you sure?": "是否确定?",
-	"Artifacts": "",
-	"Ask a question": "",
+	"Artifacts": "Artifacts",
+	"Ask a question": "提问",
 	"Attach file": "添加文件",
 	"Attention to detail": "注重细节",
 	"Audio": "语音",
@@ -294,6 +294,7 @@
 	"Export Prompts": "导出提示词",
 	"Export Tools": "导出工具",
 	"External Models": "外部模型",
+	"Failed to add file.": "添加文件失败。",
 	"Failed to create API Key.": "无法创建 API 密钥。",
 	"Failed to read clipboard contents": "无法读取剪贴板内容",
 	"Failed to update settings": "无法更新设置",
@@ -384,7 +385,7 @@
 	"Knowledge deleted successfully.": "知识成功删除",
 	"Knowledge reset successfully.": "知识成功重置",
 	"Knowledge updated successfully": "知识成功更新",
-	"Landing Page Mode": "",
+	"Landing Page Mode": "默认主页样式",
 	"Language": "语言",
 	"large language models, locally.": "本地大语言模型",
 	"Last Active": "最后在线时间",
@@ -449,9 +450,10 @@
 	"Name your model": "为您的模型命名",
 	"New Chat": "新对话",
 	"New Password": "新密码",
+	"No content found": "未发现内容",
 	"No content to speak": "没有内容可朗读",
 	"No file selected": "未选中文件",
-	"No HTML, CSS, or JavaScript content found.": "",
+	"No HTML, CSS, or JavaScript content found.": "未找到 HTML、CSS 或 JavaScript 内容。",
 	"No knowledge found": "未找到知识",
 	"No results found": "未找到结果",
 	"No search query generated": "未生成搜索查询",
@@ -483,7 +485,7 @@
 	"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 file": "打开文件",
-	"Open in full screen": "",
+	"Open in full screen": "全屏打开",
 	"Open new chat": "打开新对话",
 	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "当前 Open WebUI 版本 (v{{OPEN_WEBUI_VERSION}}) 低于所需的版本 (v{{REQUIRED_VERSION}})",
 	"OpenAI": "OpenAI",
@@ -561,7 +563,7 @@
 	"Save & Update": "保存并更新",
 	"Save As Copy": "另存为副本",
 	"Save Tag": "保存标签",
-	"Saved": "",
+	"Saved": "已保存",
 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "我们不再支持将聊天记录直接保存到浏览器的存储空间。请点击下面的按钮下载并删除您的聊天记录。别担心,您可以轻松地将聊天记录重新导入到后台。",
 	"Scroll to bottom when switching between branches": "在分支间切换时滚动到底部",
 	"Search": "搜索",
@@ -587,6 +589,7 @@
 	"Seed": "种子 (Seed)",
 	"Select a base model": "选择一个基础模型",
 	"Select a engine": "选择一个搜索引擎",
+	"Select a file to view or drag and drop a file to upload": "选择文件查看或拖放文件上传",
 	"Select a function": "选择一个函数",
 	"Select a model": "选择一个模型",
 	"Select a pipeline": "选择一个管道",
@@ -597,7 +600,6 @@
 	"Select Knowledge": "选择知识",
 	"Select model": "选择模型",
 	"Select only one model to call": "请仅选择一个模型来呼叫",
-	"Select/Add Files": "选择/添加 文件",
 	"Selected model(s) do not support image inputs": "已选择的模型不支持发送图像",
 	"Send": "发送",
 	"Send a Message": "输入消息",
@@ -632,11 +634,11 @@
 	"Show your support!": "表达你的支持!",
 	"Showcased creativity": "很有创意",
 	"Sign in": "登录",
-	"Sign in to {{WEBUI_NAME}}": "",
+	"Sign in to {{WEBUI_NAME}}": "登录 {{WEBUI_NAME}}",
 	"Sign Out": "登出",
 	"Sign up": "注册",
-	"Sign up to {{WEBUI_NAME}}": "",
-	"Signing in to {{WEBUI_NAME}}": "",
+	"Sign up to {{WEBUI_NAME}}": "注册 {{WEBUI_NAME}}",
+	"Signing in to {{WEBUI_NAME}}": "正在登录 {{WEBUI_NAME}}",
 	"Source": "来源",
 	"Speech Playback Speed": "语音播放速度",
 	"Speech recognition error: {{error}}": "语音识别错误:{{error}}",
@@ -677,7 +679,7 @@
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "这是一个实验功能,可能不会如预期那样工作,而且可能随时发生变化。",
 	"This option will delete all existing files in the collection and replace them with newly uploaded files.": "此选项将会删除文件集中所有文件,并用新上传的文件替换。",
 	"This will delete": "这将删除",
-	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
+	"This will reset the knowledge base and sync all files. Do you wish to continue?": "这将重置知识库并同步所有文件。确认继续吗?",
 	"Thorough explanation": "解释较为详细",
 	"Tika": "Tika",
 	"Tika Server URL required.": "请输入 Tika 服务器地址。",
@@ -725,6 +727,7 @@
 	"Update and Copy Link": "更新和复制链接",
 	"Update for the latest features and improvements.": "更新来获得最新功能与改进。",
 	"Update password": "更新密码",
+	"Updated": "已更新",
 	"Updated at": "更新于",
 	"Upload": "上传",
 	"Upload a GGUF model": "上传一个 GGUF 模型",
@@ -752,7 +755,7 @@
 	"variable": "变量",
 	"variable to have them replaced with clipboard content.": "变量将被剪贴板内容替换。",
 	"Version": "版本",
-	"Version {{selectedVersion}} of {{totalVersions}}": "",
+	"Version {{selectedVersion}} of {{totalVersions}}": "版本 {{selectedVersion}}/{{totalVersions}}",
 	"Voice": "语音",
 	"Warning": "警告",
 	"Warning:": "警告:",

Some files were not shown because too many files changed in this diff