Sfoglia il codice sorgente

Merge branch 'dev' into dev

Timothy Jaeryang Baek 6 mesi fa
parent
commit
768b7e139c
100 ha cambiato i file con 5077 aggiunte e 1302 eliminazioni
  1. 5 3
      backend/open_webui/apps/retrieval/main.py
  2. 10 8
      backend/open_webui/apps/retrieval/utils.py
  3. 8 2
      backend/open_webui/apps/retrieval/vector/dbs/chroma.py
  4. 3 1
      backend/open_webui/apps/webui/main.py
  5. 119 4
      backend/open_webui/apps/webui/models/chats.py
  6. 7 3
      backend/open_webui/apps/webui/models/files.py
  7. 271 0
      backend/open_webui/apps/webui/models/folders.py
  8. 81 0
      backend/open_webui/apps/webui/routers/chats.py
  9. 3 3
      backend/open_webui/apps/webui/routers/files.py
  10. 251 0
      backend/open_webui/apps/webui/routers/folders.py
  11. 6 0
      backend/open_webui/config.py
  12. 4 1
      backend/open_webui/constants.py
  13. 79 0
      backend/open_webui/main.py
  14. 79 0
      backend/open_webui/migrations/versions/c29facfe716b_update_file_table_path.py
  15. 50 0
      backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py
  16. 18 0
      backend/open_webui/utils/task.py
  17. 1 1
      backend/requirements.txt
  18. 3 3
      cypress/e2e/chat.cy.ts
  19. 233 3
      package-lock.json
  20. 10 0
      package.json
  21. 1 1
      pyproject.toml
  22. 28 2
      src/app.css
  23. 107 2
      src/lib/apis/chats/index.ts
  24. 269 0
      src/lib/apis/folders/index.ts
  25. 72 0
      src/lib/apis/index.ts
  26. 2 3
      src/lib/components/admin/Settings/Documents.svelte
  27. 18 6
      src/lib/components/admin/Settings/Interface.svelte
  28. 1 1
      src/lib/components/admin/Settings/Models.svelte
  29. 10 8
      src/lib/components/admin/Settings/Pipelines.svelte
  30. 88 22
      src/lib/components/chat/Chat.svelte
  31. 164 160
      src/lib/components/chat/MessageInput.svelte
  32. 4 4
      src/lib/components/chat/MessageInput/Commands.svelte
  33. 3 3
      src/lib/components/chat/MessageInput/Commands/Knowledge.svelte
  34. 1 1
      src/lib/components/chat/MessageInput/Commands/Models.svelte
  35. 7 11
      src/lib/components/chat/MessageInput/Commands/Prompts.svelte
  36. 8 3
      src/lib/components/chat/MessageInput/InputMenu.svelte
  37. 3 2
      src/lib/components/chat/MessageInput/VoiceRecording.svelte
  38. 6 12
      src/lib/components/chat/Messages.svelte
  39. 201 49
      src/lib/components/chat/Messages/Citations.svelte
  40. 69 9
      src/lib/components/chat/Messages/CitationsModal.svelte
  41. 17 4
      src/lib/components/chat/Messages/RateComment.svelte
  42. 24 22
      src/lib/components/chat/Messages/UserMessage.svelte
  43. 1 0
      src/lib/components/chat/ModelSelector.svelte
  44. 21 10
      src/lib/components/chat/ModelSelector/Selector.svelte
  45. 11 15
      src/lib/components/chat/Placeholder.svelte
  46. 28 0
      src/lib/components/chat/Settings/Interface.svelte
  47. 1 1
      src/lib/components/chat/ShortcutsModal.svelte
  48. 9 1
      src/lib/components/chat/Tags.svelte
  49. 2 1
      src/lib/components/common/Badge.svelte
  50. 28 7
      src/lib/components/common/Collapsible.svelte
  51. 9 4
      src/lib/components/common/ConfirmDialog.svelte
  52. 1 1
      src/lib/components/common/DragGhost.svelte
  53. 2 14
      src/lib/components/common/Drawer.svelte
  54. 1 1
      src/lib/components/common/Dropdown.svelte
  55. 54 15
      src/lib/components/common/Folder.svelte
  56. 7 3
      src/lib/components/common/Modal.svelte
  57. 470 0
      src/lib/components/common/RichTextInput.svelte
  58. 22 24
      src/lib/components/common/Tags/TagList.svelte
  59. 37 0
      src/lib/components/common/Textarea.svelte
  60. 0 124
      src/lib/components/icons/ChatMenu.svelte
  61. 19 0
      src/lib/components/icons/Document.svelte
  62. 19 0
      src/lib/components/icons/Download.svelte
  63. 10 0
      src/lib/components/icons/Mic.svelte
  64. 1 1
      src/lib/components/layout/Navbar.svelte
  65. 44 44
      src/lib/components/layout/Navbar/Menu.svelte
  66. 238 117
      src/lib/components/layout/Sidebar.svelte
  67. 88 58
      src/lib/components/layout/Sidebar/ChatItem.svelte
  68. 123 9
      src/lib/components/layout/Sidebar/ChatMenu.svelte
  69. 35 0
      src/lib/components/layout/Sidebar/Folders.svelte
  70. 70 0
      src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte
  71. 482 0
      src/lib/components/layout/Sidebar/RecursiveFolder.svelte
  72. 14 4
      src/lib/components/layout/Sidebar/SearchInput.svelte
  73. 8 8
      src/lib/components/layout/Sidebar/UserMenu.svelte
  74. 36 29
      src/lib/components/workspace/Functions.svelte
  75. 53 47
      src/lib/components/workspace/Functions/FunctionEditor.svelte
  76. 4 11
      src/lib/components/workspace/Knowledge.svelte
  77. 265 127
      src/lib/components/workspace/Knowledge/Collection.svelte
  78. 1 1
      src/lib/components/workspace/Knowledge/Collection/AddContentMenu.svelte
  79. 116 76
      src/lib/components/workspace/Knowledge/Collection/AddTextContentModal.svelte
  80. 9 1
      src/lib/components/workspace/Knowledge/Collection/Files.svelte
  81. 36 29
      src/lib/components/workspace/Models.svelte
  82. 48 41
      src/lib/components/workspace/Prompts.svelte
  83. 36 29
      src/lib/components/workspace/Tools.svelte
  84. 53 47
      src/lib/components/workspace/Tools/ToolkitEditor.svelte
  85. 13 2
      src/lib/i18n/locales/ar-BH/translation.json
  86. 13 2
      src/lib/i18n/locales/bg-BG/translation.json
  87. 13 2
      src/lib/i18n/locales/bn-BD/translation.json
  88. 26 15
      src/lib/i18n/locales/ca-ES/translation.json
  89. 13 2
      src/lib/i18n/locales/ceb-PH/translation.json
  90. 13 2
      src/lib/i18n/locales/de-DE/translation.json
  91. 13 2
      src/lib/i18n/locales/dg-DG/translation.json
  92. 13 2
      src/lib/i18n/locales/en-GB/translation.json
  93. 13 2
      src/lib/i18n/locales/en-US/translation.json
  94. 13 2
      src/lib/i18n/locales/es-ES/translation.json
  95. 13 2
      src/lib/i18n/locales/fa-IR/translation.json
  96. 13 2
      src/lib/i18n/locales/fi-FI/translation.json
  97. 13 2
      src/lib/i18n/locales/fr-CA/translation.json
  98. 13 2
      src/lib/i18n/locales/fr-FR/translation.json
  99. 13 2
      src/lib/i18n/locales/he-IL/translation.json
  100. 13 2
      src/lib/i18n/locales/hi-IN/translation.json

+ 5 - 3
backend/open_webui/apps/retrieval/main.py

@@ -709,8 +709,10 @@ def save_docs_to_vector_db(
             if overwrite:
             if overwrite:
                 VECTOR_DB_CLIENT.delete_collection(collection_name=collection_name)
                 VECTOR_DB_CLIENT.delete_collection(collection_name=collection_name)
                 log.info(f"deleting existing collection {collection_name}")
                 log.info(f"deleting existing collection {collection_name}")
-
-            if add is False:
+            elif add is False:
+                log.info(
+                    f"collection {collection_name} already exists, overwrite is False and add is False"
+                )
                 return True
                 return True
 
 
         log.info(f"adding to collection {collection_name}")
         log.info(f"adding to collection {collection_name}")
@@ -823,7 +825,7 @@ def process_file(
             # Process the file and save the content
             # Process the file and save the content
             # Usage: /files/
             # Usage: /files/
 
 
-            file_path = file.meta.get("path", None)
+            file_path = file.path
             if file_path:
             if file_path:
                 loader = Loader(
                 loader = Loader(
                     engine=app.state.config.CONTENT_EXTRACTION_ENGINE,
                     engine=app.state.config.CONTENT_EXTRACTION_ENGINE,

+ 10 - 8
backend/open_webui/apps/retrieval/utils.py

@@ -385,6 +385,8 @@ def get_rag_context(
             extracted_collections.extend(collection_names)
             extracted_collections.extend(collection_names)
 
 
         if context:
         if context:
+            if "data" in file:
+                del file["data"]
             relevant_contexts.append({**context, "file": file})
             relevant_contexts.append({**context, "file": file})
 
 
     contexts = []
     contexts = []
@@ -401,7 +403,6 @@ def get_rag_context(
                         ]
                         ]
                     )
                     )
                 )
                 )
-
                 contexts.append(
                 contexts.append(
                     ((", ".join(file_names) + ":\n\n") if file_names else "")
                     ((", ".join(file_names) + ":\n\n") if file_names else "")
                     + "\n\n".join(
                     + "\n\n".join(
@@ -410,13 +411,14 @@ def get_rag_context(
                 )
                 )
 
 
                 if "metadatas" in context:
                 if "metadatas" in context:
-                    citations.append(
-                        {
-                            "source": context["file"],
-                            "document": context["documents"][0],
-                            "metadata": context["metadatas"][0],
-                        }
-                    )
+                    citation = {
+                        "source": context["file"],
+                        "document": context["documents"][0],
+                        "metadata": context["metadatas"][0],
+                    }
+                    if "distances" in context and context["distances"]:
+                        citation["distances"] = context["distances"][0]
+                    citations.append(citation)
         except Exception as e:
         except Exception as e:
             log.exception(e)
             log.exception(e)
 
 

+ 8 - 2
backend/open_webui/apps/retrieval/vector/dbs/chroma.py

@@ -109,7 +109,10 @@ class ChromaClient:
 
 
     def insert(self, collection_name: str, items: list[VectorItem]):
     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.
         # Insert the items into the collection, if the collection does not exist, it will be created.
-        collection = self.client.get_or_create_collection(name=collection_name)
+        collection = self.client.get_or_create_collection(
+            name=collection_name,
+            metadata={"hnsw:space": "cosine"}
+            )
 
 
         ids = [item["id"] for item in items]
         ids = [item["id"] for item in items]
         documents = [item["text"] for item in items]
         documents = [item["text"] for item in items]
@@ -127,7 +130,10 @@ class ChromaClient:
 
 
     def upsert(self, collection_name: str, items: list[VectorItem]):
     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.
         # Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created.
-        collection = self.client.get_or_create_collection(name=collection_name)
+        collection = self.client.get_or_create_collection(
+            name=collection_name,
+            metadata={"hnsw:space": "cosine"}
+            )
 
 
         ids = [item["id"] for item in items]
         ids = [item["id"] for item in items]
         documents = [item["text"] for item in items]
         documents = [item["text"] for item in items]

+ 3 - 1
backend/open_webui/apps/webui/main.py

@@ -9,6 +9,7 @@ from open_webui.apps.webui.models.models import Models
 from open_webui.apps.webui.routers import (
 from open_webui.apps.webui.routers import (
     auths,
     auths,
     chats,
     chats,
+    folders,
     configs,
     configs,
     files,
     files,
     functions,
     functions,
@@ -119,6 +120,7 @@ app.include_router(configs.router, prefix="/configs", tags=["configs"])
 app.include_router(auths.router, prefix="/auths", tags=["auths"])
 app.include_router(auths.router, prefix="/auths", tags=["auths"])
 app.include_router(users.router, prefix="/users", tags=["users"])
 app.include_router(users.router, prefix="/users", tags=["users"])
 app.include_router(chats.router, prefix="/chats", tags=["chats"])
 app.include_router(chats.router, prefix="/chats", tags=["chats"])
+app.include_router(folders.router, prefix="/folders", tags=["folders"])
 
 
 app.include_router(models.router, prefix="/models", tags=["models"])
 app.include_router(models.router, prefix="/models", tags=["models"])
 app.include_router(knowledge.router, prefix="/knowledge", tags=["knowledge"])
 app.include_router(knowledge.router, prefix="/knowledge", tags=["knowledge"])
@@ -344,7 +346,7 @@ async def generate_function_chat_completion(form_data, user):
     pipe = function_module.pipe
     pipe = function_module.pipe
     params = get_function_params(function_module, form_data, user, extra_params)
     params = get_function_params(function_module, form_data, user, extra_params)
 
 
-    if form_data["stream"]:
+    if form_data.get("stream", False):
 
 
         async def stream_content():
         async def stream_content():
             try:
             try:

+ 119 - 4
backend/open_webui/apps/webui/models/chats.py

@@ -33,6 +33,7 @@ class Chat(Base):
     pinned = Column(Boolean, default=False, nullable=True)
     pinned = Column(Boolean, default=False, nullable=True)
 
 
     meta = Column(JSON, server_default="{}")
     meta = Column(JSON, server_default="{}")
+    folder_id = Column(Text, nullable=True)
 
 
 
 
 class ChatModel(BaseModel):
 class ChatModel(BaseModel):
@@ -51,6 +52,7 @@ class ChatModel(BaseModel):
     pinned: Optional[bool] = False
     pinned: Optional[bool] = False
 
 
     meta: dict = {}
     meta: dict = {}
+    folder_id: Optional[str] = None
 
 
 
 
 ####################
 ####################
@@ -62,6 +64,12 @@ class ChatForm(BaseModel):
     chat: dict
     chat: dict
 
 
 
 
+class ChatImportForm(ChatForm):
+    meta: Optional[dict] = {}
+    pinned: Optional[bool] = False
+    folder_id: Optional[str] = None
+
+
 class ChatTitleMessagesForm(BaseModel):
 class ChatTitleMessagesForm(BaseModel):
     title: str
     title: str
     messages: list[dict]
     messages: list[dict]
@@ -82,6 +90,7 @@ class ChatResponse(BaseModel):
     archived: bool
     archived: bool
     pinned: Optional[bool] = False
     pinned: Optional[bool] = False
     meta: dict = {}
     meta: dict = {}
+    folder_id: Optional[str] = None
 
 
 
 
 class ChatTitleIdResponse(BaseModel):
 class ChatTitleIdResponse(BaseModel):
@@ -116,6 +125,35 @@ class ChatTable:
             db.refresh(result)
             db.refresh(result)
             return ChatModel.model_validate(result) if result else None
             return ChatModel.model_validate(result) if result else None
 
 
+    def import_chat(
+        self, user_id: str, form_data: ChatImportForm
+    ) -> Optional[ChatModel]:
+        with get_db() as db:
+            id = str(uuid.uuid4())
+            chat = ChatModel(
+                **{
+                    "id": id,
+                    "user_id": user_id,
+                    "title": (
+                        form_data.chat["title"]
+                        if "title" in form_data.chat
+                        else "New Chat"
+                    ),
+                    "chat": form_data.chat,
+                    "meta": form_data.meta,
+                    "pinned": form_data.pinned,
+                    "folder_id": form_data.folder_id,
+                    "created_at": int(time.time()),
+                    "updated_at": int(time.time()),
+                }
+            )
+
+            result = Chat(**chat.model_dump())
+            db.add(result)
+            db.commit()
+            db.refresh(result)
+            return ChatModel.model_validate(result) if result else None
+
     def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
     def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
         try:
         try:
             with get_db() as db:
             with get_db() as db:
@@ -254,7 +292,7 @@ class ChatTable:
         limit: int = 50,
         limit: int = 50,
     ) -> list[ChatModel]:
     ) -> list[ChatModel]:
         with get_db() as db:
         with get_db() as db:
-            query = db.query(Chat).filter_by(user_id=user_id)
+            query = db.query(Chat).filter_by(user_id=user_id).filter_by(folder_id=None)
             if not include_archived:
             if not include_archived:
                 query = query.filter_by(archived=False)
                 query = query.filter_by(archived=False)
 
 
@@ -276,7 +314,7 @@ class ChatTable:
         limit: Optional[int] = None,
         limit: Optional[int] = None,
     ) -> list[ChatTitleIdResponse]:
     ) -> list[ChatTitleIdResponse]:
         with get_db() as db:
         with get_db() as db:
-            query = db.query(Chat).filter_by(user_id=user_id)
+            query = db.query(Chat).filter_by(user_id=user_id).filter_by(folder_id=None)
             query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
             query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
 
 
             if not include_archived:
             if not include_archived:
@@ -444,7 +482,18 @@ class ChatTable:
                 )
                 )
 
 
                 # Check if there are any tags to filter, it should have all the tags
                 # Check if there are any tags to filter, it should have all the tags
-                if tag_ids:
+                if "none" in tag_ids:
+                    query = query.filter(
+                        text(
+                            """
+                            NOT EXISTS (
+                                SELECT 1
+                                FROM json_each(Chat.meta, '$.tags') AS tag
+                            )
+                            """
+                        )
+                    )
+                elif tag_ids:
                     query = query.filter(
                     query = query.filter(
                         and_(
                         and_(
                             *[
                             *[
@@ -482,7 +531,18 @@ class ChatTable:
                 )
                 )
 
 
                 # Check if there are any tags to filter, it should have all the tags
                 # Check if there are any tags to filter, it should have all the tags
-                if tag_ids:
+                if "none" in tag_ids:
+                    query = query.filter(
+                        text(
+                            """
+                            NOT EXISTS (
+                                SELECT 1
+                                FROM json_array_elements_text(Chat.meta->'tags') AS tag
+                            )
+                            """
+                        )
+                    )
+                elif tag_ids:
                     query = query.filter(
                     query = query.filter(
                         and_(
                         and_(
                             *[
                             *[
@@ -512,6 +572,49 @@ class ChatTable:
             # Validate and return chats
             # Validate and return chats
             return [ChatModel.model_validate(chat) for chat in all_chats]
             return [ChatModel.model_validate(chat) for chat in all_chats]
 
 
+    def get_chats_by_folder_id_and_user_id(
+        self, folder_id: str, user_id: str
+    ) -> list[ChatModel]:
+        with get_db() as db:
+            query = db.query(Chat).filter_by(folder_id=folder_id, user_id=user_id)
+            query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
+            query = query.filter_by(archived=False)
+
+            query = query.order_by(Chat.updated_at.desc())
+
+            all_chats = query.all()
+            return [ChatModel.model_validate(chat) for chat in all_chats]
+
+    def get_chats_by_folder_ids_and_user_id(
+        self, folder_ids: list[str], user_id: str
+    ) -> list[ChatModel]:
+        with get_db() as db:
+            query = db.query(Chat).filter(
+                Chat.folder_id.in_(folder_ids), Chat.user_id == user_id
+            )
+            query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
+            query = query.filter_by(archived=False)
+
+            query = query.order_by(Chat.updated_at.desc())
+
+            all_chats = query.all()
+            return [ChatModel.model_validate(chat) for chat in all_chats]
+
+    def update_chat_folder_id_by_id_and_user_id(
+        self, id: str, user_id: str, folder_id: str
+    ) -> Optional[ChatModel]:
+        try:
+            with get_db() as db:
+                chat = db.get(Chat, id)
+                chat.folder_id = folder_id
+                chat.updated_at = int(time.time())
+                chat.pinned = False
+                db.commit()
+                db.refresh(chat)
+                return ChatModel.model_validate(chat)
+        except Exception:
+            return None
+
     def get_chat_tags_by_id_and_user_id(self, id: str, user_id: str) -> list[TagModel]:
     def get_chat_tags_by_id_and_user_id(self, id: str, user_id: str) -> list[TagModel]:
         with get_db() as db:
         with get_db() as db:
             chat = db.get(Chat, id)
             chat = db.get(Chat, id)
@@ -673,6 +776,18 @@ class ChatTable:
         except Exception:
         except Exception:
             return False
             return False
 
 
+    def delete_chats_by_user_id_and_folder_id(
+        self, user_id: str, folder_id: str
+    ) -> bool:
+        try:
+            with get_db() as db:
+                db.query(Chat).filter_by(user_id=user_id, folder_id=folder_id).delete()
+                db.commit()
+
+                return True
+        except Exception:
+            return False
+
     def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
     def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
         try:
         try:
             with get_db() as db:
             with get_db() as db:

+ 7 - 3
backend/open_webui/apps/webui/models/files.py

@@ -17,14 +17,15 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
 
 
 class File(Base):
 class File(Base):
     __tablename__ = "file"
     __tablename__ = "file"
-
     id = Column(String, primary_key=True)
     id = Column(String, primary_key=True)
     user_id = Column(String)
     user_id = Column(String)
     hash = Column(Text, nullable=True)
     hash = Column(Text, nullable=True)
 
 
     filename = Column(Text)
     filename = Column(Text)
+    path = Column(Text, nullable=True)
+
     data = Column(JSON, nullable=True)
     data = Column(JSON, nullable=True)
-    meta = Column(JSONField)
+    meta = Column(JSON, nullable=True)
 
 
     created_at = Column(BigInteger)
     created_at = Column(BigInteger)
     updated_at = Column(BigInteger)
     updated_at = Column(BigInteger)
@@ -38,8 +39,10 @@ class FileModel(BaseModel):
     hash: Optional[str] = None
     hash: Optional[str] = None
 
 
     filename: str
     filename: str
+    path: Optional[str] = None
+
     data: Optional[dict] = None
     data: Optional[dict] = None
-    meta: dict
+    meta: Optional[dict] = None
 
 
     created_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
@@ -82,6 +85,7 @@ class FileForm(BaseModel):
     id: str
     id: str
     hash: Optional[str] = None
     hash: Optional[str] = None
     filename: str
     filename: str
+    path: str
     data: dict = {}
     data: dict = {}
     meta: dict = {}
     meta: dict = {}
 
 

+ 271 - 0
backend/open_webui/apps/webui/models/folders.py

@@ -0,0 +1,271 @@
+import logging
+import time
+import uuid
+from typing import Optional
+
+from open_webui.apps.webui.internal.db import Base, get_db
+from open_webui.apps.webui.models.chats import Chats
+
+from open_webui.env import SRC_LOG_LEVELS
+from pydantic import BaseModel, ConfigDict
+from sqlalchemy import BigInteger, Column, Text, JSON, Boolean
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["MODELS"])
+
+
+####################
+# Folder DB Schema
+####################
+
+
+class Folder(Base):
+    __tablename__ = "folder"
+    id = Column(Text, primary_key=True)
+    parent_id = Column(Text, nullable=True)
+    user_id = Column(Text)
+    name = Column(Text)
+    items = Column(JSON, nullable=True)
+    meta = Column(JSON, nullable=True)
+    is_expanded = Column(Boolean, default=False)
+    created_at = Column(BigInteger)
+    updated_at = Column(BigInteger)
+
+
+class FolderModel(BaseModel):
+    id: str
+    parent_id: Optional[str] = None
+    user_id: str
+    name: str
+    items: Optional[dict] = None
+    meta: Optional[dict] = None
+    is_expanded: bool = False
+    created_at: int
+    updated_at: int
+
+    model_config = ConfigDict(from_attributes=True)
+
+
+####################
+# Forms
+####################
+
+
+class FolderForm(BaseModel):
+    name: str
+    model_config = ConfigDict(extra="allow")
+
+
+class FolderTable:
+    def insert_new_folder(
+        self, user_id: str, name: str, parent_id: Optional[str] = None
+    ) -> Optional[FolderModel]:
+        with get_db() as db:
+            id = str(uuid.uuid4())
+            folder = FolderModel(
+                **{
+                    "id": id,
+                    "user_id": user_id,
+                    "name": name,
+                    "parent_id": parent_id,
+                    "created_at": int(time.time()),
+                    "updated_at": int(time.time()),
+                }
+            )
+            try:
+                result = Folder(**folder.model_dump())
+                db.add(result)
+                db.commit()
+                db.refresh(result)
+                if result:
+                    return FolderModel.model_validate(result)
+                else:
+                    return None
+            except Exception as e:
+                print(e)
+                return None
+
+    def get_folder_by_id_and_user_id(
+        self, id: str, user_id: str
+    ) -> Optional[FolderModel]:
+        try:
+            with get_db() as db:
+                folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
+
+                if not folder:
+                    return None
+
+                return FolderModel.model_validate(folder)
+        except Exception:
+            return None
+
+    def get_children_folders_by_id_and_user_id(
+        self, id: str, user_id: str
+    ) -> Optional[FolderModel]:
+        try:
+            with get_db() as db:
+                folders = []
+
+                def get_children(folder):
+                    children = self.get_folders_by_parent_id_and_user_id(
+                        folder.id, user_id
+                    )
+                    for child in children:
+                        get_children(child)
+                        folders.append(child)
+
+                folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
+                if not folder:
+                    return None
+
+                get_children(folder)
+                return folders
+        except Exception:
+            return None
+
+    def get_folders_by_user_id(self, user_id: str) -> list[FolderModel]:
+        with get_db() as db:
+            return [
+                FolderModel.model_validate(folder)
+                for folder in db.query(Folder).filter_by(user_id=user_id).all()
+            ]
+
+    def get_folder_by_parent_id_and_user_id_and_name(
+        self, parent_id: Optional[str], user_id: str, name: str
+    ) -> Optional[FolderModel]:
+        try:
+            with get_db() as db:
+                # Check if folder exists
+                folder = (
+                    db.query(Folder)
+                    .filter_by(parent_id=parent_id, user_id=user_id)
+                    .filter(Folder.name.ilike(name))
+                    .first()
+                )
+
+                if not folder:
+                    return None
+
+                return FolderModel.model_validate(folder)
+        except Exception as e:
+            log.error(f"get_folder_by_parent_id_and_user_id_and_name: {e}")
+            return None
+
+    def get_folders_by_parent_id_and_user_id(
+        self, parent_id: Optional[str], user_id: str
+    ) -> list[FolderModel]:
+        with get_db() as db:
+            return [
+                FolderModel.model_validate(folder)
+                for folder in db.query(Folder)
+                .filter_by(parent_id=parent_id, user_id=user_id)
+                .all()
+            ]
+
+    def update_folder_parent_id_by_id_and_user_id(
+        self,
+        id: str,
+        user_id: str,
+        parent_id: str,
+    ) -> Optional[FolderModel]:
+        try:
+            with get_db() as db:
+                folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
+
+                if not folder:
+                    return None
+
+                folder.parent_id = parent_id
+                folder.updated_at = int(time.time())
+
+                db.commit()
+
+                return FolderModel.model_validate(folder)
+        except Exception as e:
+            log.error(f"update_folder: {e}")
+            return
+
+    def update_folder_name_by_id_and_user_id(
+        self, id: str, user_id: str, name: str
+    ) -> Optional[FolderModel]:
+        try:
+            with get_db() as db:
+                folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
+
+                if not folder:
+                    return None
+
+                existing_folder = (
+                    db.query(Folder)
+                    .filter_by(name=name, parent_id=folder.parent_id, user_id=user_id)
+                    .first()
+                )
+
+                if existing_folder:
+                    return None
+
+                folder.name = name
+                folder.updated_at = int(time.time())
+
+                db.commit()
+
+                return FolderModel.model_validate(folder)
+        except Exception as e:
+            log.error(f"update_folder: {e}")
+            return
+
+    def update_folder_is_expanded_by_id_and_user_id(
+        self, id: str, user_id: str, is_expanded: bool
+    ) -> Optional[FolderModel]:
+        try:
+            with get_db() as db:
+                folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
+
+                if not folder:
+                    return None
+
+                folder.is_expanded = is_expanded
+                folder.updated_at = int(time.time())
+
+                db.commit()
+
+                return FolderModel.model_validate(folder)
+        except Exception as e:
+            log.error(f"update_folder: {e}")
+            return
+
+    def delete_folder_by_id_and_user_id(self, id: str, user_id: str) -> bool:
+        try:
+            with get_db() as db:
+                folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
+                if not folder:
+                    return False
+
+                # Delete all chats in the folder
+                Chats.delete_chats_by_user_id_and_folder_id(user_id, folder.id)
+
+                # Delete all children folders
+                def delete_children(folder):
+                    folder_children = self.get_folders_by_parent_id_and_user_id(
+                        folder.id, user_id
+                    )
+                    for folder_child in folder_children:
+                        Chats.delete_chats_by_user_id_and_folder_id(
+                            user_id, folder_child.id
+                        )
+                        delete_children(folder_child)
+
+                        folder = db.query(Folder).filter_by(id=folder_child.id).first()
+                        db.delete(folder)
+                        db.commit()
+
+                delete_children(folder)
+                db.delete(folder)
+                db.commit()
+                return True
+        except Exception as e:
+            log.error(f"delete_folder: {e}")
+            return False
+
+
+Folders = FolderTable()

+ 81 - 0
backend/open_webui/apps/webui/routers/chats.py

@@ -4,11 +4,13 @@ from typing import Optional
 
 
 from open_webui.apps.webui.models.chats import (
 from open_webui.apps.webui.models.chats import (
     ChatForm,
     ChatForm,
+    ChatImportForm,
     ChatResponse,
     ChatResponse,
     Chats,
     Chats,
     ChatTitleIdResponse,
     ChatTitleIdResponse,
 )
 )
 from open_webui.apps.webui.models.tags import TagModel, Tags
 from open_webui.apps.webui.models.tags import TagModel, Tags
+from open_webui.apps.webui.models.folders import Folders
 
 
 from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
 from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
@@ -99,6 +101,34 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_verified_user)):
         )
         )
 
 
 
 
+############################
+# ImportChat
+############################
+
+
+@router.post("/import", response_model=Optional[ChatResponse])
+async def import_chat(form_data: ChatImportForm, user=Depends(get_verified_user)):
+    try:
+        chat = Chats.import_chat(user.id, form_data)
+        if chat:
+            tags = chat.meta.get("tags", [])
+            for tag_id in tags:
+                tag_id = tag_id.replace(" ", "_").lower()
+                tag_name = " ".join([word.capitalize() for word in tag_id.split("_")])
+                if (
+                    tag_id != "none"
+                    and Tags.get_tag_by_name_and_user_id(tag_name, user.id) is None
+                ):
+                    Tags.insert_new_tag(tag_name, user.id)
+
+        return ChatResponse(**chat.model_dump())
+    except Exception as e:
+        log.exception(e)
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+        )
+
+
 ############################
 ############################
 # GetChats
 # GetChats
 ############################
 ############################
@@ -133,6 +163,26 @@ async def search_user_chats(
     return chat_list
     return chat_list
 
 
 
 
+############################
+# GetChatsByFolderId
+############################
+
+
+@router.get("/folder/{folder_id}", response_model=list[ChatResponse])
+async def get_chats_by_folder_id(folder_id: str, user=Depends(get_verified_user)):
+    folder_ids = [folder_id]
+    children_folders = Folders.get_children_folders_by_id_and_user_id(
+        folder_id, user.id
+    )
+    if children_folders:
+        folder_ids.extend([folder.id for folder in children_folders])
+
+    return [
+        ChatResponse(**chat.model_dump())
+        for chat in Chats.get_chats_by_folder_ids_and_user_id(folder_ids, user.id)
+    ]
+
+
 ############################
 ############################
 # GetPinnedChats
 # GetPinnedChats
 ############################
 ############################
@@ -491,6 +541,31 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
         )
         )
 
 
 
 
+############################
+# UpdateChatFolderIdById
+############################
+
+
+class ChatFolderIdForm(BaseModel):
+    folder_id: Optional[str] = None
+
+
+@router.post("/{id}/folder", response_model=Optional[ChatResponse])
+async def update_chat_folder_id_by_id(
+    id: str, form_data: ChatFolderIdForm, user=Depends(get_verified_user)
+):
+    chat = Chats.get_chat_by_id_and_user_id(id, user.id)
+    if chat:
+        chat = Chats.update_chat_folder_id_by_id_and_user_id(
+            id, user.id, form_data.folder_id
+        )
+        return ChatResponse(**chat.model_dump())
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
+        )
+
+
 ############################
 ############################
 # GetChatTagsById
 # GetChatTagsById
 ############################
 ############################
@@ -522,6 +597,12 @@ async def add_tag_by_id_and_tag_name(
         tags = chat.meta.get("tags", [])
         tags = chat.meta.get("tags", [])
         tag_id = form_data.name.replace(" ", "_").lower()
         tag_id = form_data.name.replace(" ", "_").lower()
 
 
+        if tag_id == "none":
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT("Tag name cannot be 'None'"),
+            )
+
         print(tags, tag_id)
         print(tags, tag_id)
         if tag_id not in tags:
         if tag_id not in tags:
             Chats.add_chat_tag_by_id_and_user_id_and_tag_name(
             Chats.add_chat_tag_by_id_and_user_id_and_tag_name(

+ 3 - 3
backend/open_webui/apps/webui/routers/files.py

@@ -57,11 +57,11 @@ def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
                 **{
                 **{
                     "id": id,
                     "id": id,
                     "filename": filename,
                     "filename": filename,
+                    "path": file_path,
                     "meta": {
                     "meta": {
                         "name": name,
                         "name": name,
                         "content_type": file.content_type,
                         "content_type": file.content_type,
                         "size": len(contents),
                         "size": len(contents),
-                        "path": file_path,
                     },
                     },
                 }
                 }
             ),
             ),
@@ -218,7 +218,7 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
     file = Files.get_file_by_id(id)
     file = Files.get_file_by_id(id)
 
 
     if file and (file.user_id == user.id or user.role == "admin"):
     if file and (file.user_id == user.id or user.role == "admin"):
-        file_path = Path(file.meta["path"])
+        file_path = Path(file.path)
 
 
         # Check if the file already exists in the cache
         # Check if the file already exists in the cache
         if file_path.is_file():
         if file_path.is_file():
@@ -244,7 +244,7 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
     file = Files.get_file_by_id(id)
     file = Files.get_file_by_id(id)
 
 
     if file and (file.user_id == user.id or user.role == "admin"):
     if file and (file.user_id == user.id or user.role == "admin"):
-        file_path = file.meta.get("path")
+        file_path = file.path
         if file_path:
         if file_path:
             file_path = Path(file_path)
             file_path = Path(file_path)
 
 

+ 251 - 0
backend/open_webui/apps/webui/routers/folders.py

@@ -0,0 +1,251 @@
+import logging
+import os
+import shutil
+import uuid
+from pathlib import Path
+from typing import Optional
+from pydantic import BaseModel
+import mimetypes
+
+
+from open_webui.apps.webui.models.folders import (
+    FolderForm,
+    FolderModel,
+    Folders,
+)
+from open_webui.apps.webui.models.chats import Chats
+
+from open_webui.config import UPLOAD_DIR
+from open_webui.env import SRC_LOG_LEVELS
+from open_webui.constants import ERROR_MESSAGES
+
+
+from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
+from fastapi.responses import FileResponse, StreamingResponse
+
+
+from open_webui.utils.utils import get_admin_user, get_verified_user
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["MODELS"])
+
+
+router = APIRouter()
+
+
+############################
+# Get Folders
+############################
+
+
+@router.get("/", response_model=list[FolderModel])
+async def get_folders(user=Depends(get_verified_user)):
+    folders = Folders.get_folders_by_user_id(user.id)
+
+    return [
+        {
+            **folder.model_dump(),
+            "items": {
+                "chats": [
+                    {"title": chat.title, "id": chat.id}
+                    for chat in Chats.get_chats_by_folder_id_and_user_id(
+                        folder.id, user.id
+                    )
+                ]
+            },
+        }
+        for folder in folders
+    ]
+
+
+############################
+# Create Folder
+############################
+
+
+@router.post("/")
+def create_folder(form_data: FolderForm, user=Depends(get_verified_user)):
+    folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
+        None, user.id, form_data.name
+    )
+
+    if folder:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
+        )
+
+    try:
+        folder = Folders.insert_new_folder(user.id, form_data.name)
+        return folder
+    except Exception as e:
+        log.exception(e)
+        log.error("Error creating folder")
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ERROR_MESSAGES.DEFAULT("Error creating folder"),
+        )
+
+
+############################
+# Get Folders By Id
+############################
+
+
+@router.get("/{id}", response_model=Optional[FolderModel])
+async def get_folder_by_id(id: str, user=Depends(get_verified_user)):
+    folder = Folders.get_folder_by_id_and_user_id(id, user.id)
+    if folder:
+        return folder
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+
+############################
+# Update Folder Name By Id
+############################
+
+
+@router.post("/{id}/update")
+async def update_folder_name_by_id(
+    id: str, form_data: FolderForm, user=Depends(get_verified_user)
+):
+    folder = Folders.get_folder_by_id_and_user_id(id, user.id)
+    if folder:
+        existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
+            folder.parent_id, user.id, form_data.name
+        )
+        if existing_folder:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
+            )
+
+        try:
+            folder = Folders.update_folder_name_by_id_and_user_id(
+                id, user.id, form_data.name
+            )
+
+            return folder
+        except Exception as e:
+            log.exception(e)
+            log.error(f"Error updating folder: {id}")
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT("Error updating folder"),
+            )
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+
+############################
+# Update Folder Parent Id By Id
+############################
+
+
+class FolderParentIdForm(BaseModel):
+    parent_id: Optional[str] = None
+
+
+@router.post("/{id}/update/parent")
+async def update_folder_parent_id_by_id(
+    id: str, form_data: FolderParentIdForm, user=Depends(get_verified_user)
+):
+    folder = Folders.get_folder_by_id_and_user_id(id, user.id)
+    if folder:
+        existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
+            form_data.parent_id, user.id, folder.name
+        )
+
+        if existing_folder:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
+            )
+
+        try:
+            folder = Folders.update_folder_parent_id_by_id_and_user_id(
+                id, user.id, form_data.parent_id
+            )
+            return folder
+        except Exception as e:
+            log.exception(e)
+            log.error(f"Error updating folder: {id}")
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT("Error updating folder"),
+            )
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+
+############################
+# Update Folder Is Expanded By Id
+############################
+
+
+class FolderIsExpandedForm(BaseModel):
+    is_expanded: bool
+
+
+@router.post("/{id}/update/expanded")
+async def update_folder_is_expanded_by_id(
+    id: str, form_data: FolderIsExpandedForm, user=Depends(get_verified_user)
+):
+    folder = Folders.get_folder_by_id_and_user_id(id, user.id)
+    if folder:
+        try:
+            folder = Folders.update_folder_is_expanded_by_id_and_user_id(
+                id, user.id, form_data.is_expanded
+            )
+            return folder
+        except Exception as e:
+            log.exception(e)
+            log.error(f"Error updating folder: {id}")
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT("Error updating folder"),
+            )
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+
+############################
+# Delete Folder By Id
+############################
+
+
+@router.delete("/{id}")
+async def delete_folder_by_id(id: str, user=Depends(get_verified_user)):
+    folder = Folders.get_folder_by_id_and_user_id(id, user.id)
+    if folder:
+        try:
+            result = Folders.delete_folder_by_id_and_user_id(id, user.id)
+            if result:
+                return result
+            else:
+                raise Exception("Error deleting folder")
+        except Exception as e:
+            log.exception(e)
+            log.error(f"Error deleting folder: {id}")
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ERROR_MESSAGES.DEFAULT("Error deleting folder"),
+            )
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )

+ 6 - 0
backend/open_webui/config.py

@@ -876,6 +876,12 @@ TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
     os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""),
     os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""),
 )
 )
 
 
+TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
+    "TAGS_GENERATION_PROMPT_TEMPLATE",
+    "task.tags.prompt_template",
+    os.environ.get("TAGS_GENERATION_PROMPT_TEMPLATE", ""),
+)
+
 ENABLE_SEARCH_QUERY = PersistentConfig(
 ENABLE_SEARCH_QUERY = PersistentConfig(
     "ENABLE_SEARCH_QUERY",
     "ENABLE_SEARCH_QUERY",
     "task.search.enable",
     "task.search.enable",

+ 4 - 1
backend/open_webui/constants.py

@@ -20,7 +20,9 @@ class ERROR_MESSAGES(str, Enum):
     def __str__(self) -> str:
     def __str__(self) -> str:
         return super().__str__()
         return super().__str__()
 
 
-    DEFAULT = lambda err="": f"Something went wrong :/\n[ERROR: {err if err else ''}]"
+    DEFAULT = (
+        lambda err="": f'{"Something went wrong :/" if err == "" else "[ERROR: " + str(err) + "]"}'
+    )
     ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
     ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
     CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
     CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
     DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
     DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
@@ -106,6 +108,7 @@ class TASKS(str, Enum):
 
 
     DEFAULT = lambda task="": f"{task if task else 'generation'}"
     DEFAULT = lambda task="": f"{task if task else 'generation'}"
     TITLE_GENERATION = "title_generation"
     TITLE_GENERATION = "title_generation"
+    TAGS_GENERATION = "tags_generation"
     EMOJI_GENERATION = "emoji_generation"
     EMOJI_GENERATION = "emoji_generation"
     QUERY_GENERATION = "query_generation"
     QUERY_GENERATION = "query_generation"
     FUNCTION_CALLING = "function_calling"
     FUNCTION_CALLING = "function_calling"

+ 79 - 0
backend/open_webui/main.py

@@ -82,6 +82,7 @@ from open_webui.config import (
     TASK_MODEL,
     TASK_MODEL,
     TASK_MODEL_EXTERNAL,
     TASK_MODEL_EXTERNAL,
     TITLE_GENERATION_PROMPT_TEMPLATE,
     TITLE_GENERATION_PROMPT_TEMPLATE,
+    TAGS_GENERATION_PROMPT_TEMPLATE,
     TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
     TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
     WEBHOOK_URL,
     WEBHOOK_URL,
     WEBUI_AUTH,
     WEBUI_AUTH,
@@ -118,6 +119,7 @@ from open_webui.utils.response import (
 from open_webui.utils.security_headers import SecurityHeadersMiddleware
 from open_webui.utils.security_headers import SecurityHeadersMiddleware
 from open_webui.utils.task import (
 from open_webui.utils.task import (
     moa_response_generation_template,
     moa_response_generation_template,
+    tags_generation_template,
     search_query_generation_template,
     search_query_generation_template,
     title_generation_template,
     title_generation_template,
     tools_function_calling_generation_template,
     tools_function_calling_generation_template,
@@ -194,6 +196,7 @@ app.state.config.WEBHOOK_URL = WEBHOOK_URL
 app.state.config.TASK_MODEL = TASK_MODEL
 app.state.config.TASK_MODEL = TASK_MODEL
 app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
 app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
 app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
 app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
+app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE
 app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
 app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
     SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
     SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
 )
 )
@@ -1403,6 +1406,7 @@ async def get_task_config(user=Depends(get_verified_user)):
         "TASK_MODEL": app.state.config.TASK_MODEL,
         "TASK_MODEL": app.state.config.TASK_MODEL,
         "TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
         "TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
         "TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
         "TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
+        "TAGS_GENERATION_PROMPT_TEMPLATE": app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
         "ENABLE_SEARCH_QUERY": app.state.config.ENABLE_SEARCH_QUERY,
         "ENABLE_SEARCH_QUERY": app.state.config.ENABLE_SEARCH_QUERY,
         "SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
         "SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
         "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
         "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
@@ -1413,6 +1417,7 @@ class TaskConfigForm(BaseModel):
     TASK_MODEL: Optional[str]
     TASK_MODEL: Optional[str]
     TASK_MODEL_EXTERNAL: Optional[str]
     TASK_MODEL_EXTERNAL: Optional[str]
     TITLE_GENERATION_PROMPT_TEMPLATE: str
     TITLE_GENERATION_PROMPT_TEMPLATE: str
+    TAGS_GENERATION_PROMPT_TEMPLATE: str
     SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: str
     SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: str
     ENABLE_SEARCH_QUERY: bool
     ENABLE_SEARCH_QUERY: bool
     TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: str
     TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: str
@@ -1425,6 +1430,10 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
     app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = (
     app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = (
         form_data.TITLE_GENERATION_PROMPT_TEMPLATE
         form_data.TITLE_GENERATION_PROMPT_TEMPLATE
     )
     )
+    app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = (
+        form_data.TAGS_GENERATION_PROMPT_TEMPLATE
+    )
+
     app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
     app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
         form_data.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
         form_data.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
     )
     )
@@ -1437,6 +1446,7 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
         "TASK_MODEL": app.state.config.TASK_MODEL,
         "TASK_MODEL": app.state.config.TASK_MODEL,
         "TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
         "TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
         "TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
         "TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
+        "TAGS_GENERATION_PROMPT_TEMPLATE": app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
         "SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
         "SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
         "ENABLE_SEARCH_QUERY": app.state.config.ENABLE_SEARCH_QUERY,
         "ENABLE_SEARCH_QUERY": app.state.config.ENABLE_SEARCH_QUERY,
         "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
         "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
@@ -1521,6 +1531,75 @@ Prompt: {{prompt:middletruncate:8000}}"""
     return await generate_chat_completions(form_data=payload, user=user)
     return await generate_chat_completions(form_data=payload, user=user)
 
 
 
 
+@app.post("/api/task/tags/completions")
+async def generate_chat_tags(form_data: dict, user=Depends(get_verified_user)):
+    print("generate_chat_tags")
+    model_id = form_data["model"]
+    if model_id not in app.state.MODELS:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail="Model not found",
+        )
+
+    # Check if the user has a custom task model
+    # If the user has a custom task model, use that model
+    task_model_id = get_task_model_id(model_id)
+    print(task_model_id)
+
+    if app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != "":
+        template = app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE
+    else:
+        template = """### Task:
+Generate 1-3 broad tags categorizing the main themes of the chat history, along with 1-3 more specific subtopic tags.
+
+### Guidelines:
+- Start with high-level domains (e.g. Science, Technology, Philosophy, Arts, Politics, Business, Health, Sports, Entertainment, Education)
+- Consider including relevant subfields/subdomains if they are strongly represented throughout the conversation
+- If content is too short (less than 3 messages) or too diverse, use only ["General"]
+- Use the chat's primary language; default to English if multilingual
+- Prioritize accuracy over specificity
+
+### Output:
+JSON format: { "tags": ["tag1", "tag2", "tag3"] }
+
+### Chat History:
+<chat_history>
+{{MESSAGES:END:6}}
+</chat_history>"""
+
+    content = tags_generation_template(
+        template, form_data["messages"], {"name": user.name}
+    )
+
+    print("content", content)
+    payload = {
+        "model": task_model_id,
+        "messages": [{"role": "user", "content": content}],
+        "stream": False,
+        "metadata": {"task": str(TASKS.TAGS_GENERATION), "task_body": form_data},
+    }
+    log.debug(payload)
+
+    # Handle pipeline filters
+    try:
+        payload = filter_pipeline(payload, user)
+    except Exception as e:
+        if len(e.args) > 1:
+            return JSONResponse(
+                status_code=e.args[0],
+                content={"detail": e.args[1]},
+            )
+        else:
+            return JSONResponse(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                content={"detail": str(e)},
+            )
+    if "chat_id" in payload:
+        del payload["chat_id"]
+
+    return await generate_chat_completions(form_data=payload, user=user)
+
+
 @app.post("/api/task/query/completions")
 @app.post("/api/task/query/completions")
 async def generate_search_query(form_data: dict, user=Depends(get_verified_user)):
 async def generate_search_query(form_data: dict, user=Depends(get_verified_user)):
     print("generate_search_query")
     print("generate_search_query")

+ 79 - 0
backend/open_webui/migrations/versions/c29facfe716b_update_file_table_path.py

@@ -0,0 +1,79 @@
+"""Update file table path
+
+Revision ID: c29facfe716b
+Revises: c69f45358db4
+Create Date: 2024-10-20 17:02:35.241684
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+import json
+from sqlalchemy.sql import table, column
+from sqlalchemy import String, Text, JSON, and_
+
+
+revision = "c29facfe716b"
+down_revision = "c69f45358db4"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # 1. Add the `path` column to the "file" table.
+    op.add_column("file", sa.Column("path", sa.Text(), nullable=True))
+
+    # 2. Convert the `meta` column from Text/JSONField to `JSON()`
+    # Use Alembic's default batch_op for dialect compatibility.
+    with op.batch_alter_table("file", schema=None) as batch_op:
+        batch_op.alter_column(
+            "meta",
+            type_=sa.JSON(),
+            existing_type=sa.Text(),
+            existing_nullable=True,
+            nullable=True,
+            postgresql_using="meta::json",
+        )
+
+    # 3. Migrate legacy data from `meta` JSONField
+    # Fetch and process `meta` data from the table, add values to the new `path` column as necessary.
+    # We will use SQLAlchemy core bindings to ensure safety across different databases.
+
+    file_table = table(
+        "file", column("id", String), column("meta", JSON), column("path", Text)
+    )
+
+    # Create connection to the database
+    connection = op.get_bind()
+
+    # Get the rows where `meta` has a path and `path` column is null (new column)
+    # Loop through each row in the result set to update the path
+    results = connection.execute(
+        sa.select(file_table.c.id, file_table.c.meta).where(
+            and_(file_table.c.path.is_(None), file_table.c.meta.isnot(None))
+        )
+    ).fetchall()
+
+    # Iterate over each row to extract and update the `path` from `meta` column
+    for row in results:
+        if "path" in row.meta:
+            # Extract the `path` field from the `meta` JSON
+            path = row.meta.get("path")
+
+            # Update the `file` table with the new `path` value
+            connection.execute(
+                file_table.update()
+                .where(file_table.c.id == row.id)
+                .values({"path": path})
+            )
+
+
+def downgrade():
+    # 1. Remove the `path` column
+    op.drop_column("file", "path")
+
+    # 2. Revert the `meta` column back to Text/JSONField
+    with op.batch_alter_table("file", schema=None) as batch_op:
+        batch_op.alter_column(
+            "meta", type_=sa.Text(), existing_type=sa.JSON(), existing_nullable=True
+        )

+ 50 - 0
backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py

@@ -0,0 +1,50 @@
+"""Add folder table
+
+Revision ID: c69f45358db4
+Revises: 3ab32c4b8f59
+Create Date: 2024-10-16 02:02:35.241684
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+revision = "c69f45358db4"
+down_revision = "3ab32c4b8f59"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.create_table(
+        "folder",
+        sa.Column("id", sa.Text(), nullable=False),
+        sa.Column("parent_id", sa.Text(), nullable=True),
+        sa.Column("user_id", sa.Text(), nullable=False),
+        sa.Column("name", sa.Text(), nullable=False),
+        sa.Column("items", sa.JSON(), nullable=True),
+        sa.Column("meta", sa.JSON(), nullable=True),
+        sa.Column("is_expanded", sa.Boolean(), default=False, nullable=False),
+        sa.Column(
+            "created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False
+        ),
+        sa.Column(
+            "updated_at",
+            sa.DateTime(),
+            nullable=False,
+            server_default=sa.func.now(),
+            onupdate=sa.func.now(),
+        ),
+        sa.PrimaryKeyConstraint("id", "user_id"),
+    )
+
+    op.add_column(
+        "chat",
+        sa.Column("folder_id", sa.Text(), nullable=True),
+    )
+
+
+def downgrade():
+    op.drop_column("chat", "folder_id")
+
+    op.drop_table("folder")

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

@@ -123,6 +123,24 @@ def replace_messages_variable(template: str, messages: list[str]) -> str:
     return template
     return template
 
 
 
 
+def tags_generation_template(
+    template: str, messages: list[dict], user: Optional[dict] = None
+) -> str:
+    prompt = get_last_user_message(messages)
+    template = replace_prompt_variable(template, prompt)
+    template = replace_messages_variable(template, messages)
+
+    template = prompt_template(
+        template,
+        **(
+            {"user_name": user.get("name"), "user_location": user.get("location")}
+            if user
+            else {}
+        ),
+    )
+    return template
+
+
 def search_query_generation_template(
 def search_query_generation_template(
     template: str, messages: list[dict], user: Optional[dict] = None
     template: str, messages: list[dict], user: Optional[dict] = None
 ) -> str:
 ) -> str:

+ 1 - 1
backend/requirements.txt

@@ -91,4 +91,4 @@ docker~=7.1.0
 pytest~=8.3.2
 pytest~=8.3.2
 pytest-docker~=3.1.1
 pytest-docker~=3.1.1
 
 
-googleapis-common-protos=1.63.2
+googleapis-common-protos==1.63.2

+ 3 - 3
cypress/e2e/chat.cy.ts

@@ -30,7 +30,7 @@ describe('Settings', () => {
 			// Select the first model
 			// Select the first model
 			cy.get('button[aria-label="model-item"]').first().click();
 			cy.get('button[aria-label="model-item"]').first().click();
 			// Type a message
 			// Type a message
-			cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
+			cy.get('#chat-input').type('Hi, what can you do? A single sentence only please.', {
 				force: true
 				force: true
 			});
 			});
 			// Send the message
 			// Send the message
@@ -50,7 +50,7 @@ describe('Settings', () => {
 			// Select the first model
 			// Select the first model
 			cy.get('button[aria-label="model-item"]').first().click();
 			cy.get('button[aria-label="model-item"]').first().click();
 			// Type a message
 			// Type a message
-			cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
+			cy.get('#chat-input').type('Hi, what can you do? A single sentence only please.', {
 				force: true
 				force: true
 			});
 			});
 			// Send the message
 			// Send the message
@@ -85,7 +85,7 @@ describe('Settings', () => {
 			// Select the first model
 			// Select the first model
 			cy.get('button[aria-label="model-item"]').first().click();
 			cy.get('button[aria-label="model-item"]').first().click();
 			// Type a message
 			// Type a message
-			cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
+			cy.get('#chat-input').type('Hi, what can you do? A single sentence only please.', {
 				force: true
 				force: true
 			});
 			});
 			// Send the message
 			// Send the message

+ 233 - 3
package-lock.json

@@ -35,6 +35,16 @@
 				"mermaid": "^10.9.1",
 				"mermaid": "^10.9.1",
 				"paneforge": "^0.0.6",
 				"paneforge": "^0.0.6",
 				"panzoom": "^9.4.3",
 				"panzoom": "^9.4.3",
+				"prosemirror-commands": "^1.6.0",
+				"prosemirror-example-setup": "^1.2.3",
+				"prosemirror-history": "^1.4.1",
+				"prosemirror-keymap": "^1.2.2",
+				"prosemirror-markdown": "^1.13.1",
+				"prosemirror-model": "^1.23.0",
+				"prosemirror-schema-basic": "^1.2.3",
+				"prosemirror-schema-list": "^1.4.1",
+				"prosemirror-state": "^1.4.3",
+				"prosemirror-view": "^1.34.3",
 				"pyodide": "^0.26.1",
 				"pyodide": "^0.26.1",
 				"socket.io-client": "^4.2.0",
 				"socket.io-client": "^4.2.0",
 				"sortablejs": "^1.15.2",
 				"sortablejs": "^1.15.2",
@@ -1963,6 +1973,20 @@
 			"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
 			"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
 			"dev": true
 			"dev": true
 		},
 		},
+		"node_modules/@types/linkify-it": {
+			"version": "5.0.0",
+			"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+			"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="
+		},
+		"node_modules/@types/markdown-it": {
+			"version": "14.1.2",
+			"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+			"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+			"dependencies": {
+				"@types/linkify-it": "^5",
+				"@types/mdurl": "^2"
+			}
+		},
 		"node_modules/@types/mdast": {
 		"node_modules/@types/mdast": {
 			"version": "3.0.15",
 			"version": "3.0.15",
 			"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
 			"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
@@ -1971,6 +1995,11 @@
 				"@types/unist": "^2"
 				"@types/unist": "^2"
 			}
 			}
 		},
 		},
+		"node_modules/@types/mdurl": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+			"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="
+		},
 		"node_modules/@types/minimatch": {
 		"node_modules/@types/minimatch": {
 			"version": "3.0.5",
 			"version": "3.0.5",
 			"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
 			"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@@ -2552,8 +2581,7 @@
 		"node_modules/argparse": {
 		"node_modules/argparse": {
 			"version": "2.0.1",
 			"version": "2.0.1",
 			"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
 			"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-			"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-			"dev": true
+			"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
 		},
 		},
 		"node_modules/aria-query": {
 		"node_modules/aria-query": {
 			"version": "5.3.0",
 			"version": "5.3.0",
@@ -4460,7 +4488,6 @@
 			"version": "4.5.0",
 			"version": "4.5.0",
 			"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
 			"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
 			"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
 			"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
-			"dev": true,
 			"engines": {
 			"engines": {
 				"node": ">=0.12"
 				"node": ">=0.12"
 			},
 			},
@@ -6233,6 +6260,14 @@
 			"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
 			"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
 			"dev": true
 			"dev": true
 		},
 		},
+		"node_modules/linkify-it": {
+			"version": "5.0.0",
+			"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+			"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+			"dependencies": {
+				"uc.micro": "^2.0.0"
+			}
+		},
 		"node_modules/listr2": {
 		"node_modules/listr2": {
 			"version": "3.14.0",
 			"version": "3.14.0",
 			"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
 			"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
@@ -6470,6 +6505,22 @@
 				"@jridgewell/sourcemap-codec": "^1.5.0"
 				"@jridgewell/sourcemap-codec": "^1.5.0"
 			}
 			}
 		},
 		},
+		"node_modules/markdown-it": {
+			"version": "14.1.0",
+			"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
+			"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+			"dependencies": {
+				"argparse": "^2.0.1",
+				"entities": "^4.4.0",
+				"linkify-it": "^5.0.0",
+				"mdurl": "^2.0.0",
+				"punycode.js": "^2.3.1",
+				"uc.micro": "^2.1.0"
+			},
+			"bin": {
+				"markdown-it": "bin/markdown-it.mjs"
+			}
+		},
 		"node_modules/marked": {
 		"node_modules/marked": {
 			"version": "9.1.6",
 			"version": "9.1.6",
 			"resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz",
 			"resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz",
@@ -6556,6 +6607,11 @@
 			"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
 			"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
 			"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
 			"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
 		},
 		},
+		"node_modules/mdurl": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+			"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
+		},
 		"node_modules/merge-stream": {
 		"node_modules/merge-stream": {
 			"version": "2.0.0",
 			"version": "2.0.0",
 			"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
 			"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -7332,6 +7388,11 @@
 				"node": ">= 0.8.0"
 				"node": ">= 0.8.0"
 			}
 			}
 		},
 		},
+		"node_modules/orderedmap": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
+			"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="
+		},
 		"node_modules/ospath": {
 		"node_modules/ospath": {
 			"version": "1.2.2",
 			"version": "1.2.2",
 			"resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
 			"resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
@@ -7941,6 +8002,157 @@
 				"node": "10.* || >= 12.*"
 				"node": "10.* || >= 12.*"
 			}
 			}
 		},
 		},
+		"node_modules/prosemirror-commands": {
+			"version": "1.6.0",
+			"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz",
+			"integrity": "sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==",
+			"dependencies": {
+				"prosemirror-model": "^1.0.0",
+				"prosemirror-state": "^1.0.0",
+				"prosemirror-transform": "^1.0.0"
+			}
+		},
+		"node_modules/prosemirror-dropcursor": {
+			"version": "1.8.1",
+			"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz",
+			"integrity": "sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==",
+			"dependencies": {
+				"prosemirror-state": "^1.0.0",
+				"prosemirror-transform": "^1.1.0",
+				"prosemirror-view": "^1.1.0"
+			}
+		},
+		"node_modules/prosemirror-example-setup": {
+			"version": "1.2.3",
+			"resolved": "https://registry.npmjs.org/prosemirror-example-setup/-/prosemirror-example-setup-1.2.3.tgz",
+			"integrity": "sha512-+hXZi8+xbFvYM465zZH3rdZ9w7EguVKmUYwYLZjIJIjPK+I0nPTwn8j0ByW2avchVczRwZmOJGNvehblyIerSQ==",
+			"dependencies": {
+				"prosemirror-commands": "^1.0.0",
+				"prosemirror-dropcursor": "^1.0.0",
+				"prosemirror-gapcursor": "^1.0.0",
+				"prosemirror-history": "^1.0.0",
+				"prosemirror-inputrules": "^1.0.0",
+				"prosemirror-keymap": "^1.0.0",
+				"prosemirror-menu": "^1.0.0",
+				"prosemirror-schema-list": "^1.0.0",
+				"prosemirror-state": "^1.0.0"
+			}
+		},
+		"node_modules/prosemirror-gapcursor": {
+			"version": "1.3.2",
+			"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz",
+			"integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==",
+			"dependencies": {
+				"prosemirror-keymap": "^1.0.0",
+				"prosemirror-model": "^1.0.0",
+				"prosemirror-state": "^1.0.0",
+				"prosemirror-view": "^1.0.0"
+			}
+		},
+		"node_modules/prosemirror-history": {
+			"version": "1.4.1",
+			"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz",
+			"integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==",
+			"dependencies": {
+				"prosemirror-state": "^1.2.2",
+				"prosemirror-transform": "^1.0.0",
+				"prosemirror-view": "^1.31.0",
+				"rope-sequence": "^1.3.0"
+			}
+		},
+		"node_modules/prosemirror-inputrules": {
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz",
+			"integrity": "sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==",
+			"dependencies": {
+				"prosemirror-state": "^1.0.0",
+				"prosemirror-transform": "^1.0.0"
+			}
+		},
+		"node_modules/prosemirror-keymap": {
+			"version": "1.2.2",
+			"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz",
+			"integrity": "sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==",
+			"dependencies": {
+				"prosemirror-state": "^1.0.0",
+				"w3c-keyname": "^2.2.0"
+			}
+		},
+		"node_modules/prosemirror-markdown": {
+			"version": "1.13.1",
+			"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.1.tgz",
+			"integrity": "sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==",
+			"dependencies": {
+				"@types/markdown-it": "^14.0.0",
+				"markdown-it": "^14.0.0",
+				"prosemirror-model": "^1.20.0"
+			}
+		},
+		"node_modules/prosemirror-menu": {
+			"version": "1.2.4",
+			"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz",
+			"integrity": "sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==",
+			"dependencies": {
+				"crelt": "^1.0.0",
+				"prosemirror-commands": "^1.0.0",
+				"prosemirror-history": "^1.0.0",
+				"prosemirror-state": "^1.0.0"
+			}
+		},
+		"node_modules/prosemirror-model": {
+			"version": "1.23.0",
+			"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.23.0.tgz",
+			"integrity": "sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==",
+			"dependencies": {
+				"orderedmap": "^2.0.0"
+			}
+		},
+		"node_modules/prosemirror-schema-basic": {
+			"version": "1.2.3",
+			"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.3.tgz",
+			"integrity": "sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==",
+			"dependencies": {
+				"prosemirror-model": "^1.19.0"
+			}
+		},
+		"node_modules/prosemirror-schema-list": {
+			"version": "1.4.1",
+			"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.4.1.tgz",
+			"integrity": "sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==",
+			"dependencies": {
+				"prosemirror-model": "^1.0.0",
+				"prosemirror-state": "^1.0.0",
+				"prosemirror-transform": "^1.7.3"
+			}
+		},
+		"node_modules/prosemirror-state": {
+			"version": "1.4.3",
+			"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz",
+			"integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==",
+			"dependencies": {
+				"prosemirror-model": "^1.0.0",
+				"prosemirror-transform": "^1.0.0",
+				"prosemirror-view": "^1.27.0"
+			}
+		},
+		"node_modules/prosemirror-transform": {
+			"version": "1.10.0",
+			"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.0.tgz",
+			"integrity": "sha512-9UOgFSgN6Gj2ekQH5CTDJ8Rp/fnKR2IkYfGdzzp5zQMFsS4zDllLVx/+jGcX86YlACpG7UR5fwAXiWzxqWtBTg==",
+			"dependencies": {
+				"prosemirror-model": "^1.21.0"
+			}
+		},
+		"node_modules/prosemirror-view": {
+			"version": "1.34.3",
+			"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.34.3.tgz",
+			"integrity": "sha512-mKZ54PrX19sSaQye+sef+YjBbNu2voNwLS1ivb6aD2IRmxRGW64HU9B644+7OfJStGLyxvOreKqEgfvXa91WIA==",
+			"dependencies": {
+				"prosemirror-model": "^1.20.0",
+				"prosemirror-state": "^1.0.0",
+				"prosemirror-transform": "^1.1.0"
+			}
+		},
 		"node_modules/proxy-from-env": {
 		"node_modules/proxy-from-env": {
 			"version": "1.0.0",
 			"version": "1.0.0",
 			"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
 			"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
@@ -7977,6 +8189,14 @@
 				"node": ">=6"
 				"node": ">=6"
 			}
 			}
 		},
 		},
+		"node_modules/punycode.js": {
+			"version": "2.3.1",
+			"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+			"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+			"engines": {
+				"node": ">=6"
+			}
+		},
 		"node_modules/pyodide": {
 		"node_modules/pyodide": {
 			"version": "0.26.1",
 			"version": "0.26.1",
 			"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.26.1.tgz",
 			"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.26.1.tgz",
@@ -8345,6 +8565,11 @@
 				"fsevents": "~2.3.2"
 				"fsevents": "~2.3.2"
 			}
 			}
 		},
 		},
+		"node_modules/rope-sequence": {
+			"version": "1.3.4",
+			"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
+			"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="
+		},
 		"node_modules/rsvp": {
 		"node_modules/rsvp": {
 			"version": "4.8.5",
 			"version": "4.8.5",
 			"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
 			"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
@@ -9562,6 +9787,11 @@
 				"node": ">=14.17"
 				"node": ">=14.17"
 			}
 			}
 		},
 		},
+		"node_modules/uc.micro": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+			"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
+		},
 		"node_modules/ufo": {
 		"node_modules/ufo": {
 			"version": "1.5.3",
 			"version": "1.5.3",
 			"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz",
 			"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz",

+ 10 - 0
package.json

@@ -75,6 +75,16 @@
 		"mermaid": "^10.9.1",
 		"mermaid": "^10.9.1",
 		"paneforge": "^0.0.6",
 		"paneforge": "^0.0.6",
 		"panzoom": "^9.4.3",
 		"panzoom": "^9.4.3",
+		"prosemirror-commands": "^1.6.0",
+		"prosemirror-example-setup": "^1.2.3",
+		"prosemirror-history": "^1.4.1",
+		"prosemirror-keymap": "^1.2.2",
+		"prosemirror-markdown": "^1.13.1",
+		"prosemirror-model": "^1.23.0",
+		"prosemirror-schema-basic": "^1.2.3",
+		"prosemirror-schema-list": "^1.4.1",
+		"prosemirror-state": "^1.4.3",
+		"prosemirror-view": "^1.34.3",
 		"pyodide": "^0.26.1",
 		"pyodide": "^0.26.1",
 		"socket.io-client": "^4.2.0",
 		"socket.io-client": "^4.2.0",
 		"sortablejs": "^1.15.2",
 		"sortablejs": "^1.15.2",

+ 1 - 1
pyproject.toml

@@ -95,7 +95,7 @@ dependencies = [
     "pytest~=8.3.2",
     "pytest~=8.3.2",
     "pytest-docker~=3.1.1",
     "pytest-docker~=3.1.1",
 
 
-    "googleapis-common-protos=1.63.2"
+    "googleapis-common-protos==1.63.2"
 ]
 ]
 readme = "README.md"
 readme = "README.md"
 requires-python = ">= 3.11, < 3.12.0a1"
 requires-python = ">= 3.11, < 3.12.0a1"

+ 28 - 2
src/app.css

@@ -34,6 +34,14 @@ math {
 	@apply rounded-lg;
 	@apply rounded-lg;
 }
 }
 
 
+.input-prose {
+	@apply prose dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
+}
+
+.input-prose-sm {
+	@apply prose dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line text-sm;
+}
+
 .markdown-prose {
 .markdown-prose {
 	@apply prose dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
 	@apply prose dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
 }
 }
@@ -56,7 +64,7 @@ li p {
 
 
 ::-webkit-scrollbar-thumb {
 ::-webkit-scrollbar-thumb {
 	--tw-border-opacity: 1;
 	--tw-border-opacity: 1;
-	background-color: rgba(217, 217, 227, 0.8);
+	background-color: rgba(236, 236, 236, 0.8);
 	border-color: rgba(255, 255, 255, var(--tw-border-opacity));
 	border-color: rgba(255, 255, 255, var(--tw-border-opacity));
 	border-radius: 9999px;
 	border-radius: 9999px;
 	border-width: 1px;
 	border-width: 1px;
@@ -64,7 +72,7 @@ li p {
 
 
 /* Dark theme scrollbar styles */
 /* Dark theme scrollbar styles */
 .dark ::-webkit-scrollbar-thumb {
 .dark ::-webkit-scrollbar-thumb {
-	background-color: rgba(69, 69, 74, 0.8); /* Darker color for dark theme */
+	background-color: rgba(33, 33, 33, 0.8); /* Darker color for dark theme */
 	border-color: rgba(0, 0, 0, var(--tw-border-opacity));
 	border-color: rgba(0, 0, 0, var(--tw-border-opacity));
 }
 }
 
 
@@ -179,3 +187,21 @@ input[type='number'] {
 .bg-gray-950-90 {
 .bg-gray-950-90 {
 	background-color: rgba(var(--color-gray-950, #0d0d0d), 0.9);
 	background-color: rgba(var(--color-gray-950, #0d0d0d), 0.9);
 }
 }
+
+.ProseMirror {
+	@apply h-full  min-h-fit max-h-full;
+}
+
+.ProseMirror:focus {
+	outline: none;
+}
+
+.placeholder::after {
+	content: attr(data-placeholder);
+	cursor: text;
+	pointer-events: none;
+
+	float: left;
+
+	@apply absolute inset-0 z-0 text-gray-500;
+}

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

@@ -32,6 +32,46 @@ export const createNewChat = async (token: string, chat: object) => {
 	return res;
 	return res;
 };
 };
 
 
+export const importChat = async (
+	token: string,
+	chat: object,
+	meta: object | null,
+	pinned?: boolean,
+	folderId?: string | null
+) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/import`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			chat: chat,
+			meta: meta ?? {},
+			pinned: pinned,
+			folder_id: folderId
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			error = err;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
 export const getChatList = async (token: string = '', page: number | null = null) => {
 export const getChatList = async (token: string = '', page: number | null = null) => {
 	let error = null;
 	let error = null;
 	const searchParams = new URLSearchParams();
 	const searchParams = new URLSearchParams();
@@ -205,6 +245,37 @@ export const getChatListBySearchText = async (token: string, text: string, page:
 	}));
 	}));
 };
 };
 
 
+export const getChatsByFolderId = async (token: string, folderId: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/folder/${folderId}`, {
+		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;
+};
+
 export const getAllArchivedChats = async (token: string) => {
 export const getAllArchivedChats = async (token: string) => {
 	let error = null;
 	let error = null;
 
 
@@ -579,6 +650,41 @@ export const shareChatById = async (token: string, id: string) => {
 	return res;
 	return res;
 };
 };
 
 
+export const updateChatFolderIdById = async (token: string, id: string, folderId?: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/folder`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		},
+		body: JSON.stringify({
+			folder_id: folderId
+		})
+	})
+		.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;
+};
+
 export const archiveChatById = async (token: string, id: string) => {
 export const archiveChatById = async (token: string, id: string) => {
 	let error = null;
 	let error = null;
 
 
@@ -764,8 +870,7 @@ export const addTagById = async (token: string, id: string, tagName: string) =>
 			return json;
 			return json;
 		})
 		})
 		.catch((err) => {
 		.catch((err) => {
-			error = err;
-
+			error = err.detail;
 			console.log(err);
 			console.log(err);
 			return null;
 			return null;
 		});
 		});

+ 269 - 0
src/lib/apis/folders/index.ts

@@ -0,0 +1,269 @@
+import { WEBUI_API_BASE_URL } from '$lib/constants';
+
+export const createNewFolder = async (token: string, name: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/folders/`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			name: name
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			error = err.detail;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const getFolders = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/folders/`, {
+		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 getFolderById = async (token: string, id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const updateFolderNameById = async (token: string, id: string, name: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			name: name
+		})
+	})
+		.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 updateFolderIsExpandedById = async (
+	token: string,
+	id: string,
+	isExpanded: boolean
+) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update/expanded`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			is_expanded: isExpanded
+		})
+	})
+		.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 updateFolderParentIdById = async (token: string, id: string, parentId?: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update/parent`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			parent_id: parentId
+		})
+	})
+		.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 FolderItems = {
+	chat_ids: string[];
+	file_ids: string[];
+};
+
+export const updateFolderItemsById = async (token: string, id: string, items: FolderItems) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update/items`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			items: items
+		})
+	})
+		.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 deleteFolderById = async (token: string, id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}`, {
+		method: 'DELETE',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};

+ 72 - 0
src/lib/apis/index.ts

@@ -245,6 +245,78 @@ export const generateTitle = async (
 	return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat';
 	return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat';
 };
 };
 
 
+export const generateTags = async (
+	token: string = '',
+	model: string,
+	messages: string,
+	chat_id?: string
+) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_BASE_URL}/api/task/tags/completions`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			model: model,
+			messages: messages,
+			...(chat_id && { chat_id: chat_id })
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			if ('detail' in err) {
+				error = err.detail;
+			}
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	try {
+		// Step 1: Safely extract the response string
+		const response = res?.choices[0]?.message?.content ?? '';
+
+		// Step 2: Attempt to fix common JSON format issues like single quotes
+		const sanitizedResponse = response.replace(/['‘’`]/g, '"'); // Convert single quotes to double quotes for valid JSON
+
+		// Step 3: Find the relevant JSON block within the response
+		const jsonStartIndex = sanitizedResponse.indexOf('{');
+		const jsonEndIndex = sanitizedResponse.lastIndexOf('}');
+
+		// Step 4: Check if we found a valid JSON block (with both `{` and `}`)
+		if (jsonStartIndex !== -1 && jsonEndIndex !== -1) {
+			const jsonResponse = sanitizedResponse.substring(jsonStartIndex, jsonEndIndex + 1);
+
+			// Step 5: Parse the JSON block
+			const parsed = JSON.parse(jsonResponse);
+
+			// Step 6: If there's a "tags" key, return the tags array; otherwise, return an empty array
+			if (parsed && parsed.tags) {
+				return Array.isArray(parsed.tags) ? parsed.tags : [];
+			} else {
+				return [];
+			}
+		}
+
+		// If no valid JSON block found, return an empty array
+		return [];
+	} catch (e) {
+		// Catch and safely return empty array on any parsing errors
+		console.error('Failed to parse response: ', e);
+		return [];
+	}
+};
+
 export const generateEmoji = async (
 export const generateEmoji = async (
 	token: string = '',
 	token: string = '',
 	model: string,
 	model: string,

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

@@ -28,6 +28,7 @@
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import { text } from '@sveltejs/kit';
 	import { text } from '@sveltejs/kit';
+	import Textarea from '$lib/components/common/Textarea.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -629,11 +630,9 @@
 					content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
 					content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
 					placement="top-start"
 					placement="top-start"
 				>
 				>
-					<textarea
+					<Textarea
 						bind:value={querySettings.template}
 						bind:value={querySettings.template}
 						placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
 						placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
-						class="w-full rounded-lg px-4 py-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
-						rows="4"
 					/>
 					/>
 				</Tooltip>
 				</Tooltip>
 			</div>
 			</div>

+ 18 - 6
src/lib/components/admin/Settings/Interface.svelte

@@ -14,6 +14,7 @@
 
 
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
+	import Textarea from '$lib/components/common/Textarea.svelte';
 
 
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
@@ -23,6 +24,7 @@
 		TASK_MODEL: '',
 		TASK_MODEL: '',
 		TASK_MODEL_EXTERNAL: '',
 		TASK_MODEL_EXTERNAL: '',
 		TITLE_GENERATION_PROMPT_TEMPLATE: '',
 		TITLE_GENERATION_PROMPT_TEMPLATE: '',
+		TAG_GENERATION_PROMPT_TEMPLATE: '',
 		ENABLE_SEARCH_QUERY: true,
 		ENABLE_SEARCH_QUERY: true,
 		SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: ''
 		SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: ''
 	};
 	};
@@ -124,10 +126,22 @@
 					content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
 					content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
 					placement="top-start"
 					placement="top-start"
 				>
 				>
-					<textarea
+					<Textarea
 						bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
 						bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
-						class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
-						rows="3"
+						placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
+					/>
+				</Tooltip>
+			</div>
+
+			<div class="mt-3">
+				<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div>
+
+				<Tooltip
+					content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
+					placement="top-start"
+				>
+					<Textarea
+						bind:value={taskConfig.TAG_GENERATION_PROMPT_TEMPLATE}
 						placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
 						placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
 					/>
 					/>
 				</Tooltip>
 				</Tooltip>
@@ -151,10 +165,8 @@
 						content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
 						content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
 						placement="top-start"
 						placement="top-start"
 					>
 					>
-						<textarea
+						<Textarea
 							bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
 							bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
-							class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
-							rows="3"
 							placeholder={$i18n.t(
 							placeholder={$i18n.t(
 								'Leave empty to use the default prompt, or enter a custom prompt'
 								'Leave empty to use the default prompt, or enter a custom prompt'
 							)}
 							)}

+ 1 - 1
src/lib/components/admin/Settings/Models.svelte

@@ -763,7 +763,7 @@
 											<option value="" disabled selected>{$i18n.t('Select a model')}</option>
 											<option value="" disabled selected>{$i18n.t('Select a model')}</option>
 										{/if}
 										{/if}
 										{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
 										{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
-											<option value={model.name} class="bg-gray-50 dark:bg-gray-700"
+											<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
 												>{model.name +
 												>{model.name +
 													' (' +
 													' (' +
 													(model.ollama.size / 1024 ** 3).toFixed(1) +
 													(model.ollama.size / 1024 ** 3).toFixed(1) +

+ 10 - 8
src/lib/components/admin/Settings/Pipelines.svelte

@@ -546,12 +546,14 @@
 		{/if}
 		{/if}
 	</div>
 	</div>
 
 
-	<div class="flex justify-end pt-3 text-sm font-medium">
-		<button
-			class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
-			type="submit"
-		>
-			{$i18n.t('Save')}
-		</button>
-	</div>
+	{#if PIPELINES_LIST !== null && PIPELINES_LIST.length > 0}
+		<div class="flex justify-end pt-3 text-sm font-medium">
+			<button
+				class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
+				type="submit"
+			>
+				{$i18n.t('Save')}
+			</button>
+		</div>
+	{/if}
 </form>
 </form>

+ 88 - 22
src/lib/components/chat/Chat.svelte

@@ -10,7 +10,7 @@
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
 	import { page } from '$app/stores';
 	import { page } from '$app/stores';
 
 
-	import type { Unsubscriber, Writable } from 'svelte/store';
+	import { get, type Unsubscriber, type Writable } from 'svelte/store';
 	import type { i18n as i18nType } from 'i18next';
 	import type { i18n as i18nType } from 'i18next';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 
 
@@ -20,6 +20,7 @@
 		config,
 		config,
 		type Model,
 		type Model,
 		models,
 		models,
+		tags as allTags,
 		settings,
 		settings,
 		showSidebar,
 		showSidebar,
 		WEBUI_NAME,
 		WEBUI_NAME,
@@ -46,7 +47,9 @@
 
 
 	import { generateChatCompletion } from '$lib/apis/ollama';
 	import { generateChatCompletion } from '$lib/apis/ollama';
 	import {
 	import {
+		addTagById,
 		createNewChat,
 		createNewChat,
+		getAllTags,
 		getChatById,
 		getChatById,
 		getChatList,
 		getChatList,
 		getTagsById,
 		getTagsById,
@@ -62,7 +65,8 @@
 		generateTitle,
 		generateTitle,
 		generateSearchQuery,
 		generateSearchQuery,
 		chatAction,
 		chatAction,
-		generateMoACompletion
+		generateMoACompletion,
+		generateTags
 	} from '$lib/apis';
 	} from '$lib/apis';
 
 
 	import Banner from '../common/Banner.svelte';
 	import Banner from '../common/Banner.svelte';
@@ -85,6 +89,8 @@
 	let processing = '';
 	let processing = '';
 	let messagesContainerElement: HTMLDivElement;
 	let messagesContainerElement: HTMLDivElement;
 
 
+	let navbarElement;
+
 	let showEventConfirmation = false;
 	let showEventConfirmation = false;
 	let eventConfirmationTitle = '';
 	let eventConfirmationTitle = '';
 	let eventConfirmationMessage = '';
 	let eventConfirmationMessage = '';
@@ -125,7 +131,7 @@
 				loaded = true;
 				loaded = true;
 
 
 				window.setTimeout(() => scrollToBottom(), 0);
 				window.setTimeout(() => scrollToBottom(), 0);
-				const chatInput = document.getElementById('chat-textarea');
+				const chatInput = document.getElementById('chat-input');
 				chatInput?.focus();
 				chatInput?.focus();
 			} else {
 			} else {
 				await goto('/');
 				await goto('/');
@@ -264,7 +270,7 @@
 		if (event.data.type === 'input:prompt') {
 		if (event.data.type === 'input:prompt') {
 			console.debug(event.data.text);
 			console.debug(event.data.text);
 
 
-			const inputElement = document.getElementById('chat-textarea');
+			const inputElement = document.getElementById('chat-input');
 
 
 			if (inputElement) {
 			if (inputElement) {
 				prompt = event.data.text;
 				prompt = event.data.text;
@@ -327,7 +333,7 @@
 			}
 			}
 		});
 		});
 
 
-		const chatInput = document.getElementById('chat-textarea');
+		const chatInput = document.getElementById('chat-input');
 		chatInput?.focus();
 		chatInput?.focus();
 
 
 		chats.subscribe(() => {});
 		chats.subscribe(() => {});
@@ -437,7 +443,29 @@
 		if ($page.url.searchParams.get('models')) {
 		if ($page.url.searchParams.get('models')) {
 			selectedModels = $page.url.searchParams.get('models')?.split(',');
 			selectedModels = $page.url.searchParams.get('models')?.split(',');
 		} else if ($page.url.searchParams.get('model')) {
 		} else if ($page.url.searchParams.get('model')) {
-			selectedModels = $page.url.searchParams.get('model')?.split(',');
+			const urlModels = $page.url.searchParams.get('model')?.split(',');
+
+			if (urlModels.length === 1) {
+				const m = $models.find((m) => m.id === urlModels[0]);
+				if (!m) {
+					const modelSelectorButton = document.getElementById('model-selector-0-button');
+					if (modelSelectorButton) {
+						modelSelectorButton.click();
+						await tick();
+
+						const modelSelectorInput = document.getElementById('model-search-input');
+						if (modelSelectorInput) {
+							modelSelectorInput.focus();
+							modelSelectorInput.value = urlModels[0];
+							modelSelectorInput.dispatchEvent(new Event('input'));
+						}
+					}
+				} else {
+					selectedModels = urlModels;
+				}
+			} else {
+				selectedModels = urlModels;
+			}
 		} else if ($settings?.models) {
 		} else if ($settings?.models) {
 			selectedModels = $settings?.models;
 			selectedModels = $settings?.models;
 		} else if ($config?.default_models) {
 		} else if ($config?.default_models) {
@@ -501,7 +529,7 @@
 			settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
 			settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
 		}
 		}
 
 
-		const chatInput = document.getElementById('chat-textarea');
+		const chatInput = document.getElementById('chat-input');
 		setTimeout(() => chatInput?.focus(), 0);
 		setTimeout(() => chatInput?.focus(), 0);
 	};
 	};
 
 
@@ -513,7 +541,10 @@
 		});
 		});
 
 
 		if (chat) {
 		if (chat) {
-			tags = await getTags();
+			tags = await getTagsById(localStorage.token, $chatId).catch(async (error) => {
+				return [];
+			});
+
 			const chatContent = chat.chat;
 			const chatContent = chat.chat;
 
 
 			if (chatContent) {
 			if (chatContent) {
@@ -798,12 +829,14 @@
 				})
 				})
 			);
 			);
 		} else {
 		} else {
+			prompt = '';
+
 			// Reset chat input textarea
 			// Reset chat input textarea
-			const chatTextAreaElement = document.getElementById('chat-textarea');
+			const chatInputContainer = document.getElementById('chat-input-container');
 
 
-			if (chatTextAreaElement) {
-				chatTextAreaElement.value = '';
-				chatTextAreaElement.style.height = '';
+			if (chatInputContainer) {
+				chatInputContainer.value = '';
+				chatInputContainer.style.height = '';
 			}
 			}
 
 
 			const _files = JSON.parse(JSON.stringify(files));
 			const _files = JSON.parse(JSON.stringify(files));
@@ -841,6 +874,11 @@
 
 
 			// Wait until history/message have been updated
 			// Wait until history/message have been updated
 			await tick();
 			await tick();
+
+			// focus on chat input
+			const chatInput = document.getElementById('chat-input');
+			chatInput?.focus();
+
 			_responses = await sendPrompt(userPrompt, userMessageId, { newChat: true });
 			_responses = await sendPrompt(userPrompt, userMessageId, { newChat: true });
 		}
 		}
 
 
@@ -1364,6 +1402,10 @@
 			window.history.replaceState(history.state, '', `/c/${_chatId}`);
 			window.history.replaceState(history.state, '', `/c/${_chatId}`);
 			const title = await generateChatTitle(userPrompt);
 			const title = await generateChatTitle(userPrompt);
 			await setChatTitle(_chatId, title);
 			await setChatTitle(_chatId, title);
+
+			if ($settings?.autoTags ?? true) {
+				await setChatTags(messages);
+			}
 		}
 		}
 
 
 		return _response;
 		return _response;
@@ -1678,6 +1720,10 @@
 			window.history.replaceState(history.state, '', `/c/${_chatId}`);
 			window.history.replaceState(history.state, '', `/c/${_chatId}`);
 			const title = await generateChatTitle(userPrompt);
 			const title = await generateChatTitle(userPrompt);
 			await setChatTitle(_chatId, title);
 			await setChatTitle(_chatId, title);
+
+			if ($settings?.autoTags ?? true) {
+				await setChatTags(messages);
+			}
 		}
 		}
 
 
 		return _response;
 		return _response;
@@ -1864,6 +1910,33 @@
 		}
 		}
 	};
 	};
 
 
+	const setChatTags = async (messages) => {
+		if (!$temporaryChatEnabled) {
+			let generatedTags = await generateTags(
+				localStorage.token,
+				selectedModels[0],
+				messages,
+				$chatId
+			).catch((error) => {
+				console.error(error);
+				return [];
+			});
+
+			const currentTags = await getTagsById(localStorage.token, $chatId);
+			generatedTags = generatedTags.filter(
+				(tag) => !currentTags.find((t) => t.id === tag.replaceAll(' ', '_').toLowerCase())
+			);
+			console.log(generatedTags);
+
+			for (const tag of generatedTags) {
+				await addTagById(localStorage.token, $chatId, tag);
+			}
+
+			chat = await getChatById(localStorage.token, $chatId);
+			allTags.set(await getAllTags(localStorage.token));
+		}
+	};
+
 	const getWebSearchResults = async (
 	const getWebSearchResults = async (
 		model: string,
 		model: string,
 		parentId: string,
 		parentId: string,
@@ -1949,12 +2022,6 @@
 		}
 		}
 	};
 	};
 
 
-	const getTags = async () => {
-		return await getTagsById(localStorage.token, $chatId).catch(async (error) => {
-			return [];
-		});
-	};
-
 	const initChatHandler = async () => {
 	const initChatHandler = async () => {
 		if (!$temporaryChatEnabled) {
 		if (!$temporaryChatEnabled) {
 			chat = await createNewChat(localStorage.token, {
 			chat = await createNewChat(localStorage.token, {
@@ -2046,6 +2113,7 @@
 		{/if}
 		{/if}
 
 
 		<Navbar
 		<Navbar
+			bind:this={navbarElement}
 			chat={{
 			chat={{
 				id: $chatId,
 				id: $chatId,
 				chat: {
 				chat: {
@@ -2182,9 +2250,8 @@
 								}}
 								}}
 								on:submit={async (e) => {
 								on:submit={async (e) => {
 									if (e.detail) {
 									if (e.detail) {
-										prompt = '';
 										await tick();
 										await tick();
-										submitPrompt(e.detail);
+										submitPrompt(e.detail.replaceAll('\n\n', '\n'));
 									}
 									}
 								}}
 								}}
 							/>
 							/>
@@ -2227,9 +2294,8 @@
 								}}
 								}}
 								on:submit={async (e) => {
 								on:submit={async (e) => {
 									if (e.detail) {
 									if (e.detail) {
-										prompt = '';
 										await tick();
 										await tick();
-										submitPrompt(e.detail);
+										submitPrompt(e.detail.replaceAll('\n\n', '\n'));
 									}
 									}
 								}}
 								}}
 							/>
 							/>

+ 164 - 160
src/lib/components/chat/MessageInput.svelte

@@ -29,6 +29,7 @@
 	import FilesOverlay from './MessageInput/FilesOverlay.svelte';
 	import FilesOverlay from './MessageInput/FilesOverlay.svelte';
 	import Commands from './MessageInput/Commands.svelte';
 	import Commands from './MessageInput/Commands.svelte';
 	import XMark from '../icons/XMark.svelte';
 	import XMark from '../icons/XMark.svelte';
+	import RichTextInput from '../common/RichTextInput.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -52,9 +53,10 @@
 
 
 	let recording = false;
 	let recording = false;
 
 
-	let chatTextAreaElement: HTMLTextAreaElement;
-	let filesInputElement;
+	let chatInputContainerElement;
+	let chatInputElement;
 
 
+	let filesInputElement;
 	let commandsElement;
 	let commandsElement;
 
 
 	let inputFiles;
 	let inputFiles;
@@ -69,9 +71,10 @@
 	);
 	);
 
 
 	$: if (prompt) {
 	$: if (prompt) {
-		if (chatTextAreaElement) {
-			chatTextAreaElement.style.height = '';
-			chatTextAreaElement.style.height = Math.min(chatTextAreaElement.scrollHeight, 200) + 'px';
+		if (chatInputContainerElement) {
+			chatInputContainerElement.style.height = '';
+			chatInputContainerElement.style.height =
+				Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
 		}
 		}
 	}
 	}
 
 
@@ -213,7 +216,10 @@
 	};
 	};
 
 
 	onMount(() => {
 	onMount(() => {
-		window.setTimeout(() => chatTextAreaElement?.focus(), 0);
+		window.setTimeout(() => {
+			const chatInput = document.getElementById('chat-input');
+			chatInput?.focus();
+		}, 0);
 
 
 		window.addEventListener('keydown', handleKeyDown);
 		window.addEventListener('keydown', handleKeyDown);
 
 
@@ -316,7 +322,8 @@
 							atSelectedModel = data.data;
 							atSelectedModel = data.data;
 						}
 						}
 
 
-						chatTextAreaElement?.focus();
+						const chatInputElement = document.getElementById('chat-input');
+						chatInputElement?.focus();
 					}}
 					}}
 				/>
 				/>
 			</div>
 			</div>
@@ -351,7 +358,7 @@
 							recording = false;
 							recording = false;
 
 
 							await tick();
 							await tick();
-							document.getElementById('chat-textarea')?.focus();
+							document.getElementById('chat-input')?.focus();
 						}}
 						}}
 						on:confirm={async (e) => {
 						on:confirm={async (e) => {
 							const response = e.detail;
 							const response = e.detail;
@@ -360,7 +367,7 @@
 							recording = false;
 							recording = false;
 
 
 							await tick();
 							await tick();
-							document.getElementById('chat-textarea')?.focus();
+							document.getElementById('chat-input')?.focus();
 
 
 							if ($settings?.speechAutoSend ?? false) {
 							if ($settings?.speechAutoSend ?? false) {
 								dispatch('submit', prompt);
 								dispatch('submit', prompt);
@@ -478,7 +485,9 @@
 										}}
 										}}
 										onClose={async () => {
 										onClose={async () => {
 											await tick();
 											await tick();
-											chatTextAreaElement?.focus();
+
+											const chatInput = document.getElementById('chat-input');
+											chatInput?.focus();
 										}}
 										}}
 									>
 									>
 										<button
 										<button
@@ -500,177 +509,172 @@
 									</InputMenu>
 									</InputMenu>
 								</div>
 								</div>
 
 
-								<textarea
-									id="chat-textarea"
-									bind:this={chatTextAreaElement}
-									class="scrollbar-hidden bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-3 px-1 rounded-xl resize-none h-[48px]"
-									placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
-									bind:value={prompt}
-									on:keypress={(e) => {
-										if (
-											!$mobile ||
+								<div
+									bind:this={chatInputContainerElement}
+									id="chat-input-container"
+									class="scrollbar-hidden text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-[48px] overflow-auto"
+								>
+									<RichTextInput
+										bind:this={chatInputElement}
+										id="chat-input"
+										placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
+										bind:value={prompt}
+										shiftEnter={!$mobile ||
 											!(
 											!(
 												'ontouchstart' in window ||
 												'ontouchstart' in window ||
 												navigator.maxTouchPoints > 0 ||
 												navigator.maxTouchPoints > 0 ||
 												navigator.msMaxTouchPoints > 0
 												navigator.msMaxTouchPoints > 0
-											)
-										) {
-											// Prevent Enter key from creating a new line
-											if (e.key === 'Enter' && !e.shiftKey) {
+											)}
+										on:enter={async (e) => {
+											if (prompt !== '') {
+												dispatch('submit', prompt);
+											}
+										}}
+										on:input={async (e) => {
+											if (chatInputContainerElement) {
+												chatInputContainerElement.style.height = '';
+												chatInputContainerElement.style.height =
+													Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
+											}
+										}}
+										on:focus={async (e) => {
+											if (chatInputContainerElement) {
+												chatInputContainerElement.style.height = '';
+												chatInputContainerElement.style.height =
+													Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
+											}
+										}}
+										on:keypress={(e) => {
+											e = e.detail.event;
+										}}
+										on:keydown={async (e) => {
+											e = e.detail.event;
+
+											if (chatInputContainerElement) {
+												chatInputContainerElement.style.height = '';
+												chatInputContainerElement.style.height =
+													Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
+											}
+
+											const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
+											const commandsContainerElement =
+												document.getElementById('commands-container');
+
+											// Command/Ctrl + Shift + Enter to submit a message pair
+											if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
 												e.preventDefault();
 												e.preventDefault();
+												createMessagePair(prompt);
 											}
 											}
 
 
-											// Submit the prompt when Enter key is pressed
-											if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) {
-												dispatch('submit', prompt);
+											// Check if Ctrl + R is pressed
+											if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
+												e.preventDefault();
+												console.log('regenerate');
+
+												const regenerateButton = [
+													...document.getElementsByClassName('regenerate-response-button')
+												]?.at(-1);
+
+												regenerateButton?.click();
 											}
 											}
-										}
-									}}
-									on:keydown={async (e) => {
-										const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
-										const commandsContainerElement = document.getElementById('commands-container');
-
-										// Command/Ctrl + Shift + Enter to submit a message pair
-										if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
-											e.preventDefault();
-											createMessagePair(prompt);
-										}
-
-										// Check if Ctrl + R is pressed
-										if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
-											e.preventDefault();
-											console.log('regenerate');
-
-											const regenerateButton = [
-												...document.getElementsByClassName('regenerate-response-button')
-											]?.at(-1);
-
-											regenerateButton?.click();
-										}
-
-										if (prompt === '' && e.key == 'ArrowUp') {
-											e.preventDefault();
-
-											const userMessageElement = [
-												...document.getElementsByClassName('user-message')
-											]?.at(-1);
-
-											const editButton = [
-												...document.getElementsByClassName('edit-user-message-button')
-											]?.at(-1);
-
-											console.log(userMessageElement);
-
-											userMessageElement.scrollIntoView({ block: 'center' });
-											editButton?.click();
-										}
-
-										if (commandsContainerElement && e.key === 'ArrowUp') {
-											e.preventDefault();
-											commandsElement.selectUp();
-
-											const commandOptionButton = [
-												...document.getElementsByClassName('selected-command-option-button')
-											]?.at(-1);
-											commandOptionButton.scrollIntoView({ block: 'center' });
-										}
-
-										if (commandsContainerElement && e.key === 'ArrowDown') {
-											e.preventDefault();
-											commandsElement.selectDown();
-
-											const commandOptionButton = [
-												...document.getElementsByClassName('selected-command-option-button')
-											]?.at(-1);
-											commandOptionButton.scrollIntoView({ block: 'center' });
-										}
-
-										if (commandsContainerElement && e.key === 'Enter') {
-											e.preventDefault();
-
-											const commandOptionButton = [
-												...document.getElementsByClassName('selected-command-option-button')
-											]?.at(-1);
-
-											if (e.shiftKey) {
-												prompt = `${prompt}\n`;
-											} else if (commandOptionButton) {
-												commandOptionButton?.click();
-											} else {
-												document.getElementById('send-message-button')?.click();
+
+											if (prompt === '' && e.key == 'ArrowUp') {
+												e.preventDefault();
+
+												const userMessageElement = [
+													...document.getElementsByClassName('user-message')
+												]?.at(-1);
+
+												const editButton = [
+													...document.getElementsByClassName('edit-user-message-button')
+												]?.at(-1);
+
+												console.log(userMessageElement);
+
+												userMessageElement.scrollIntoView({ block: 'center' });
+												editButton?.click();
 											}
 											}
-										}
 
 
-										if (commandsContainerElement && e.key === 'Tab') {
-											e.preventDefault();
+											if (commandsContainerElement && e.key === 'ArrowUp') {
+												e.preventDefault();
+												commandsElement.selectUp();
 
 
-											const commandOptionButton = [
-												...document.getElementsByClassName('selected-command-option-button')
-											]?.at(-1);
+												const commandOptionButton = [
+													...document.getElementsByClassName('selected-command-option-button')
+												]?.at(-1);
+												commandOptionButton.scrollIntoView({ block: 'center' });
+											}
 
 
-											commandOptionButton?.click();
-										} else if (e.key === 'Tab') {
-											const words = findWordIndices(prompt);
+											if (commandsContainerElement && e.key === 'ArrowDown') {
+												e.preventDefault();
+												commandsElement.selectDown();
 
 
-											if (words.length > 0) {
-												const word = words.at(0);
-												const fullPrompt = prompt;
+												const commandOptionButton = [
+													...document.getElementsByClassName('selected-command-option-button')
+												]?.at(-1);
+												commandOptionButton.scrollIntoView({ block: 'center' });
+											}
 
 
-												prompt = prompt.substring(0, word?.endIndex + 1);
-												await tick();
+											if (commandsContainerElement && e.key === 'Enter') {
+												e.preventDefault();
 
 
-												e.target.scrollTop = e.target.scrollHeight;
-												prompt = fullPrompt;
-												await tick();
+												const commandOptionButton = [
+													...document.getElementsByClassName('selected-command-option-button')
+												]?.at(-1);
 
 
+												if (e.shiftKey) {
+													prompt = `${prompt}\n`;
+												} else if (commandOptionButton) {
+													commandOptionButton?.click();
+												} else {
+													document.getElementById('send-message-button')?.click();
+												}
+											}
+
+											if (commandsContainerElement && e.key === 'Tab') {
 												e.preventDefault();
 												e.preventDefault();
-												e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
+
+												const commandOptionButton = [
+													...document.getElementsByClassName('selected-command-option-button')
+												]?.at(-1);
+
+												commandOptionButton?.click();
 											}
 											}
 
 
-											e.target.style.height = '';
-											e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
-										}
-
-										if (e.key === 'Escape') {
-											console.log('Escape');
-											atSelectedModel = undefined;
-										}
-									}}
-									rows="1"
-									on:input={async (e) => {
-										e.target.style.height = '';
-										e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
-										user = null;
-									}}
-									on:focus={async (e) => {
-										e.target.style.height = '';
-										e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
-									}}
-									on:paste={async (e) => {
-										const clipboardData = e.clipboardData || window.clipboardData;
-
-										if (clipboardData && clipboardData.items) {
-											for (const item of clipboardData.items) {
-												if (item.type.indexOf('image') !== -1) {
-													const blob = item.getAsFile();
-													const reader = new FileReader();
-
-													reader.onload = function (e) {
-														files = [
-															...files,
-															{
-																type: 'image',
-																url: `${e.target.result}`
-															}
-														];
-													};
-
-													reader.readAsDataURL(blob);
+											if (e.key === 'Escape') {
+												console.log('Escape');
+												atSelectedModel = undefined;
+											}
+										}}
+										on:paste={async (e) => {
+											e = e.detail.event;
+											console.log(e);
+
+											const clipboardData = e.clipboardData || window.clipboardData;
+
+											if (clipboardData && clipboardData.items) {
+												for (const item of clipboardData.items) {
+													if (item.type.indexOf('image') !== -1) {
+														const blob = item.getAsFile();
+														const reader = new FileReader();
+
+														reader.onload = function (e) {
+															files = [
+																...files,
+																{
+																	type: 'image',
+																	url: `${e.target.result}`
+																}
+															];
+														};
+
+														reader.readAsDataURL(blob);
+													}
 												}
 												}
 											}
 											}
-										}
-									}}
-								/>
+										}}
+									/>
+								</div>
 
 
 								<div class="self-end mb-2 flex space-x-1 mr-1">
 								<div class="self-end mb-2 flex space-x-1 mr-1">
 									{#if !history?.currentId || history.messages[history.currentId]?.done == true}
 									{#if !history?.currentId || history.messages[history.currentId]?.done == true}

+ 4 - 4
src/lib/components/chat/MessageInput/Commands.svelte

@@ -25,17 +25,17 @@
 	};
 	};
 
 
 	let command = '';
 	let command = '';
-	$: command = (prompt?.trim() ?? '').split(' ')?.at(-1) ?? '';
+	$: command = prompt?.split('\n').pop()?.split(' ')?.pop() ?? '';
 </script>
 </script>
 
 
-{#if ['/', '#', '@'].includes(command?.charAt(0))}
+{#if ['/', '#', '@'].includes(command?.charAt(0)) || '\\#' === command.slice(0, 2)}
 	{#if command?.charAt(0) === '/'}
 	{#if command?.charAt(0) === '/'}
 		<Prompts bind:this={commandElement} bind:prompt bind:files {command} />
 		<Prompts bind:this={commandElement} bind:prompt bind:files {command} />
-	{:else if command?.charAt(0) === '#'}
+	{:else if (command?.charAt(0) === '#' && command.startsWith('#') && !command.includes('# ')) || ('\\#' === command.slice(0, 2) && command.startsWith('#') && !command.includes('# '))}
 		<Knowledge
 		<Knowledge
 			bind:this={commandElement}
 			bind:this={commandElement}
 			bind:prompt
 			bind:prompt
-			{command}
+			command={command.includes('\\#') ? command.slice(2) : command}
 			on:youtube={(e) => {
 			on:youtube={(e) => {
 				console.log(e);
 				console.log(e);
 				dispatch('upload', {
 				dispatch('upload', {

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

@@ -46,7 +46,7 @@
 		dispatch('select', item);
 		dispatch('select', item);
 
 
 		prompt = removeLastWordFromString(prompt, command);
 		prompt = removeLastWordFromString(prompt, command);
-		const chatInputElement = document.getElementById('chat-textarea');
+		const chatInputElement = document.getElementById('chat-input');
 
 
 		await tick();
 		await tick();
 		chatInputElement?.focus();
 		chatInputElement?.focus();
@@ -57,7 +57,7 @@
 		dispatch('url', url);
 		dispatch('url', url);
 
 
 		prompt = removeLastWordFromString(prompt, command);
 		prompt = removeLastWordFromString(prompt, command);
-		const chatInputElement = document.getElementById('chat-textarea');
+		const chatInputElement = document.getElementById('chat-input');
 
 
 		await tick();
 		await tick();
 		chatInputElement?.focus();
 		chatInputElement?.focus();
@@ -68,7 +68,7 @@
 		dispatch('youtube', url);
 		dispatch('youtube', url);
 
 
 		prompt = removeLastWordFromString(prompt, command);
 		prompt = removeLastWordFromString(prompt, command);
-		const chatInputElement = document.getElementById('chat-textarea');
+		const chatInputElement = document.getElementById('chat-input');
 
 
 		await tick();
 		await tick();
 		chatInputElement?.focus();
 		chatInputElement?.focus();

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

@@ -58,7 +58,7 @@
 
 
 	onMount(async () => {
 	onMount(async () => {
 		await tick();
 		await tick();
-		const chatInputElement = document.getElementById('chat-textarea');
+		const chatInputElement = document.getElementById('chat-input');
 		await tick();
 		await tick();
 		chatInputElement?.focus();
 		chatInputElement?.focus();
 		await tick();
 		await tick();

+ 7 - 11
src/lib/components/chat/MessageInput/Commands/Prompts.svelte

@@ -110,21 +110,17 @@
 
 
 		prompt = text;
 		prompt = text;
 
 
-		const chatInputElement = document.getElementById('chat-textarea');
+		const chatInputContainerElement = document.getElementById('chat-input-container');
+		const chatInputElement = document.getElementById('chat-input');
 
 
 		await tick();
 		await tick();
 
 
-		chatInputElement.style.height = '';
-		chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
+		if (chatInputContainerElement) {
+			chatInputContainerElement.style.height = '';
+			chatInputContainerElement.style.height =
+				Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
 
 
-		chatInputElement?.focus();
-
-		await tick();
-
-		const words = findWordIndices(prompt);
-		if (words.length > 0) {
-			const word = words.at(0);
-			chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
+			chatInputElement?.focus();
 		}
 		}
 	};
 	};
 </script>
 </script>

+ 8 - 3
src/lib/components/chat/MessageInput/InputMenu.svelte

@@ -61,9 +61,14 @@
 						<div
 						<div
 							class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
 							class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
 						>
 						>
-							<div class="flex-1 flex items-center gap-2">
-								<WrenchSolid />
-								<Tooltip content={tools[toolId]?.description ?? ''} className="flex-1">
+							<div class="flex-1">
+								<Tooltip
+									content={tools[toolId]?.description ?? ''}
+									placement="top-start"
+									className="flex flex-1  gap-2 items-center"
+								>
+									<WrenchSolid />
+
 									<div class=" line-clamp-1">{tools[toolId].name}</div>
 									<div class=" line-clamp-1">{tools[toolId].name}</div>
 								</Tooltip>
 								</Tooltip>
 							</div>
 							</div>

+ 3 - 2
src/lib/components/chat/MessageInput/VoiceRecording.svelte

@@ -11,6 +11,7 @@
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
 	export let recording = false;
 	export let recording = false;
+	export let className = ' p-2.5 w-full max-w-full';
 
 
 	let loading = false;
 	let loading = false;
 	let confirmed = false;
 	let confirmed = false;
@@ -213,7 +214,7 @@
 					transcription = `${transcription}${transcript}`;
 					transcription = `${transcription}${transcript}`;
 
 
 					await tick();
 					await tick();
-					document.getElementById('chat-textarea')?.focus();
+					document.getElementById('chat-input')?.focus();
 
 
 					// Restart the inactivity timeout
 					// Restart the inactivity timeout
 					timeoutId = setTimeout(() => {
 					timeoutId = setTimeout(() => {
@@ -282,7 +283,7 @@
 <div
 <div
 	class="{loading
 	class="{loading
 		? ' bg-gray-100/50 dark:bg-gray-850/50'
 		? ' bg-gray-100/50 dark:bg-gray-850/50'
-		: 'bg-indigo-300/10 dark:bg-indigo-500/10 '} rounded-full flex p-2.5"
+		: 'bg-indigo-300/10 dark:bg-indigo-500/10 '} rounded-full flex {className}"
 >
 >
 	<div class="flex items-center mr-1">
 	<div class="flex items-center mr-1">
 		<button
 		<button

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

@@ -330,20 +330,14 @@
 
 
 				await tick();
 				await tick();
 
 
-				const chatInputElement = document.getElementById('chat-textarea');
-				if (chatInputElement) {
+				const chatInputContainerElement = document.getElementById('chat-input-container');
+				if (chatInputContainerElement) {
 					prompt = p;
 					prompt = p;
 
 
-					chatInputElement.style.height = '';
-					chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
-					chatInputElement.focus();
-
-					const words = findWordIndices(prompt);
-
-					if (words.length > 0) {
-						const word = words.at(0);
-						chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
-					}
+					chatInputContainerElement.style.height = '';
+					chatInputContainerElement.style.height =
+						Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
+					chatInputContainerElement.focus();
 				}
 				}
 
 
 				await tick();
 				await tick();

+ 201 - 49
src/lib/components/chat/Messages/Citations.svelte

@@ -1,67 +1,219 @@
 <script lang="ts">
 <script lang="ts">
+	import { getContext } from 'svelte';
 	import CitationsModal from './CitationsModal.svelte';
 	import CitationsModal from './CitationsModal.svelte';
+	import Collapsible from '$lib/components/common/Collapsible.svelte';
+	import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
+	import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
+
+	const i18n = getContext('i18n');
 
 
 	export let citations = [];
 	export let citations = [];
 
 
 	let _citations = [];
 	let _citations = [];
-
-	$: _citations = citations.reduce((acc, citation) => {
-		citation.document.forEach((document, index) => {
-			const metadata = citation.metadata?.[index];
-			const id = metadata?.source ?? 'N/A';
-			let source = citation?.source;
-
-			if (metadata?.name) {
-				source = { ...source, name: metadata.name };
-			}
-
-			// Check if ID looks like a URL
-			if (id.startsWith('http://') || id.startsWith('https://')) {
-				source = { name: id };
-			}
-
-			const existingSource = acc.find((item) => item.id === id);
-
-			if (existingSource) {
-				existingSource.document.push(document);
-				existingSource.metadata.push(metadata);
-			} else {
-				acc.push({
-					id: id,
-					source: source,
-					document: [document],
-					metadata: metadata ? [metadata] : []
-				});
-			}
-		});
-		return acc;
-	}, []);
+	let showPercentage = false;
+	let showRelevance = true;
 
 
 	let showCitationModal = false;
 	let showCitationModal = false;
-	let selectedCitation = null;
+	let selectedCitation: any = null;
+	let isCollapsibleOpen = false;
+
+	function calculateShowRelevance(citations: any[]) {
+		const distances = citations.flatMap((citation) => citation.distances ?? []);
+		const inRange = distances.filter((d) => d !== undefined && d >= -1 && d <= 1).length;
+		const outOfRange = distances.filter((d) => d !== undefined && (d < -1 || d > 1)).length;
+
+		if (distances.length === 0) {
+			return false;
+		}
+
+		if (
+			(inRange === distances.length - 1 && outOfRange === 1) ||
+			(outOfRange === distances.length - 1 && inRange === 1)
+		) {
+			return false;
+		}
+
+		return true;
+	}
+
+	function shouldShowPercentage(citations: any[]) {
+		const distances = citations.flatMap((citation) => citation.distances ?? []);
+		return distances.every((d) => d !== undefined && d >= -1 && d <= 1);
+	}
+
+	$: {
+		_citations = citations.reduce((acc, citation) => {
+			citation.document.forEach((document, index) => {
+				const metadata = citation.metadata?.[index];
+				const distance = citation.distances?.[index];
+				const id = metadata?.source ?? 'N/A';
+				let source = citation?.source;
+
+				if (metadata?.name) {
+					source = { ...source, name: metadata.name };
+				}
+
+				if (id.startsWith('http://') || id.startsWith('https://')) {
+					source = { name: id };
+				}
+
+				const existingSource = acc.find((item) => item.id === id);
+
+				if (existingSource) {
+					existingSource.document.push(document);
+					existingSource.metadata.push(metadata);
+					if (distance !== undefined) existingSource.distances.push(distance);
+				} else {
+					acc.push({
+						id: id,
+						source: source,
+						document: [document],
+						metadata: metadata ? [metadata] : [],
+						distances: distance !== undefined ? [distance] : undefined
+					});
+				}
+			});
+			return acc;
+		}, []);
+
+		showRelevance = calculateShowRelevance(_citations);
+		showPercentage = shouldShowPercentage(_citations);
+	}
 </script>
 </script>
 
 
-<CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
+<CitationsModal
+	bind:show={showCitationModal}
+	citation={selectedCitation}
+	{showPercentage}
+	{showRelevance}
+/>
 
 
 {#if _citations.length > 0}
 {#if _citations.length > 0}
 	<div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
 	<div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
-		{#each _citations as citation, idx}
-			<div class="flex gap-1 text-xs font-semibold">
-				<button
-					class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96"
-					on:click={() => {
-						showCitationModal = true;
-						selectedCitation = citation;
-					}}
+		{#if _citations.length <= 3}
+			{#each _citations as citation, idx}
+				<div class="flex gap-1 text-xs font-semibold">
+					<button
+						class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96"
+						on:click={() => {
+							showCitationModal = true;
+							selectedCitation = citation;
+						}}
+					>
+						{#if _citations.every((c) => c.distances !== undefined)}
+							<div class="bg-white dark:bg-gray-700 rounded-full size-4">
+								{idx + 1}
+							</div>
+						{/if}
+						<div class="flex-1 mx-2 line-clamp-1 truncate">
+							{citation.source.name}
+						</div>
+					</button>
+				</div>
+			{/each}
+		{:else}
+			<Collapsible bind:open={isCollapsibleOpen} className="w-full">
+				<div
+					class="flex items-center gap-1 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
 				>
 				>
-					<div class="bg-white dark:bg-gray-700 rounded-full size-4">
-						{idx + 1}
+					<div class="flex-grow flex items-center gap-1 overflow-hidden">
+						<span class="whitespace-nowrap hidden sm:inline">{$i18n.t('References from')}</span>
+						<div class="flex items-center">
+							{#if _citations.length > 1 && _citations
+									.slice(0, 2)
+									.reduce((acc, citation) => acc + citation.source.name.length, 0) <= 50}
+								{#each _citations.slice(0, 2) as citation, idx}
+									<div class="flex items-center">
+										<button
+											class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96 text-xs font-semibold"
+											on:click={() => {
+												showCitationModal = true;
+												selectedCitation = citation;
+											}}
+										>
+											{#if _citations.every((c) => c.distances !== undefined)}
+												<div class="bg-white dark:bg-gray-700 rounded-full size-4">
+													{idx + 1}
+												</div>
+											{/if}
+											<div class="flex-1 mx-2 line-clamp-1">
+												{citation.source.name}
+											</div>
+										</button>
+										{#if idx === 0}<span class="mr-1">,</span>
+										{/if}
+									</div>
+								{/each}
+							{:else}
+								{#each _citations.slice(0, 1) as citation, idx}
+									<div class="flex items-center">
+										<button
+											class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96 text-xs font-semibold"
+											on:click={() => {
+												showCitationModal = true;
+												selectedCitation = citation;
+											}}
+										>
+											{#if _citations.every((c) => c.distances !== undefined)}
+												<div class="bg-white dark:bg-gray-700 rounded-full size-4">
+													{idx + 1}
+												</div>
+											{/if}
+											<div class="flex-1 mx-2 line-clamp-1">
+												{citation.source.name}
+											</div>
+										</button>
+									</div>
+								{/each}
+							{/if}
+						</div>
+						<div class="flex items-center gap-1 whitespace-nowrap">
+							<span class="hidden sm:inline">{$i18n.t('and')}</span>
+							<span class="text-gray-600 dark:text-gray-400">
+								{_citations.length -
+									(_citations.length > 1 &&
+									_citations
+										.slice(0, 2)
+										.reduce((acc, citation) => acc + citation.source.name.length, 0) <= 50
+										? 2
+										: 1)}
+							</span>
+							<span>{$i18n.t('more')}</span>
+						</div>
+					</div>
+					<div class="flex-shrink-0">
+						{#if isCollapsibleOpen}
+							<ChevronUp strokeWidth="3.5" className="size-3.5" />
+						{:else}
+							<ChevronDown strokeWidth="3.5" className="size-3.5" />
+						{/if}
 					</div>
 					</div>
-					<div class="flex-1 mx-2 line-clamp-1">
-						{citation.source.name}
+				</div>
+				<div slot="content" class="mt-2">
+					<div class="flex flex-wrap gap-2">
+						{#each _citations as citation, idx}
+							<div class="flex gap-1 text-xs font-semibold">
+								<button
+									class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96"
+									on:click={() => {
+										showCitationModal = true;
+										selectedCitation = citation;
+									}}
+								>
+									{#if _citations.every((c) => c.distances !== undefined)}
+										<div class="bg-white dark:bg-gray-700 rounded-full size-4">
+											{idx + 1}
+										</div>
+									{/if}
+									<div class="flex-1 mx-2 line-clamp-1">
+										{citation.source.name}
+									</div>
+								</button>
+							</div>
+						{/each}
 					</div>
 					</div>
-				</button>
-			</div>
-		{/each}
+				</div>
+			</Collapsible>
+		{/if}
 	</div>
 	</div>
 {/if}
 {/if}

+ 69 - 9
src/lib/components/chat/Messages/CitationsModal.svelte

@@ -2,21 +2,44 @@
 	import { getContext, onMount, tick } from 'svelte';
 	import { getContext, onMount, tick } from 'svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
+
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
 	export let show = false;
 	export let show = false;
 	export let citation;
 	export let citation;
+	export let showPercentage = false;
+	export let showRelevance = true;
 
 
 	let mergedDocuments = [];
 	let mergedDocuments = [];
 
 
+	function calculatePercentage(distance: number) {
+		if (distance < 0) return 100;
+		if (distance > 1) return 0;
+		return Math.round((1 - distance) * 10000) / 100;
+	}
+
+	function getRelevanceColor(percentage: number) {
+		if (percentage >= 80)
+			return 'bg-green-200 dark:bg-green-800 text-green-800 dark:text-green-200';
+		if (percentage >= 60)
+			return 'bg-yellow-200 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200';
+		if (percentage >= 40)
+			return 'bg-orange-200 dark:bg-orange-800 text-orange-800 dark:text-orange-200';
+		return 'bg-red-200 dark:bg-red-800 text-red-800 dark:text-red-200';
+	}
+
 	$: if (citation) {
 	$: if (citation) {
 		mergedDocuments = citation.document?.map((c, i) => {
 		mergedDocuments = citation.document?.map((c, i) => {
 			return {
 			return {
 				source: citation.source,
 				source: citation.source,
 				document: c,
 				document: c,
-				metadata: citation.metadata?.[i]
+				metadata: citation.metadata?.[i],
+				distance: citation.distances?.[i]
 			};
 			};
 		});
 		});
+		if (mergedDocuments.every((doc) => doc.distance !== undefined)) {
+			mergedDocuments.sort((a, b) => (a.distance ?? Infinity) - (b.distance ?? Infinity));
+		}
 	}
 	}
 </script>
 </script>
 
 
@@ -57,13 +80,14 @@
 
 
 						{#if document.source?.name}
 						{#if document.source?.name}
 							<Tooltip
 							<Tooltip
+								className="w-fit"
 								content={$i18n.t('Open file')}
 								content={$i18n.t('Open file')}
-								placement="left"
-								tippyOptions={{ duration: [500, 0], animation: 'perspective' }}
+								placement="top-start"
+								tippyOptions={{ duration: [500, 0] }}
 							>
 							>
-								<div class="text-sm dark:text-gray-400">
+								<div class="text-sm dark:text-gray-400 flex items-center gap-2 w-fit">
 									<a
 									<a
-										class="hover:text-gray-500 hover:dark:text-gray-100 underline"
+										class="hover:text-gray-500 hover:dark:text-gray-100 underline flex-grow"
 										href={document?.metadata?.file_id
 										href={document?.metadata?.file_id
 											? `/api/v1/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
 											? `/api/v1/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
 											: document.source.name.includes('http')
 											: document.source.name.includes('http')
@@ -73,11 +97,47 @@
 									>
 									>
 										{document?.metadata?.name ?? document.source.name}
 										{document?.metadata?.name ?? document.source.name}
 									</a>
 									</a>
-									{document?.metadata?.page
-										? `(${$i18n.t('page')} ${document.metadata.page + 1})`
-										: ''}
+									{#if document?.metadata?.page}
+										<span class="text-xs text-gray-500 dark:text-gray-400">
+											({$i18n.t('page')}
+											{document.metadata.page + 1})
+										</span>
+									{/if}
 								</div>
 								</div>
 							</Tooltip>
 							</Tooltip>
+							{#if showRelevance}
+								<div class="text-sm font-medium dark:text-gray-300 mt-2">
+									{$i18n.t('Relevance')}
+								</div>
+								{#if document.distance !== undefined}
+									<Tooltip
+										className="w-fit"
+										content={$i18n.t('Semantic distance to query')}
+										placement="top-start"
+										tippyOptions={{ duration: [500, 0] }}
+									>
+										<div class="text-sm my-1 dark:text-gray-400 flex items-center gap-2 w-fit">
+											{#if showPercentage}
+												{@const percentage = calculatePercentage(document.distance)}
+												<span class={`px-1 rounded font-medium ${getRelevanceColor(percentage)}`}>
+													{percentage.toFixed(2)}%
+												</span>
+												<span class="text-gray-500 dark:text-gray-500">
+													({document.distance.toFixed(4)})
+												</span>
+											{:else}
+												<span class="text-gray-500 dark:text-gray-500">
+													{document.distance.toFixed(4)}
+												</span>
+											{/if}
+										</div>
+									</Tooltip>
+								{:else}
+									<div class="text-sm dark:text-gray-400">
+										{$i18n.t('No distance available')}
+									</div>
+								{/if}
+							{/if}
 						{:else}
 						{:else}
 							<div class="text-sm dark:text-gray-400">
 							<div class="text-sm dark:text-gray-400">
 								{$i18n.t('No source available')}
 								{$i18n.t('No source available')}
@@ -85,7 +145,7 @@
 						{/if}
 						{/if}
 					</div>
 					</div>
 					<div class="flex flex-col w-full">
 					<div class="flex flex-col w-full">
-						<div class=" text-sm font-medium dark:text-gray-300">
+						<div class=" text-sm font-medium dark:text-gray-300 mt-2">
 							{$i18n.t('Content')}
 							{$i18n.t('Content')}
 						</div>
 						</div>
 						<pre class="text-sm dark:text-gray-400 whitespace-pre-line">
 						<pre class="text-sm dark:text-gray-400 whitespace-pre-line">

+ 17 - 4
src/lib/components/chat/Messages/RateComment.svelte

@@ -2,6 +2,7 @@
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 
 
 	import { createEventDispatcher, onMount, getContext } from 'svelte';
 	import { createEventDispatcher, onMount, getContext } from 'svelte';
+	import { config } from '$lib/stores';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -69,7 +70,7 @@
 </script>
 </script>
 
 
 <div
 <div
-	class=" my-2.5 rounded-xl px-4 py-3 border dark:border-gray-850"
+	class=" my-2.5 rounded-xl px-4 py-3 border border-gray-50 dark:border-gray-850"
 	id="message-feedback-{message.id}"
 	id="message-feedback-{message.id}"
 >
 >
 	<div class="flex justify-between items-center">
 	<div class="flex justify-between items-center">
@@ -97,7 +98,7 @@
 		<div class="flex flex-wrap gap-2 text-sm mt-2.5">
 		<div class="flex flex-wrap gap-2 text-sm mt-2.5">
 			{#each reasons as reason}
 			{#each reasons as reason}
 				<button
 				<button
-					class="px-3.5 py-1 border dark:border-gray-850 hover:bg-gray-100 dark:hover:bg-gray-850 {selectedReason ===
+					class="px-3.5 py-1 border border-gray-50 dark:border-gray-850 hover:bg-gray-100 dark:hover:bg-gray-850 {selectedReason ===
 					reason
 					reason
 						? 'bg-gray-200 dark:bg-gray-800'
 						? 'bg-gray-200 dark:bg-gray-800'
 						: ''} transition rounded-lg"
 						: ''} transition rounded-lg"
@@ -120,9 +121,21 @@
 		/>
 		/>
 	</div>
 	</div>
 
 
-	<div class="mt-2 flex justify-end">
+	<div class="mt-2 gap-1.5 flex justify-end">
+		{#if $config?.features.enable_community_sharing}
+			<button
+				class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition"
+				type="button"
+				on:click={() => {
+					show = false;
+				}}
+			>
+				{$i18n.t('Share to OpenWebUI Community')}
+			</button>
+		{/if}
+
 		<button
 		<button
-			class=" bg-emerald-700 text-white text-sm font-medium rounded-lg px-3.5 py-1.5"
+			class=" bg-emerald-700 hover:bg-emerald-800 transition text-white text-sm font-medium rounded-xl px-3.5 py-1.5"
 			on:click={() => {
 			on:click={() => {
 				saveHandler();
 				saveHandler();
 			}}
 			}}

+ 24 - 22
src/lib/components/chat/Messages/UserMessage.svelte

@@ -142,28 +142,30 @@
 
 
 			{#if edit === true}
 			{#if edit === true}
 				<div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2">
 				<div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2">
-					<textarea
-						id="message-edit-{message.id}"
-						bind:this={messageEditTextAreaElement}
-						class=" bg-transparent outline-none w-full resize-none"
-						bind:value={editedContent}
-						on:input={(e) => {
-							e.target.style.height = '';
-							e.target.style.height = `${e.target.scrollHeight}px`;
-						}}
-						on:keydown={(e) => {
-							if (e.key === 'Escape') {
-								document.getElementById('close-edit-message-button')?.click();
-							}
-
-							const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
-							const isEnterPressed = e.key === 'Enter';
-
-							if (isCmdOrCtrlPressed && isEnterPressed) {
-								document.getElementById('confirm-edit-message-button')?.click();
-							}
-						}}
-					/>
+					<div class="max-h-[25dvh] overflow-auto">
+						<textarea
+							id="message-edit-{message.id}"
+							bind:this={messageEditTextAreaElement}
+							class=" bg-transparent outline-none w-full resize-none"
+							bind:value={editedContent}
+							on:input={(e) => {
+								e.target.style.height = '';
+								e.target.style.height = `${e.target.scrollHeight}px`;
+							}}
+							on:keydown={(e) => {
+								if (e.key === 'Escape') {
+									document.getElementById('close-edit-message-button')?.click();
+								}
+
+								const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
+								const isEnterPressed = e.key === 'Enter';
+
+								if (isCmdOrCtrlPressed && isEnterPressed) {
+									document.getElementById('confirm-edit-message-button')?.click();
+								}
+							}}
+						/>
+					</div>
 
 
 					<div class=" mt-2 mb-1 flex justify-between text-sm font-medium">
 					<div class=" mt-2 mb-1 flex justify-between text-sm font-medium">
 						<div>
 						<div>

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

@@ -40,6 +40,7 @@
 			<div class="overflow-hidden w-full">
 			<div class="overflow-hidden w-full">
 				<div class="mr-1 max-w-full">
 				<div class="mr-1 max-w-full">
 					<Selector
 					<Selector
+						id={`${selectedModelIdx}`}
 						placeholder={$i18n.t('Select a model')}
 						placeholder={$i18n.t('Select a model')}
 						items={$models.map((model) => ({
 						items={$models.map((model) => ({
 							value: model.id,
 							value: model.id,

+ 21 - 10
src/lib/components/chat/ModelSelector/Selector.svelte

@@ -25,6 +25,7 @@
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
+	export let id = '';
 	export let value = '';
 	export let value = '';
 	export let placeholder = 'Select a model';
 	export let placeholder = 'Select a model';
 	export let searchEnabled = true;
 	export let searchEnabled = true;
@@ -229,7 +230,11 @@
 	}}
 	}}
 	closeFocus={false}
 	closeFocus={false}
 >
 >
-	<DropdownMenu.Trigger class="relative w-full font-primary" aria-label={placeholder}>
+	<DropdownMenu.Trigger
+		class="relative w-full font-primary"
+		aria-label={placeholder}
+		id="model-selector-{id}-button"
+	>
 		<div
 		<div
 			class="flex w-full text-left px-0.5 outline-none bg-transparent truncate text-lg font-medium placeholder-gray-400 focus:outline-none"
 			class="flex w-full text-left px-0.5 outline-none bg-transparent truncate text-lg font-medium placeholder-gray-400 focus:outline-none"
 		>
 		>
@@ -245,10 +250,10 @@
 	<DropdownMenu.Content
 	<DropdownMenu.Content
 		class=" z-40 {$mobile
 		class=" z-40 {$mobile
 			? `w-full`
 			? `w-full`
-			: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl  bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/40  outline-none"
+			: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl  bg-white dark:bg-gray-850 dark:text-white shadow-lg  outline-none"
 		transition={flyAndScale}
 		transition={flyAndScale}
 		side={$mobile ? 'bottom' : 'bottom-start'}
 		side={$mobile ? 'bottom' : 'bottom-start'}
-		sideOffset={4}
+		sideOffset={3}
 	>
 	>
 		<slot>
 		<slot>
 			{#if searchEnabled}
 			{#if searchEnabled}
@@ -281,7 +286,7 @@
 					/>
 					/>
 				</div>
 				</div>
 
 
-				<hr class="border-gray-100 dark:border-gray-800" />
+				<hr class="border-gray-50 dark:border-gray-800" />
 			{/if}
 			{/if}
 
 
 			<div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden group">
 			<div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden group">
@@ -474,10 +479,16 @@
 							</div>
 							</div>
 
 
 							<div class="flex flex-col self-start">
 							<div class="flex flex-col self-start">
-								<div class="line-clamp-1">
-									Downloading "{model}" {'pullProgress' in $MODEL_DOWNLOAD_POOL[model]
-										? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)`
-										: ''}
+								<div class="flex gap-1">
+									<div class="line-clamp-1">
+										Downloading "{model}"
+									</div>
+
+									<div class="flex-shrink-0">
+										{'pullProgress' in $MODEL_DOWNLOAD_POOL[model]
+											? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)`
+											: ''}
+									</div>
 								</div>
 								</div>
 
 
 								{#if 'digest' in $MODEL_DOWNLOAD_POOL[model] && $MODEL_DOWNLOAD_POOL[model].digest}
 								{#if 'digest' in $MODEL_DOWNLOAD_POOL[model] && $MODEL_DOWNLOAD_POOL[model].digest}
@@ -488,7 +499,7 @@
 							</div>
 							</div>
 						</div>
 						</div>
 
 
-						<div class="mr-2 translate-y-0.5">
+						<div class="mr-2 ml-1 translate-y-0.5">
 							<Tooltip content={$i18n.t('Cancel')}>
 							<Tooltip content={$i18n.t('Cancel')}>
 								<button
 								<button
 									class="text-gray-800 dark:text-gray-100"
 									class="text-gray-800 dark:text-gray-100"
@@ -521,7 +532,7 @@
 			</div>
 			</div>
 
 
 			{#if showTemporaryChatControl}
 			{#if showTemporaryChatControl}
-				<hr class="border-gray-100 dark:border-gray-800" />
+				<hr class="border-gray-50 dark:border-gray-800" />
 
 
 				<div class="flex items-center mx-2 my-2">
 				<div class="flex items-center mx-2 my-2">
 					<button
 					<button

+ 11 - 15
src/lib/components/chat/Placeholder.svelte

@@ -57,18 +57,14 @@
 		console.log(prompt);
 		console.log(prompt);
 		await tick();
 		await tick();
 
 
-		const chatInputElement = document.getElementById('chat-textarea');
-		if (chatInputElement) {
-			chatInputElement.style.height = '';
-			chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
-			chatInputElement.focus();
-
-			const words = findWordIndices(prompt);
-
-			if (words.length > 0) {
-				const word = words.at(0);
-				chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
-			}
+		const chatInputContainerElement = document.getElementById('chat-input-container');
+		if (chatInputContainerElement) {
+			chatInputContainerElement.style.height = '';
+			chatInputContainerElement.style.height =
+				Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
+
+			const chatInputElement = document.getElementById('chat-input');
+			chatInputElement?.focus();
 		}
 		}
 
 
 		await tick();
 		await tick();
@@ -106,7 +102,7 @@
 			class="w-full text-3xl text-gray-800 dark:text-gray-100 font-medium text-center flex items-center gap-4 font-primary"
 			class="w-full text-3xl text-gray-800 dark:text-gray-100 font-medium text-center flex items-center gap-4 font-primary"
 		>
 		>
 			<div class="w-full flex flex-col justify-center items-center">
 			<div class="w-full flex flex-col justify-center items-center">
-				<div class="flex flex-col md:flex-row justify-center gap-2 md:gap-3.5 w-fit">
+				<div class="flex flex-row justify-center gap-3 sm:gap-3.5 w-fit px-5">
 					<div class="flex flex-shrink-0 justify-center">
 					<div class="flex flex-shrink-0 justify-center">
 						<div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 100 }}>
 						<div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 100 }}>
 							{#each models as model, modelIdx}
 							{#each models as model, modelIdx}
@@ -127,7 +123,7 @@
 												($i18n.language === 'dg-DG'
 												($i18n.language === 'dg-DG'
 													? `/doge.png`
 													? `/doge.png`
 													: `${WEBUI_BASE_URL}/static/favicon.png`)}
 													: `${WEBUI_BASE_URL}/static/favicon.png`)}
-											class=" size-[2.5rem] rounded-full border-[1px] border-gray-200 dark:border-none"
+											class=" size-9 sm:size-10 rounded-full border-[1px] border-gray-200 dark:border-none"
 											alt="logo"
 											alt="logo"
 											draggable="false"
 											draggable="false"
 										/>
 										/>
@@ -137,7 +133,7 @@
 						</div>
 						</div>
 					</div>
 					</div>
 
 
-					<div class=" capitalize line-clamp-1 text-3xl md:text-4xl" in:fade={{ duration: 100 }}>
+					<div class=" capitalize text-3xl sm:text-4xl line-clamp-1" in:fade={{ duration: 100 }}>
 						{#if models[selectedModelIdx]?.info}
 						{#if models[selectedModelIdx]?.info}
 							{models[selectedModelIdx]?.info?.name}
 							{models[selectedModelIdx]?.info?.name}
 						{:else}
 						{:else}

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

@@ -19,6 +19,8 @@
 
 
 	// Addons
 	// Addons
 	let titleAutoGenerate = true;
 	let titleAutoGenerate = true;
+	let autoTags = true;
+
 	let responseAutoCopy = false;
 	let responseAutoCopy = false;
 	let widescreenMode = false;
 	let widescreenMode = false;
 	let splitLargeChunks = false;
 	let splitLargeChunks = false;
@@ -112,6 +114,11 @@
 		});
 		});
 	};
 	};
 
 
+	const toggleAutoTags = async () => {
+		autoTags = !autoTags;
+		saveSettings({ autoTags });
+	};
+
 	const toggleResponseAutoCopy = async () => {
 	const toggleResponseAutoCopy = async () => {
 		const permission = await navigator.clipboard
 		const permission = await navigator.clipboard
 			.readText()
 			.readText()
@@ -149,6 +156,7 @@
 
 
 	onMount(async () => {
 	onMount(async () => {
 		titleAutoGenerate = $settings?.title?.auto ?? true;
 		titleAutoGenerate = $settings?.title?.auto ?? true;
+		autoTags = $settings.autoTags ?? true;
 
 
 		responseAutoCopy = $settings.responseAutoCopy ?? false;
 		responseAutoCopy = $settings.responseAutoCopy ?? false;
 		showUsername = $settings.showUsername ?? false;
 		showUsername = $settings.showUsername ?? false;
@@ -431,6 +439,26 @@
 				</div>
 				</div>
 			</div>
 			</div>
 
 
+			<div>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs">{$i18n.t('Chat Tags Auto-Generation')}</div>
+
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						on:click={() => {
+							toggleAutoTags();
+						}}
+						type="button"
+					>
+						{#if autoTags === true}
+							<span class="ml-2 self-center">{$i18n.t('On')}</span>
+						{:else}
+							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
+						{/if}
+					</button>
+				</div>
+			</div>
+
 			<div>
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
 				<div class=" py-0.5 flex w-full justify-between">
 					<div class=" self-center text-xs">
 					<div class=" self-center text-xs">

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

@@ -184,7 +184,7 @@
 							<div
 							<div
 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
 							>
 							>
-								⌫
+								⌫/Delete
 							</div>
 							</div>
 						</div>
 						</div>
 					</div>
 					</div>

+ 9 - 1
src/lib/components/chat/Tags.svelte

@@ -20,6 +20,7 @@
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
 	import Tags from '../common/Tags.svelte';
 	import Tags from '../common/Tags.svelte';
+	import { toast } from 'svelte-sonner';
 
 
 	export let chatId = '';
 	export let chatId = '';
 	let tags = [];
 	let tags = [];
@@ -31,7 +32,14 @@
 	};
 	};
 
 
 	const addTag = async (tagName) => {
 	const addTag = async (tagName) => {
-		const res = await addTagById(localStorage.token, chatId, tagName);
+		const res = await addTagById(localStorage.token, chatId, tagName).catch(async (error) => {
+			toast.error(error);
+			return null;
+		});
+		if (!res) {
+			return;
+		}
+
 		tags = await getTags();
 		tags = await getTags();
 		await updateChatById(localStorage.token, chatId, {
 		await updateChatById(localStorage.token, chatId, {
 			tags: tags
 			tags: tags

+ 2 - 1
src/lib/components/common/Badge.svelte

@@ -6,7 +6,8 @@
 		info: 'bg-blue-500/20 text-blue-700 dark:text-blue-200 ',
 		info: 'bg-blue-500/20 text-blue-700 dark:text-blue-200 ',
 		success: 'bg-green-500/20 text-green-700 dark:text-green-200',
 		success: 'bg-green-500/20 text-green-700 dark:text-green-200',
 		warning: 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-200',
 		warning: 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-200',
-		error: 'bg-red-500/20 text-red-700 dark:text-red-200'
+		error: 'bg-red-500/20 text-red-700 dark:text-red-200',
+		muted: 'bg-gray-500/20 text-gray-700 dark:text-gray-200'
 	};
 	};
 </script>
 </script>
 
 

+ 28 - 7
src/lib/components/common/Collapsible.svelte

@@ -14,11 +14,23 @@
 	export let className = '';
 	export let className = '';
 	export let buttonClassName = 'w-fit';
 	export let buttonClassName = 'w-fit';
 	export let title = null;
 	export let title = null;
+
+	export let disabled = false;
+	export let hide = false;
 </script>
 </script>
 
 
 <div class={className}>
 <div class={className}>
 	{#if title !== null}
 	{#if title !== null}
-		<button class={buttonClassName} on:click={() => (open = !open)}>
+		<!-- svelte-ignore a11y-no-static-element-interactions -->
+		<!-- svelte-ignore a11y-click-events-have-key-events -->
+		<div
+			class="{buttonClassName} cursor-pointer"
+			on:pointerup={() => {
+				if (!disabled) {
+					open = !open;
+				}
+			}}
+		>
 			<div class=" w-fit font-medium transition flex items-center justify-between gap-2">
 			<div class=" w-fit font-medium transition flex items-center justify-between gap-2">
 				<div>
 				<div>
 					{title}
 					{title}
@@ -26,24 +38,33 @@
 
 
 				<div>
 				<div>
 					{#if open}
 					{#if open}
-						<ChevronUp strokeWidth="3.5" className="size-3.5 " />
+						<ChevronUp strokeWidth="3.5" className="size-3.5" />
 					{:else}
 					{:else}
-						<ChevronDown strokeWidth="3.5" className="size-3.5 " />
+						<ChevronDown strokeWidth="3.5" className="size-3.5" />
 					{/if}
 					{/if}
 				</div>
 				</div>
 			</div>
 			</div>
-		</button>
+		</div>
 	{:else}
 	{:else}
-		<button class={buttonClassName} on:click={() => (open = !open)}>
+		<!-- svelte-ignore a11y-no-static-element-interactions -->
+		<!-- svelte-ignore a11y-click-events-have-key-events -->
+		<div
+			class="{buttonClassName} cursor-pointer"
+			on:pointerup={() => {
+				if (!disabled) {
+					open = !open;
+				}
+			}}
+		>
 			<div
 			<div
 				class="flex items-center gap-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
 				class="flex items-center gap-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
 			>
 			>
 				<slot />
 				<slot />
 			</div>
 			</div>
-		</button>
+		</div>
 	{/if}
 	{/if}
 
 
-	{#if open}
+	{#if open && !hide}
 		<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
 		<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
 			<slot name="content" />
 			<slot name="content" />
 		</div>
 		</div>

+ 9 - 4
src/lib/components/common/ConfirmDialog.svelte

@@ -1,12 +1,11 @@
 <script lang="ts">
 <script lang="ts">
 	import { onMount, getContext, createEventDispatcher } from 'svelte';
 	import { onMount, getContext, createEventDispatcher } from 'svelte';
-	import { fade } from 'svelte/transition';
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
+	const dispatch = createEventDispatcher();
 
 
+	import { fade } from 'svelte/transition';
 	import { flyAndScale } from '$lib/utils/transitions';
 	import { flyAndScale } from '$lib/utils/transitions';
 
 
-	const dispatch = createEventDispatcher();
-
 	export let title = '';
 	export let title = '';
 	export let message = '';
 	export let message = '';
 
 
@@ -27,6 +26,12 @@
 			console.log('Escape');
 			console.log('Escape');
 			show = false;
 			show = false;
 		}
 		}
+
+		if (event.key === 'Enter') {
+			console.log('Enter');
+			show = false;
+			dispatch('confirm', inputValue);
+		}
 	};
 	};
 
 
 	onMount(() => {
 	onMount(() => {
@@ -56,7 +61,7 @@
 		}}
 		}}
 	>
 	>
 		<div
 		<div
-			class=" m-auto rounded-2xl max-w-full w-[32rem] mx-2 bg-gray-50 dark:bg-gray-950 max-h-[100dvh] shadow-3xl border border-gray-850"
+			class=" m-auto rounded-2xl max-w-full w-[32rem] mx-2 bg-gray-50 dark:bg-gray-950 max-h-[100dvh] shadow-3xl"
 			in:flyAndScale
 			in:flyAndScale
 			on:mousedown={(e) => {
 			on:mousedown={(e) => {
 				e.stopPropagation();
 				e.stopPropagation();

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

@@ -24,7 +24,7 @@
 	bind:this={popupElement}
 	bind:this={popupElement}
 	class="fixed top-0 left-0 w-screen h-[100dvh] z-50 touch-none pointer-events-none"
 	class="fixed top-0 left-0 w-screen h-[100dvh] z-50 touch-none pointer-events-none"
 >
 >
-	<div class=" absolute text-white z-[99999]" style="top: {y}px; left: {x}px;">
+	<div class=" absolute text-white z-[99999]" style="top: {y + 10}px; left: {x + 10}px;">
 		<slot></slot>
 		<slot></slot>
 	</div>
 	</div>
 </div>
 </div>

+ 2 - 14
src/lib/components/common/Drawer.svelte

@@ -6,23 +6,11 @@
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
 	export let show = false;
 	export let show = false;
-	export let size = 'md';
+	export let className = '';
 
 
 	let modalElement = null;
 	let modalElement = null;
 	let mounted = false;
 	let mounted = false;
 
 
-	const sizeToWidth = (size) => {
-		if (size === 'xs') {
-			return 'w-[16rem]';
-		} else if (size === 'sm') {
-			return 'w-[30rem]';
-		} else if (size === 'md') {
-			return 'w-[48rem]';
-		} else {
-			return 'w-[56rem]';
-		}
-	};
-
 	const handleKeyDown = (event: KeyboardEvent) => {
 	const handleKeyDown = (event: KeyboardEvent) => {
 		if (event.key === 'Escape' && isTopModal()) {
 		if (event.key === 'Escape' && isTopModal()) {
 			console.log('Escape');
 			console.log('Escape');
@@ -76,7 +64,7 @@
 	}}
 	}}
 >
 >
 	<div
 	<div
-		class=" mt-auto max-w-full w-full bg-gray-50 dark:bg-gray-900 max-h-[100dvh] overflow-y-auto scrollbar-hidden"
+		class=" mt-auto max-w-full w-full bg-gray-50 dark:bg-gray-900 dark:text-gray-100 {className} max-h-[100dvh] overflow-y-auto scrollbar-hidden"
 		on:mousedown={(e) => {
 		on:mousedown={(e) => {
 			e.stopPropagation();
 			e.stopPropagation();
 		}}
 		}}

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

@@ -22,7 +22,7 @@
 
 
 	<slot name="content">
 	<slot name="content">
 		<DropdownMenu.Content
 		<DropdownMenu.Content
-			class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-700 z-50 bg-gray-850 text-white"
+			class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-900 z-50 bg-gray-850 text-white"
 			sideOffset={8}
 			sideOffset={8}
 			side="bottom"
 			side="bottom"
 			align="start"
 			align="start"

+ 54 - 15
src/lib/components/common/Folder.svelte

@@ -14,34 +14,73 @@
 	export let name = '';
 	export let name = '';
 	export let collapsible = true;
 	export let collapsible = true;
 
 
+	export let className = '';
+
 	let folderElement;
 	let folderElement;
 
 
-	let dragged = false;
+	let draggedOver = false;
 
 
 	const onDragOver = (e) => {
 	const onDragOver = (e) => {
 		e.preventDefault();
 		e.preventDefault();
-		dragged = true;
+		e.stopPropagation();
+		draggedOver = true;
 	};
 	};
 
 
 	const onDrop = (e) => {
 	const onDrop = (e) => {
 		e.preventDefault();
 		e.preventDefault();
+		e.stopPropagation();
 
 
 		if (folderElement.contains(e.target)) {
 		if (folderElement.contains(e.target)) {
 			console.log('Dropped on the Button');
 			console.log('Dropped on the Button');
 
 
-			// get data from the drag event
-			const dataTransfer = e.dataTransfer.getData('text/plain');
-			const data = JSON.parse(dataTransfer);
-			console.log(data);
-			dispatch('drop', data);
-
-			dragged = false;
+			if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
+				// Iterate over all items in the DataTransferItemList use functional programming
+				for (const item of Array.from(e.dataTransfer.items)) {
+					// If dropped items aren't files, reject them
+					if (item.kind === 'file') {
+						const file = item.getAsFile();
+						if (file && file.type === 'application/json') {
+							console.log('Dropped file is a JSON file!');
+
+							// Read the JSON file with FileReader
+							const reader = new FileReader();
+							reader.onload = async function (event) {
+								try {
+									const fileContent = JSON.parse(event.target.result);
+									console.log('Parsed JSON Content: ', fileContent);
+									open = true;
+									dispatch('import', fileContent);
+								} catch (error) {
+									console.error('Error parsing JSON file:', error);
+								}
+							};
+
+							// Start reading the file
+							reader.readAsText(file);
+						} else {
+							console.error('Only JSON file types are supported.');
+						}
+					} else {
+						open = true;
+
+						const dataTransfer = e.dataTransfer.getData('text/plain');
+						const data = JSON.parse(dataTransfer);
+
+						console.log(data);
+						dispatch('drop', data);
+					}
+				}
+			}
+
+			draggedOver = false;
 		}
 		}
 	};
 	};
 
 
 	const onDragLeave = (e) => {
 	const onDragLeave = (e) => {
 		e.preventDefault();
 		e.preventDefault();
-		dragged = false;
+		e.stopPropagation();
+
+		draggedOver = false;
 	};
 	};
 
 
 	onMount(() => {
 	onMount(() => {
@@ -57,10 +96,10 @@
 	});
 	});
 </script>
 </script>
 
 
-<div bind:this={folderElement} class="relative">
-	{#if dragged}
+<div bind:this={folderElement} class="relative {className}">
+	{#if draggedOver}
 		<div
 		<div
-			class="absolute top-0 left-0 w-full h-full rounded-sm bg-gray-200 bg-opacity-50 dark:bg-opacity-10 z-50 pointer-events-none touch-none"
+			class="absolute top-0 left-0 w-full h-full rounded-sm bg-[hsla(260,85%,65%,0.1)] bg-opacity-50 dark:bg-opacity-10 z-50 pointer-events-none touch-none"
 		></div>
 		></div>
 	{/if}
 	{/if}
 
 
@@ -74,7 +113,7 @@
 			}}
 			}}
 		>
 		>
 			<!-- svelte-ignore a11y-no-static-element-interactions -->
 			<!-- svelte-ignore a11y-no-static-element-interactions -->
-			<div class="mx-2 w-full">
+			<div class="w-full">
 				<button
 				<button
 					class="w-full py-1.5 px-2 rounded-md flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
 					class="w-full py-1.5 px-2 rounded-md flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
 				>
 				>
@@ -92,7 +131,7 @@
 				</button>
 				</button>
 			</div>
 			</div>
 
 
-			<div slot="content" class=" pl-2">
+			<div slot="content" class="w-full">
 				<slot></slot>
 				<slot></slot>
 			</div>
 			</div>
 		</Collapsible>
 		</Collapsible>

+ 7 - 3
src/lib/components/common/Modal.svelte

@@ -6,11 +6,15 @@
 
 
 	export let show = true;
 	export let show = true;
 	export let size = 'md';
 	export let size = 'md';
+	export let className = 'bg-gray-50 dark:bg-gray-900  rounded-2xl';
 
 
 	let modalElement = null;
 	let modalElement = null;
 	let mounted = false;
 	let mounted = false;
 
 
 	const sizeToWidth = (size) => {
 	const sizeToWidth = (size) => {
+		if (size === 'full') {
+			return 'w-full';
+		}
 		if (size === 'xs') {
 		if (size === 'xs') {
 			return 'w-[16rem]';
 			return 'w-[16rem]';
 		} else if (size === 'sm') {
 		} else if (size === 'sm') {
@@ -68,9 +72,9 @@
 		}}
 		}}
 	>
 	>
 		<div
 		<div
-			class=" m-auto rounded-2xl max-w-full {sizeToWidth(
-				size
-			)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl max-h-[100dvh] overflow-y-auto scrollbar-hidden"
+			class=" m-auto max-w-full {sizeToWidth(size)} {size !== 'full'
+				? 'mx-2'
+				: ''} shadow-3xl max-h-[100dvh] overflow-y-auto scrollbar-hidden {className}"
 			in:flyAndScale
 			in:flyAndScale
 			on:mousedown={(e) => {
 			on:mousedown={(e) => {
 				e.stopPropagation();
 				e.stopPropagation();

+ 470 - 0
src/lib/components/common/RichTextInput.svelte

@@ -0,0 +1,470 @@
+<script lang="ts">
+	import { onDestroy, onMount } from 'svelte';
+	import { createEventDispatcher } from 'svelte';
+	const eventDispatch = createEventDispatcher();
+
+	import { EditorState, Plugin, TextSelection } from 'prosemirror-state';
+	import { EditorView, Decoration, DecorationSet } from 'prosemirror-view';
+	import { undo, redo, history } from 'prosemirror-history';
+	import {
+		schema,
+		defaultMarkdownParser,
+		MarkdownParser,
+		defaultMarkdownSerializer
+	} from 'prosemirror-markdown';
+
+	import {
+		inputRules,
+		wrappingInputRule,
+		textblockTypeInputRule,
+		InputRule
+	} from 'prosemirror-inputrules'; // Import input rules
+	import { splitListItem, liftListItem, sinkListItem } from 'prosemirror-schema-list'; // Import from prosemirror-schema-list
+	import { keymap } from 'prosemirror-keymap';
+	import { baseKeymap, chainCommands } from 'prosemirror-commands';
+	import { DOMParser, DOMSerializer, Schema, Fragment } from 'prosemirror-model';
+
+	export let className = 'input-prose';
+	export let shiftEnter = false;
+
+	export let id = '';
+	export let value = '';
+	export let placeholder = 'Type here...';
+
+	let element: HTMLElement; // Element where ProseMirror will attach
+	let state;
+	let view;
+
+	// Plugin to add placeholder when the content is empty
+	function placeholderPlugin(placeholder: string) {
+		return new Plugin({
+			props: {
+				decorations(state) {
+					const doc = state.doc;
+					if (
+						doc.childCount === 1 &&
+						doc.firstChild.isTextblock &&
+						doc.firstChild?.textContent === ''
+					) {
+						// If there's nothing in the editor, show the placeholder decoration
+						const decoration = Decoration.node(0, doc.content.size, {
+							'data-placeholder': placeholder,
+							class: 'placeholder'
+						});
+						return DecorationSet.create(doc, [decoration]);
+					}
+					return DecorationSet.empty;
+				}
+			}
+		});
+	}
+
+	function unescapeMarkdown(text: string): string {
+		return text
+			.replace(/\\([\\`*{}[\]()#+\-.!_>])/g, '$1') // unescape backslashed characters
+			.replace(/&amp;/g, '&')
+			.replace(/</g, '<')
+			.replace(/>/g, '>')
+			.replace(/&quot;/g, '"')
+			.replace(/&#39;/g, "'");
+	}
+
+	// Custom parsing rule that creates proper paragraphs for newlines and empty lines
+	function markdownToProseMirrorDoc(markdown: string) {
+		// Split the markdown into lines
+		const lines = markdown.split('\n\n');
+
+		// Create an array to hold our paragraph nodes
+		const paragraphs = [];
+
+		// Process each line
+		lines.forEach((line) => {
+			if (line.trim() === '') {
+				// For empty lines, create an empty paragraph
+				paragraphs.push(schema.nodes.paragraph.create());
+			} else {
+				// For non-empty lines, parse as usual
+				const doc = defaultMarkdownParser.parse(line);
+				// Extract the content of the parsed document
+				doc.content.forEach((node) => {
+					paragraphs.push(node);
+				});
+			}
+		});
+
+		// Create a new document with these paragraphs
+		return schema.node('doc', null, paragraphs);
+	}
+
+	// Create a custom serializer for paragraphs
+	// Custom paragraph serializer to preserve newlines for empty paragraphs (empty block).
+	function serializeParagraph(state, node: Node) {
+		const content = node.textContent.trim();
+
+		// If the paragraph is empty, just add an empty line.
+		if (content === '') {
+			state.write('\n\n');
+		} else {
+			state.renderInline(node);
+			state.closeBlock(node);
+		}
+	}
+
+	const customMarkdownSerializer = new defaultMarkdownSerializer.constructor(
+		{
+			...defaultMarkdownSerializer.nodes,
+
+			paragraph: (state, node) => {
+				serializeParagraph(state, node); // Use custom paragraph serialization
+			}
+
+			// Customize other block formats if needed
+		},
+
+		// Copy marks directly from the original serializer (or customize them if necessary)
+		defaultMarkdownSerializer.marks
+	);
+
+	// Utility function to convert ProseMirror content back to markdown text
+	function serializeEditorContent(doc) {
+		const markdown = customMarkdownSerializer.serialize(doc);
+		return unescapeMarkdown(markdown);
+	}
+
+	// ---- Input Rules ----
+	// Input rule for heading (e.g., # Headings)
+	function headingRule(schema) {
+		return textblockTypeInputRule(/^(#{1,6})\s$/, schema.nodes.heading, (match) => ({
+			level: match[1].length
+		}));
+	}
+
+	// Input rule for bullet list (e.g., `- item`)
+	function bulletListRule(schema) {
+		return wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list);
+	}
+
+	// Input rule for ordered list (e.g., `1. item`)
+	function orderedListRule(schema) {
+		return wrappingInputRule(/^(\d+)\.\s$/, schema.nodes.ordered_list, (match) => ({
+			order: +match[1]
+		}));
+	}
+
+	// Custom input rules for Bold/Italic (using * or _)
+	function markInputRule(regexp: RegExp, markType: any) {
+		return new InputRule(regexp, (state, match, start, end) => {
+			const { tr } = state;
+			if (match) {
+				tr.replaceWith(start, end, schema.text(match[1], [markType.create()]));
+			}
+			return tr;
+		});
+	}
+
+	function boldRule(schema) {
+		return markInputRule(/\*([^*]+)\*/, schema.marks.strong);
+	}
+
+	function italicRule(schema) {
+		return markInputRule(/\_([^*]+)\_/, schema.marks.em);
+	}
+
+	// Initialize Editor State and View
+	function afterSpacePress(state, dispatch) {
+		// Get the position right after the space was naturally inserted by the browser.
+		let { from, to, empty } = state.selection;
+
+		if (dispatch && empty) {
+			let tr = state.tr;
+
+			// Check for any active marks at `from - 1` (the space we just inserted)
+			const storedMarks = state.storedMarks || state.selection.$from.marks();
+
+			const hasBold = storedMarks.some((mark) => mark.type === state.schema.marks.strong);
+			const hasItalic = storedMarks.some((mark) => mark.type === state.schema.marks.em);
+
+			// Remove marks from the space character (marks applied to the space character will be marked as false)
+			if (hasBold) {
+				tr = tr.removeMark(from - 1, from, state.schema.marks.strong);
+			}
+			if (hasItalic) {
+				tr = tr.removeMark(from - 1, from, state.schema.marks.em);
+			}
+
+			// Dispatch the resulting transaction to update the editor state
+			dispatch(tr);
+		}
+
+		return true;
+	}
+
+	function toggleMark(markType) {
+		return (state, dispatch) => {
+			const { from, to } = state.selection;
+			if (state.doc.rangeHasMark(from, to, markType)) {
+				if (dispatch) dispatch(state.tr.removeMark(from, to, markType));
+				return true;
+			} else {
+				if (dispatch) dispatch(state.tr.addMark(from, to, markType.create()));
+				return true;
+			}
+		};
+	}
+
+	function isInList(state) {
+		const { $from } = state.selection;
+		return (
+			$from.parent.type === schema.nodes.paragraph && $from.node(-1).type === schema.nodes.list_item
+		);
+	}
+
+	function isEmptyListItem(state) {
+		const { $from } = state.selection;
+		return isInList(state) && $from.parent.content.size === 0 && $from.node(-1).childCount === 1;
+	}
+
+	function exitList(state, dispatch) {
+		return liftListItem(schema.nodes.list_item)(state, dispatch);
+	}
+
+	function findNextTemplate(doc, from = 0) {
+		const patterns = [
+			{ start: '[', end: ']' },
+			{ start: '{{', end: '}}' }
+		];
+
+		let result = null;
+
+		doc.nodesBetween(from, doc.content.size, (node, pos) => {
+			if (result) return false; // Stop if we've found a match
+			if (node.isText) {
+				const text = node.text;
+				let index = Math.max(0, from - pos);
+				while (index < text.length) {
+					for (const pattern of patterns) {
+						if (text.startsWith(pattern.start, index)) {
+							const endIndex = text.indexOf(pattern.end, index + pattern.start.length);
+							if (endIndex !== -1) {
+								result = {
+									from: pos + index,
+									to: pos + endIndex + pattern.end.length
+								};
+								return false; // Stop searching
+							}
+						}
+					}
+					index++;
+				}
+			}
+		});
+
+		return result;
+	}
+
+	function selectNextTemplate(state, dispatch) {
+		const { doc, selection } = state;
+		const from = selection.to;
+		let template = findNextTemplate(doc, from);
+
+		if (!template) {
+			// If not found, search from the beginning
+			template = findNextTemplate(doc, 0);
+		}
+
+		if (template) {
+			if (dispatch) {
+				const tr = state.tr.setSelection(TextSelection.create(doc, template.from, template.to));
+				dispatch(tr);
+			}
+			return true;
+		}
+		return false;
+	}
+
+	onMount(() => {
+		const initialDoc = markdownToProseMirrorDoc(value || ''); // Convert the initial content
+
+		state = EditorState.create({
+			doc: initialDoc,
+			schema,
+			plugins: [
+				history(),
+				placeholderPlugin(placeholder),
+				inputRules({
+					rules: [
+						headingRule(schema), // Handle markdown-style headings (# H1, ## H2, etc.)
+						bulletListRule(schema), // Handle `-` or `*` input to start bullet list
+						orderedListRule(schema), // Handle `1.` input to start ordered list
+						boldRule(schema), // Bold input rule
+						italicRule(schema) // Italic input rule
+					]
+				}),
+				keymap({
+					...baseKeymap,
+					'Mod-z': undo,
+					'Mod-y': redo,
+					Enter: (state, dispatch, view) => {
+						if (shiftEnter) {
+							eventDispatch('enter');
+							return true;
+						}
+						return chainCommands(
+							(state, dispatch, view) => {
+								if (isEmptyListItem(state)) {
+									return exitList(state, dispatch);
+								}
+								return false;
+							},
+							(state, dispatch, view) => {
+								if (isInList(state)) {
+									return splitListItem(schema.nodes.list_item)(state, dispatch);
+								}
+								return false;
+							},
+							baseKeymap.Enter
+						)(state, dispatch, view);
+					},
+
+					'Shift-Enter': (state, dispatch, view) => {
+						if (shiftEnter) {
+							return chainCommands(
+								(state, dispatch, view) => {
+									if (isEmptyListItem(state)) {
+										return exitList(state, dispatch);
+									}
+									return false;
+								},
+								(state, dispatch, view) => {
+									if (isInList(state)) {
+										return splitListItem(schema.nodes.list_item)(state, dispatch);
+									}
+									return false;
+								},
+								baseKeymap.Enter
+							)(state, dispatch, view);
+						} else {
+							return baseKeymap.Enter(state, dispatch, view);
+						}
+						return false;
+					},
+
+					// Prevent default tab navigation and provide indent/outdent behavior inside lists:
+					Tab: chainCommands((state, dispatch, view) => {
+						const { $from } = state.selection;
+						if (isInList(state)) {
+							return sinkListItem(schema.nodes.list_item)(state, dispatch);
+						} else {
+							return selectNextTemplate(state, dispatch);
+						}
+						return true; // Prevent Tab from moving the focus
+					}),
+					'Shift-Tab': (state, dispatch, view) => {
+						const { $from } = state.selection;
+						if (isInList(state)) {
+							return liftListItem(schema.nodes.list_item)(state, dispatch);
+						}
+						return true; // Prevent Shift-Tab from moving the focus
+					},
+					'Mod-b': toggleMark(schema.marks.strong),
+					'Mod-i': toggleMark(schema.marks.em)
+				})
+			]
+		});
+
+		view = new EditorView(element, {
+			state,
+			dispatchTransaction(transaction) {
+				// Update editor state
+				let newState = view.state.apply(transaction);
+				view.updateState(newState);
+
+				value = serializeEditorContent(newState.doc); // Convert ProseMirror content to markdown text
+				eventDispatch('input', { value });
+			},
+			handleDOMEvents: {
+				focus: (view, event) => {
+					eventDispatch('focus', { event });
+					return false;
+				},
+				keypress: (view, event) => {
+					eventDispatch('keypress', { event });
+					return false;
+				},
+				keydown: (view, event) => {
+					eventDispatch('keydown', { event });
+					return false;
+				},
+				paste: (view, event) => {
+					if (event.clipboardData) {
+						// Check if the pasted content contains image files
+						const hasImageFile = Array.from(event.clipboardData.files).some((file) =>
+							file.type.startsWith('image/')
+						);
+
+						// Check for image in dataTransfer items (for cases where files are not available)
+						const hasImageItem = Array.from(event.clipboardData.items).some((item) =>
+							item.type.startsWith('image/')
+						);
+						if (hasImageFile) {
+							// If there's an image, dispatch the event to the parent
+							eventDispatch('paste', { event });
+							event.preventDefault();
+							return true;
+						}
+
+						if (hasImageItem) {
+							// If there's an image item, dispatch the event to the parent
+							eventDispatch('paste', { event });
+							event.preventDefault();
+							return true;
+						}
+					}
+
+					// For all other cases (text, formatted text, etc.), let ProseMirror handle it
+					return false;
+				},
+				// Handle space input after browser has completed it
+				keyup: (view, event) => {
+					if (event.key === ' ' && event.code === 'Space') {
+						afterSpacePress(view.state, view.dispatch);
+					}
+					return false;
+				}
+			},
+			attributes: { id }
+		});
+	});
+
+	// Reinitialize the editor if the value is externally changed (i.e. when `value` is updated)
+	$: if (view && value !== serializeEditorContent(view.state.doc)) {
+		const newDoc = markdownToProseMirrorDoc(value || '');
+
+		const newState = EditorState.create({
+			doc: newDoc,
+			schema,
+			plugins: view.state.plugins,
+			selection: TextSelection.atEnd(newDoc) // This sets the cursor at the end
+		});
+		view.updateState(newState);
+
+		if (value !== '') {
+			// After updating the state, try to find and select the next template
+			setTimeout(() => {
+				const templateFound = selectNextTemplate(view.state, view.dispatch);
+				if (!templateFound) {
+					// If no template found, set cursor at the end
+					const endPos = view.state.doc.content.size;
+					view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, endPos)));
+				}
+			}, 0);
+		}
+	}
+
+	// Destroy ProseMirror instance on unmount
+	onDestroy(() => {
+		view?.destroy();
+	});
+</script>
+
+<div bind:this={element} class="relative w-full min-w-full h-full min-h-fit {className}"></div>

+ 22 - 24
src/lib/components/common/Tags/TagList.svelte

@@ -1,34 +1,32 @@
 <script lang="ts">
 <script lang="ts">
 	import { createEventDispatcher } from 'svelte';
 	import { createEventDispatcher } from 'svelte';
+	import Tooltip from '../Tooltip.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
+	import Badge from '../Badge.svelte';
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
 	export let tags = [];
 	export let tags = [];
 </script>
 </script>
 
 
 {#each tags as tag}
 {#each tags as tag}
-	<div
-		class="px-2 py-[0.5px] gap-0.5 flex justify-between h-fit items-center rounded-full transition border dark:border-gray-800 dark:text-white"
-	>
-		<div class=" text-[0.7rem] font-medium self-center line-clamp-1">
-			{tag.name}
-		</div>
-		<button
-			class="h-full flex self-center cursor-pointer"
-			on:click={() => {
-				dispatch('delete', tag.name);
-			}}
-			type="button"
+	<Tooltip content={tag.name}>
+		<div
+			class="relative group px-1.5 py-[0.2px] gap-0.5 flex justify-between h-fit max-h-fit w-fit items-center rounded-full bg-gray-500/20 text-gray-700 dark:text-gray-200 transition cursor-pointer"
 		>
 		>
-			<svg
-				xmlns="http://www.w3.org/2000/svg"
-				viewBox="0 0 16 16"
-				fill="currentColor"
-				class="size-3 m-auto self-center translate-y-[0.3px] translate-x-[3px]"
-			>
-				<path
-					d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
-				/>
-			</svg>
-		</button>
-	</div>
+			<div class=" text-[0.7rem] font-medium self-center line-clamp-1 w-fit">
+				{tag.name}
+			</div>
+			<div class="absolute invisible right-0.5 group-hover:visible transition">
+				<button
+					class="rounded-full border bg-white dark:bg-gray-700 h-full flex self-center cursor-pointer"
+					on:click={() => {
+						dispatch('delete', tag.name);
+					}}
+					type="button"
+				>
+					<XMark className="size-3" strokeWidth="2.5" />
+				</button>
+			</div>
+		</div>
+	</Tooltip>
 {/each}
 {/each}

+ 37 - 0
src/lib/components/common/Textarea.svelte

@@ -0,0 +1,37 @@
+<script lang="ts">
+	import { onMount, tick } from 'svelte';
+
+	export let value = '';
+	export let placeholder = '';
+
+	export let className =
+		'w-full rounded-lg px-3 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none h-full';
+
+	let textareaElement;
+
+	onMount(async () => {
+		await tick();
+		if (textareaElement) {
+			setInterval(adjustHeight, 0);
+		}
+	});
+
+	const adjustHeight = () => {
+		if (textareaElement) {
+			textareaElement.style.height = '';
+			textareaElement.style.height = `${textareaElement.scrollHeight}px`;
+		}
+	};
+</script>
+
+<textarea
+	bind:this={textareaElement}
+	bind:value
+	{placeholder}
+	class={className}
+	on:input={(e) => {
+		e.target.style.height = '';
+		e.target.style.height = `${e.target.scrollHeight}px`;
+	}}
+	rows="1"
+/>

+ 0 - 124
src/lib/components/icons/ChatMenu.svelte

@@ -1,124 +0,0 @@
-<script lang="ts">
-	import { DropdownMenu } from 'bits-ui';
-	import { flyAndScale } from '$lib/utils/transitions';
-	import { getContext } from 'svelte';
-
-	import Dropdown from '$lib/components/common/Dropdown.svelte';
-	import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
-	import Pencil from '$lib/components/icons/Pencil.svelte';
-	import Tooltip from '$lib/components/common/Tooltip.svelte';
-	import Tags from '$lib/components/chat/Tags.svelte';
-	import Share from '$lib/components/icons/Share.svelte';
-	import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
-	import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
-	import Star from '$lib/components/icons/Star.svelte';
-
-	const i18n = getContext('i18n');
-
-	export let pinHandler: Function;
-	export let shareHandler: Function;
-	export let cloneChatHandler: Function;
-	export let archiveChatHandler: Function;
-	export let renameHandler: Function;
-	export let deleteHandler: Function;
-	export let onClose: Function;
-
-	export let chatId = '';
-
-	let show = false;
-</script>
-
-<Dropdown
-	bind:show
-	on:change={(e) => {
-		if (e.detail === false) {
-			onClose();
-		}
-	}}
->
-	<Tooltip content={$i18n.t('More')}>
-		<slot />
-	</Tooltip>
-
-	<div slot="content">
-		<DropdownMenu.Content
-			class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
-			sideOffset={-2}
-			side="bottom"
-			align="start"
-			transition={flyAndScale}
-		>
-			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
-				on:click={() => {
-					pinHandler();
-				}}
-			>
-				<Star strokeWidth="2" />
-				<div class="flex items-center">{$i18n.t('Pin')}</div>
-			</DropdownMenu.Item>
-
-			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
-				on:click={() => {
-					renameHandler();
-				}}
-			>
-				<Pencil strokeWidth="2" />
-				<div class="flex items-center">{$i18n.t('Rename')}</div>
-			</DropdownMenu.Item>
-
-			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
-				on:click={() => {
-					cloneChatHandler();
-				}}
-			>
-				<DocumentDuplicate strokeWidth="2" />
-				<div class="flex items-center">{$i18n.t('Clone')}</div>
-			</DropdownMenu.Item>
-
-			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
-				on:click={() => {
-					archiveChatHandler();
-				}}
-			>
-				<ArchiveBox strokeWidth="2" />
-				<div class="flex items-center">{$i18n.t('Archive')}</div>
-			</DropdownMenu.Item>
-
-			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-md"
-				on:click={() => {
-					shareHandler();
-				}}
-			>
-				<Share />
-				<div class="flex items-center">{$i18n.t('Share')}</div>
-			</DropdownMenu.Item>
-
-			<DropdownMenu.Item
-				class="flex  gap-2  items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
-				on:click={() => {
-					deleteHandler();
-				}}
-			>
-				<GarbageBin strokeWidth="2" />
-				<div class="flex items-center">{$i18n.t('Delete')}</div>
-			</DropdownMenu.Item>
-
-			<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
-
-			<div class="flex p-1">
-				<Tags
-					{chatId}
-					on:close={() => {
-						show = false;
-						onClose();
-					}}
-				/>
-			</div>
-		</DropdownMenu.Content>
-	</div>
-</Dropdown>

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

@@ -0,0 +1,19 @@
+<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="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
+	/>
+</svg>

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

@@ -0,0 +1,19 @@
+<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="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
+	/>
+</svg>

+ 10 - 0
src/lib/components/icons/Mic.svelte

@@ -0,0 +1,10 @@
+<script lang="ts">
+	export let className = 'size-4';
+</script>
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class={className}>
+	<path d="M7 4a3 3 0 0 1 6 0v6a3 3 0 1 1-6 0V4Z" />
+	<path
+		d="M5.5 9.643a.75.75 0 0 0-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 0 0 0 1.5h4.5a.75.75 0 0 0 0-1.5h-1.5v-1.546A6.001 6.001 0 0 0 16 10v-.357a.75.75 0 0 0-1.5 0V10a4.5 4.5 0 0 1-9 0v-.357Z"
+	/>
+</svg>

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

@@ -47,7 +47,7 @@
 		class=" bg-gradient-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1] blur"
 		class=" bg-gradient-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1] blur"
 	></div>
 	></div>
 
 
-	<div class=" flex max-w-full w-full mx-auto px-5 pt-0.5 md:px-[1rem] bg-transparen">
+	<div class=" flex max-w-full w-full mx-auto px-2 pt-0.5 md:px-[1rem] bg-transparent">
 		<div class="flex items-center w-full max-w-full">
 		<div class="flex items-center w-full max-w-full">
 			<div
 			<div
 				class="{$showSidebar
 				class="{$showSidebar

+ 44 - 44
src/lib/components/layout/Navbar/Menu.svelte

@@ -103,7 +103,7 @@
 
 
 	<div slot="content">
 	<div slot="content">
 		<DropdownMenu.Content
 		<DropdownMenu.Content
-			class="w-full max-w-[200px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
+			class="w-full max-w-[200px] rounded-xl px-1 py-1.5  z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
 			sideOffset={8}
 			sideOffset={8}
 			side="bottom"
 			side="bottom"
 			align="end"
 			align="end"
@@ -152,6 +152,30 @@
 				</DropdownMenu.Item>
 				</DropdownMenu.Item>
 			{/if}
 			{/if}
 
 
+			{#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();
+					}}
+				>
+					<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.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"
 				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-overview-button"
 				id="chat-overview-button"
@@ -178,47 +202,6 @@
 				<div class="flex items-center">{$i18n.t('Artifacts')}</div>
 				<div class="flex items-center">{$i18n.t('Artifacts')}</div>
 			</DropdownMenu.Item>
 			</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-copy-button"
-				on:click={async () => {
-					const res = await copyToClipboard(await getChatAsText()).catch((e) => {
-						console.error(e);
-					});
-
-					if (res) {
-						toast.success($i18n.t('Copied to clipboard'));
-					}
-				}}
-			>
-				<Clipboard className=" size-4" strokeWidth="1.5" />
-				<div class="flex items-center">{$i18n.t('Copy')}</div>
-			</DropdownMenu.Item>
-
-			{#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();
-					}}
-				>
-					<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.Sub>
 				<DropdownMenu.SubTrigger
 				<DropdownMenu.SubTrigger
 					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"
 					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"
@@ -241,7 +224,7 @@
 					<div class="flex items-center">{$i18n.t('Download')}</div>
 					<div class="flex items-center">{$i18n.t('Download')}</div>
 				</DropdownMenu.SubTrigger>
 				</DropdownMenu.SubTrigger>
 				<DropdownMenu.SubContent
 				<DropdownMenu.SubContent
-					class="w-full rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
+					class="w-full rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
 					transition={flyAndScale}
 					transition={flyAndScale}
 					sideOffset={8}
 					sideOffset={8}
 				>
 				>
@@ -273,8 +256,25 @@
 				</DropdownMenu.SubContent>
 				</DropdownMenu.SubContent>
 			</DropdownMenu.Sub>
 			</DropdownMenu.Sub>
 
 
+			<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-copy-button"
+				on:click={async () => {
+					const res = await copyToClipboard(await getChatAsText()).catch((e) => {
+						console.error(e);
+					});
+
+					if (res) {
+						toast.success($i18n.t('Copied to clipboard'));
+					}
+				}}
+			>
+				<Clipboard className=" size-4" strokeWidth="1.5" />
+				<div class="flex items-center">{$i18n.t('Copy')}</div>
+			</DropdownMenu.Item>
+
 			{#if !$temporaryChatEnabled}
 			{#if !$temporaryChatEnabled}
-				<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
+				<hr class="border-gray-50 dark:border-gray-850 my-0.5" />
 
 
 				<div class="flex p-1">
 				<div class="flex p-1">
 					<Tags chatId={chat.id} />
 					<Tags chatId={chat.id} />

+ 238 - 117
src/lib/components/layout/Sidebar.svelte

@@ -1,5 +1,7 @@
 <script lang="ts">
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
+	import { v4 as uuidv4 } from 'uuid';
+
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
 	import {
 	import {
 		user,
 		user,
@@ -28,25 +30,25 @@
 		createNewChat,
 		createNewChat,
 		getPinnedChatList,
 		getPinnedChatList,
 		toggleChatPinnedStatusById,
 		toggleChatPinnedStatusById,
-		getChatPinnedStatusById
+		getChatPinnedStatusById,
+		getChatById,
+		updateChatFolderIdById,
+		importChat
 	} from '$lib/apis/chats';
 	} from '$lib/apis/chats';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 
 
 	import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
 	import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
 	import UserMenu from './Sidebar/UserMenu.svelte';
 	import UserMenu from './Sidebar/UserMenu.svelte';
 	import ChatItem from './Sidebar/ChatItem.svelte';
 	import ChatItem from './Sidebar/ChatItem.svelte';
-	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import Spinner from '../common/Spinner.svelte';
 	import Spinner from '../common/Spinner.svelte';
 	import Loader from '../common/Loader.svelte';
 	import Loader from '../common/Loader.svelte';
-	import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
 	import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
 	import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
-	import { select } from 'd3-selection';
 	import SearchInput from './Sidebar/SearchInput.svelte';
 	import SearchInput from './Sidebar/SearchInput.svelte';
-	import ChevronDown from '../icons/ChevronDown.svelte';
-	import ChevronUp from '../icons/ChevronUp.svelte';
-	import ChevronRight from '../icons/ChevronRight.svelte';
-	import Collapsible from '../common/Collapsible.svelte';
 	import Folder from '../common/Folder.svelte';
 	import Folder from '../common/Folder.svelte';
+	import Plus from '../icons/Plus.svelte';
+	import Tooltip from '../common/Tooltip.svelte';
+	import { createNewFolder, getFolders, updateFolderParentIdById } from '$lib/apis/folders';
+	import Folders from './Sidebar/Folders.svelte';
 
 
 	const BREAKPOINT = 768;
 	const BREAKPOINT = 768;
 
 
@@ -56,26 +58,105 @@
 	let shiftKey = false;
 	let shiftKey = false;
 
 
 	let selectedChatId = null;
 	let selectedChatId = null;
-	let deleteChat = null;
-
-	let showDeleteConfirm = false;
 	let showDropdown = false;
 	let showDropdown = false;
-
-	let selectedTagName = null;
-
 	let showPinnedChat = true;
 	let showPinnedChat = true;
 
 
 	// Pagination variables
 	// Pagination variables
 	let chatListLoading = false;
 	let chatListLoading = false;
 	let allChatsLoaded = false;
 	let allChatsLoaded = false;
 
 
+	let folders = {};
+
+	const initFolders = async () => {
+		const folderList = await getFolders(localStorage.token).catch((error) => {
+			toast.error(error);
+			return [];
+		});
+
+		folders = {};
+
+		// First pass: Initialize all folder entries
+		for (const folder of folderList) {
+			// Ensure folder is added to folders with its data
+			folders[folder.id] = { ...(folders[folder.id] || {}), ...folder };
+		}
+
+		// Second pass: Tie child folders to their parents
+		for (const folder of folderList) {
+			if (folder.parent_id) {
+				// Ensure the parent folder is initialized if it doesn't exist
+				if (!folders[folder.parent_id]) {
+					folders[folder.parent_id] = {}; // Create a placeholder if not already present
+				}
+
+				// Initialize childrenIds array if it doesn't exist and add the current folder id
+				folders[folder.parent_id].childrenIds = folders[folder.parent_id].childrenIds
+					? [...folders[folder.parent_id].childrenIds, folder.id]
+					: [folder.id];
+
+				// Sort the children by updated_at field
+				folders[folder.parent_id].childrenIds.sort((a, b) => {
+					return folders[b].updated_at - folders[a].updated_at;
+				});
+			}
+		}
+	};
+
+	const createFolder = async (name = 'Untitled') => {
+		if (name === '') {
+			toast.error($i18n.t('Folder name cannot be empty.'));
+			return;
+		}
+
+		const rootFolders = Object.values(folders).filter((folder) => folder.parent_id === null);
+		if (rootFolders.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) {
+			// If a folder with the same name already exists, append a number to the name
+			let i = 1;
+			while (
+				rootFolders.find((folder) => folder.name.toLowerCase() === `${name} ${i}`.toLowerCase())
+			) {
+				i++;
+			}
+
+			name = `${name} ${i}`;
+		}
+
+		// Add a dummy folder to the list to show the user that the folder is being created
+		const tempId = uuidv4();
+		folders = {
+			...folders,
+			tempId: {
+				id: tempId,
+				name: name,
+				created_at: Date.now(),
+				updated_at: Date.now()
+			}
+		};
+
+		const res = await createNewFolder(localStorage.token, name).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (res) {
+			await initFolders();
+		}
+	};
+
 	const initChatList = async () => {
 	const initChatList = async () => {
 		// Reset pagination variables
 		// Reset pagination variables
 		tags.set(await getAllTags(localStorage.token));
 		tags.set(await getAllTags(localStorage.token));
+		pinnedChats.set(await getPinnedChatList(localStorage.token));
+		initFolders();
 
 
 		currentChatPage.set(1);
 		currentChatPage.set(1);
 		allChatsLoaded = false;
 		allChatsLoaded = false;
-		await chats.set(await getChatList(localStorage.token, $currentChatPage));
+
+		if (search) {
+			await chats.set(await getChatListBySearchText(localStorage.token, search, $currentChatPage));
+		} else {
+			await chats.set(await getChatList(localStorage.token, $currentChatPage));
+		}
 
 
 		// Enable pagination
 		// Enable pagination
 		scrollPaginationEnabled.set(true);
 		scrollPaginationEnabled.set(true);
@@ -106,7 +187,6 @@
 	const searchDebounceHandler = async () => {
 	const searchDebounceHandler = async () => {
 		console.log('search', search);
 		console.log('search', search);
 		chats.set(null);
 		chats.set(null);
-		selectedTagName = null;
 
 
 		if (searchDebounceTimeout) {
 		if (searchDebounceTimeout) {
 			clearTimeout(searchDebounceTimeout);
 			clearTimeout(searchDebounceTimeout);
@@ -117,6 +197,7 @@
 			return;
 			return;
 		} else {
 		} else {
 			searchDebounceTimeout = setTimeout(async () => {
 			searchDebounceTimeout = setTimeout(async () => {
+				allChatsLoaded = false;
 				currentChatPage.set(1);
 				currentChatPage.set(1);
 				await chats.set(await getChatListBySearchText(localStorage.token, search));
 				await chats.set(await getChatListBySearchText(localStorage.token, search));
 
 
@@ -127,26 +208,16 @@
 		}
 		}
 	};
 	};
 
 
-	const deleteChatHandler = async (id) => {
-		const res = await deleteChatById(localStorage.token, id).catch((error) => {
-			toast.error(error);
-			return null;
-		});
-
-		if (res) {
-			tags.set(await getAllTags(localStorage.token));
-
-			if ($chatId === id) {
-				await chatId.set('');
-				await tick();
-				goto('/');
+	const importChatHandler = async (items, pinned = false, folderId = null) => {
+		console.log('importChatHandler', items, pinned, folderId);
+		for (const item of items) {
+			console.log(item);
+			if (item.chat) {
+				await importChat(localStorage.token, item.chat, item?.meta ?? {}, pinned, folderId);
 			}
 			}
-
-			allChatsLoaded = false;
-			currentChatPage.set(1);
-			await chats.set(await getChatList(localStorage.token, $currentChatPage));
-			await pinnedChats.set(await getPinnedChatList(localStorage.token));
 		}
 		}
+
+		initChatList();
 	};
 	};
 
 
 	const inputFilesHandler = async (files) => {
 	const inputFilesHandler = async (files) => {
@@ -158,18 +229,11 @@
 				const content = e.target.result;
 				const content = e.target.result;
 
 
 				try {
 				try {
-					const items = JSON.parse(content);
-
-					for (const item of items) {
-						if (item.chat) {
-							await createNewChat(localStorage.token, item.chat);
-						}
-					}
+					const chatItems = JSON.parse(content);
+					importChatHandler(chatItems);
 				} catch {
 				} catch {
 					toast.error($i18n.t(`Invalid file format.`));
 					toast.error($i18n.t(`Invalid file format.`));
 				}
 				}
-
-				initChatList();
 			};
 			};
 
 
 			reader.readAsText(file);
 			reader.readAsText(file);
@@ -179,29 +243,27 @@
 	const tagEventHandler = async (type, tagName, chatId) => {
 	const tagEventHandler = async (type, tagName, chatId) => {
 		console.log(type, tagName, chatId);
 		console.log(type, tagName, chatId);
 		if (type === 'delete') {
 		if (type === 'delete') {
-			currentChatPage.set(1);
-			await chats.set(await getChatListBySearchText(localStorage.token, search, $currentChatPage));
+			initChatList();
 		} else if (type === 'add') {
 		} else if (type === 'add') {
-			currentChatPage.set(1);
-			await chats.set(await getChatListBySearchText(localStorage.token, search, $currentChatPage));
+			initChatList();
 		}
 		}
 	};
 	};
 
 
-	let dragged = false;
+	let draggedOver = false;
 
 
 	const onDragOver = (e) => {
 	const onDragOver = (e) => {
 		e.preventDefault();
 		e.preventDefault();
 
 
-		// Check if a file is being dragged.
+		// Check if a file is being draggedOver.
 		if (e.dataTransfer?.types?.includes('Files')) {
 		if (e.dataTransfer?.types?.includes('Files')) {
-			dragged = true;
+			draggedOver = true;
 		} else {
 		} else {
-			dragged = false;
+			draggedOver = false;
 		}
 		}
 	};
 	};
 
 
 	const onDragLeave = () => {
 	const onDragLeave = () => {
-		dragged = false;
+		draggedOver = false;
 	};
 	};
 
 
 	const onDrop = async (e) => {
 	const onDrop = async (e) => {
@@ -218,7 +280,7 @@
 			}
 			}
 		}
 		}
 
 
-		dragged = false; // Reset dragged status after drop
+		draggedOver = false; // Reset draggedOver status after drop
 	};
 	};
 
 
 	let touchstart;
 	let touchstart;
@@ -284,7 +346,6 @@
 			localStorage.sidebar = value;
 			localStorage.sidebar = value;
 		});
 		});
 
 
-		await pinnedChats.set(await getPinnedChatList(localStorage.token));
 		await initChatList();
 		await initChatList();
 
 
 		window.addEventListener('keydown', onKeyDown);
 		window.addEventListener('keydown', onKeyDown);
@@ -324,23 +385,10 @@
 <ArchivedChatsModal
 <ArchivedChatsModal
 	bind:show={$showArchivedChats}
 	bind:show={$showArchivedChats}
 	on:change={async () => {
 	on:change={async () => {
-		await pinnedChats.set(await getPinnedChatList(localStorage.token));
 		await initChatList();
 		await initChatList();
 	}}
 	}}
 />
 />
 
 
-<DeleteConfirmDialog
-	bind:show={showDeleteConfirm}
-	title={$i18n.t('Delete chat?')}
-	on:confirm={() => {
-		deleteChatHandler(deleteChat.id);
-	}}
->
-	<div class=" text-sm text-gray-500 flex-1 line-clamp-3">
-		{$i18n.t('This will delete')} <span class="  font-semibold">{deleteChat.title}</span>.
-	</div>
-</DeleteConfirmDialog>
-
 <!-- svelte-ignore a11y-no-static-element-interactions -->
 <!-- svelte-ignore a11y-no-static-element-interactions -->
 
 
 {#if $showSidebar}
 {#if $showSidebar}
@@ -361,18 +409,6 @@
         "
         "
 	data-state={$showSidebar}
 	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
 	<div
 		class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] overflow-x-hidden z-50 {$showSidebar
 		class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] overflow-x-hidden z-50 {$showSidebar
 			? ''
 			? ''
@@ -381,7 +417,7 @@
 		<div class="px-2.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400">
 		<div class="px-2.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400">
 			<a
 			<a
 				id="sidebar-new-chat-button"
 				id="sidebar-new-chat-button"
-				class="flex flex-1 justify-between rounded-xl px-2 h-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
+				class="flex flex-1 justify-between rounded-lg px-2 h-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
 				href="/"
 				href="/"
 				draggable="false"
 				draggable="false"
 				on:click={async () => {
 				on:click={async () => {
@@ -425,7 +461,7 @@
 			</a>
 			</a>
 
 
 			<button
 			<button
-				class=" cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-900 transition"
+				class=" cursor-pointer px-2 py-2 flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 transition"
 				on:click={() => {
 				on:click={() => {
 					showSidebar.set(!$showSidebar);
 					showSidebar.set(!$showSidebar);
 				}}
 				}}
@@ -452,7 +488,7 @@
 		{#if $user?.role === 'admin'}
 		{#if $user?.role === 'admin'}
 			<div class="px-2.5 flex justify-center text-gray-800 dark:text-gray-200">
 			<div class="px-2.5 flex justify-center text-gray-800 dark:text-gray-200">
 				<a
 				<a
-					class="flex-grow flex space-x-3 rounded-xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
+					class="flex-grow flex space-x-3 rounded-lg px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
 					href="/workspace"
 					href="/workspace"
 					on:click={() => {
 					on:click={() => {
 						selectedChatId = null;
 						selectedChatId = null;
@@ -493,6 +529,19 @@
 				<div class="absolute z-40 w-full h-full flex justify-center"></div>
 				<div class="absolute z-40 w-full h-full flex justify-center"></div>
 			{/if}
 			{/if}
 
 
+			<div class="absolute z-40 right-4 top-1">
+				<Tooltip content={$i18n.t('New folder')}>
+					<button
+						class="p-1 rounded-lg bg-gray-50 hover:bg-gray-100 dark:bg-gray-950 dark:hover:bg-gray-900 transition"
+						on:click={() => {
+							createFolder();
+						}}
+					>
+						<Plus />
+					</button>
+				</Tooltip>
+			</div>
+
 			<SearchInput
 			<SearchInput
 				bind:value={search}
 				bind:value={search}
 				on:input={searchDebounceHandler}
 				on:input={searchDebounceHandler}
@@ -510,33 +559,60 @@
 			{/if}
 			{/if}
 
 
 			{#if !search && $pinnedChats.length > 0}
 			{#if !search && $pinnedChats.length > 0}
-				<div class=" flex flex-col space-y-1">
+				<div class="flex flex-col space-y-1 rounded-xl">
 					<Folder
 					<Folder
+						className="px-2"
 						bind:open={showPinnedChat}
 						bind:open={showPinnedChat}
 						on:change={(e) => {
 						on:change={(e) => {
 							localStorage.setItem('showPinnedChat', e.detail);
 							localStorage.setItem('showPinnedChat', e.detail);
 							console.log(e.detail);
 							console.log(e.detail);
 						}}
 						}}
+						on:import={(e) => {
+							importChatHandler(e.detail, true);
+						}}
 						on:drop={async (e) => {
 						on:drop={async (e) => {
-							const { id } = e.detail;
-
-							const status = await getChatPinnedStatusById(localStorage.token, id);
+							const { type, id } = e.detail;
+
+							if (type === 'chat') {
+								const chat = await getChatById(localStorage.token, id);
+
+								if (chat) {
+									console.log(chat);
+									if (chat.folder_id) {
+										const res = await updateChatFolderIdById(
+											localStorage.token,
+											chat.id,
+											null
+										).catch((error) => {
+											toast.error(error);
+											return null;
+										});
+
+										if (res) {
+											initChatList();
+										}
+									}
 
 
-							if (!status) {
-								const res = await toggleChatPinnedStatusById(localStorage.token, id);
+									if (!chat.pinned) {
+										const res = await toggleChatPinnedStatusById(localStorage.token, id);
 
 
-								if (res) {
-									await pinnedChats.set(await getPinnedChatList(localStorage.token));
-									initChatList();
+										if (res) {
+											initChatList();
+										}
+									}
 								}
 								}
 							}
 							}
 						}}
 						}}
 						name={$i18n.t('Pinned')}
 						name={$i18n.t('Pinned')}
 					>
 					>
-						<div class="pl-2 mt-0.5 flex flex-col overflow-y-auto scrollbar-hidden">
+						<div
+							class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900"
+						>
 							{#each $pinnedChats as chat, idx}
 							{#each $pinnedChats as chat, idx}
 								<ChatItem
 								<ChatItem
-									{chat}
+									className=""
+									id={chat.id}
+									title={chat.title}
 									{shiftKey}
 									{shiftKey}
 									selected={selectedChatId === chat.id}
 									selected={selectedChatId === chat.id}
 									on:select={() => {
 									on:select={() => {
@@ -545,13 +621,8 @@
 									on:unselect={() => {
 									on:unselect={() => {
 										selectedChatId = null;
 										selectedChatId = null;
 									}}
 									}}
-									on:delete={(e) => {
-										if ((e?.detail ?? '') === 'shift') {
-											deleteChatHandler(chat.id);
-										} else {
-											deleteChat = chat;
-											showDeleteConfirm = true;
-										}
+									on:change={async () => {
+										initChatList();
 									}}
 									}}
 									on:tag={(e) => {
 									on:tag={(e) => {
 										const { type, name } = e.detail;
 										const { type, name } = e.detail;
@@ -564,25 +635,78 @@
 				</div>
 				</div>
 			{/if}
 			{/if}
 
 
-			<div class="flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden">
+			<div class=" flex-1 flex flex-col overflow-y-auto scrollbar-hidden">
+				{#if !search && folders}
+					<Folders
+						{folders}
+						on:import={(e) => {
+							const { folderId, items } = e.detail;
+							importChatHandler(items, false, folderId);
+						}}
+						on:update={async (e) => {
+							initChatList();
+						}}
+						on:change={async () => {
+							initChatList();
+						}}
+					/>
+				{/if}
+
 				<Folder
 				<Folder
-					collapsible={false}
+					collapsible={!search}
+					className="px-2 mt-0.5"
+					name={$i18n.t('All chats')}
+					on:import={(e) => {
+						importChatHandler(e.detail);
+					}}
 					on:drop={async (e) => {
 					on:drop={async (e) => {
-						const { id } = e.detail;
+						const { type, id } = e.detail;
+
+						if (type === 'chat') {
+							const chat = await getChatById(localStorage.token, id);
+
+							if (chat) {
+								console.log(chat);
+								if (chat.folder_id) {
+									const res = await updateChatFolderIdById(localStorage.token, chat.id, null).catch(
+										(error) => {
+											toast.error(error);
+											return null;
+										}
+									);
 
 
-						const status = await getChatPinnedStatusById(localStorage.token, id);
+									if (res) {
+										initChatList();
+									}
+								}
 
 
-						if (status) {
-							const res = await toggleChatPinnedStatusById(localStorage.token, id);
+								if (chat.pinned) {
+									const res = await toggleChatPinnedStatusById(localStorage.token, id);
+
+									if (res) {
+										initChatList();
+									}
+								}
+							}
+						} else if (type === 'folder') {
+							if (folders[id].parent_id === null) {
+								return;
+							}
+
+							const res = await updateFolderParentIdById(localStorage.token, id, null).catch(
+								(error) => {
+									toast.error(error);
+									return null;
+								}
+							);
 
 
 							if (res) {
 							if (res) {
-								await pinnedChats.set(await getPinnedChatList(localStorage.token));
-								initChatList();
+								await initFolders();
 							}
 							}
 						}
 						}
 					}}
 					}}
 				>
 				>
-					<div class="pt-2 pl-2">
+					<div class="pt-1.5">
 						{#if $chats}
 						{#if $chats}
 							{#each $chats as chat, idx}
 							{#each $chats as chat, idx}
 								{#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
 								{#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
@@ -615,7 +739,9 @@
 								{/if}
 								{/if}
 
 
 								<ChatItem
 								<ChatItem
-									{chat}
+									className=""
+									id={chat.id}
+									title={chat.title}
 									{shiftKey}
 									{shiftKey}
 									selected={selectedChatId === chat.id}
 									selected={selectedChatId === chat.id}
 									on:select={() => {
 									on:select={() => {
@@ -624,13 +750,8 @@
 									on:unselect={() => {
 									on:unselect={() => {
 										selectedChatId = null;
 										selectedChatId = null;
 									}}
 									}}
-									on:delete={(e) => {
-										if ((e?.detail ?? '') === 'shift') {
-											deleteChatHandler(chat.id);
-										} else {
-											deleteChat = chat;
-											showDeleteConfirm = true;
-										}
+									on:change={async () => {
+										initChatList();
 									}}
 									}}
 									on:tag={(e) => {
 									on:tag={(e) => {
 										const { type, name } = e.detail;
 										const { type, name } = e.detail;

+ 88 - 58
src/lib/components/layout/Sidebar/ChatItem.svelte

@@ -28,13 +28,21 @@
 	} from '$lib/stores';
 	} from '$lib/stores';
 
 
 	import ChatMenu from './ChatMenu.svelte';
 	import ChatMenu from './ChatMenu.svelte';
+	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import ShareChatModal from '$lib/components/chat/ShareChatModal.svelte';
 	import ShareChatModal from '$lib/components/chat/ShareChatModal.svelte';
 	import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
 	import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
 	import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
 	import DragGhost from '$lib/components/common/DragGhost.svelte';
 	import DragGhost from '$lib/components/common/DragGhost.svelte';
+	import Check from '$lib/components/icons/Check.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
+	import Document from '$lib/components/icons/Document.svelte';
+
+	export let className = '';
+
+	export let id;
+	export let title;
 
 
-	export let chat;
 	export let selected = false;
 	export let selected = false;
 	export let shiftKey = false;
 	export let shiftKey = false;
 
 
@@ -43,7 +51,7 @@
 	let showShareChatModal = false;
 	let showShareChatModal = false;
 	let confirmEdit = false;
 	let confirmEdit = false;
 
 
-	let chatTitle = chat.title;
+	let chatTitle = title;
 
 
 	const editChatTitle = async (id, title) => {
 	const editChatTitle = async (id, title) => {
 		if (title === '') {
 		if (title === '') {
@@ -78,13 +86,27 @@
 		}
 		}
 	};
 	};
 
 
+	const deleteChatHandler = async (id) => {
+		const res = await deleteChatById(localStorage.token, id).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (res) {
+			tags.set(await getAllTags(localStorage.token));
+			if ($chatId === id) {
+				await chatId.set('');
+				await tick();
+				goto('/');
+			}
+
+			dispatch('change');
+		}
+	};
+
 	const archiveChatHandler = async (id) => {
 	const archiveChatHandler = async (id) => {
 		await archiveChatById(localStorage.token, id);
 		await archiveChatById(localStorage.token, id);
-		tags.set(await getAllTags(localStorage.token));
-
-		currentChatPage.set(1);
-		await chats.set(await getChatList(localStorage.token, $currentChatPage));
-		await pinnedChats.set(await getPinnedChatList(localStorage.token));
+		dispatch('change');
 	};
 	};
 
 
 	const focusEdit = async (node: HTMLInputElement) => {
 	const focusEdit = async (node: HTMLInputElement) => {
@@ -93,7 +115,7 @@
 
 
 	let itemElement;
 	let itemElement;
 
 
-	let drag = false;
+	let dragged = false;
 	let x = 0;
 	let x = 0;
 	let y = 0;
 	let y = 0;
 
 
@@ -102,28 +124,35 @@
 		'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
 		'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
 
 
 	const onDragStart = (event) => {
 	const onDragStart = (event) => {
+		event.stopPropagation();
+
 		event.dataTransfer.setDragImage(dragImage, 0, 0);
 		event.dataTransfer.setDragImage(dragImage, 0, 0);
 
 
 		// Set the data to be transferred
 		// Set the data to be transferred
 		event.dataTransfer.setData(
 		event.dataTransfer.setData(
 			'text/plain',
 			'text/plain',
 			JSON.stringify({
 			JSON.stringify({
-				id: chat.id
+				type: 'chat',
+				id: id
 			})
 			})
 		);
 		);
 
 
-		drag = true;
+		dragged = true;
 		itemElement.style.opacity = '0.5'; // Optional: Visual cue to show it's being dragged
 		itemElement.style.opacity = '0.5'; // Optional: Visual cue to show it's being dragged
 	};
 	};
 
 
 	const onDrag = (event) => {
 	const onDrag = (event) => {
+		event.stopPropagation();
+
 		x = event.clientX;
 		x = event.clientX;
 		y = event.clientY;
 		y = event.clientY;
 	};
 	};
 
 
 	const onDragEnd = (event) => {
 	const onDragEnd = (event) => {
+		event.stopPropagation();
+
 		itemElement.style.opacity = '1'; // Reset visual cue after drag
 		itemElement.style.opacity = '1'; // Reset visual cue after drag
-		drag = false;
+		dragged = false;
 	};
 	};
 
 
 	onMount(() => {
 	onMount(() => {
@@ -144,26 +173,42 @@
 			itemElement.removeEventListener('dragend', onDragEnd);
 			itemElement.removeEventListener('dragend', onDragEnd);
 		}
 		}
 	});
 	});
+
+	let showDeleteConfirm = false;
 </script>
 </script>
 
 
-<ShareChatModal bind:show={showShareChatModal} chatId={chat.id} />
+<ShareChatModal bind:show={showShareChatModal} chatId={id} />
+
+<DeleteConfirmDialog
+	bind:show={showDeleteConfirm}
+	title={$i18n.t('Delete chat?')}
+	on:confirm={() => {
+		deleteChatHandler(id);
+	}}
+>
+	<div class=" text-sm text-gray-500 flex-1 line-clamp-3">
+		{$i18n.t('This will delete')} <span class="  font-semibold">{title}</span>.
+	</div>
+</DeleteConfirmDialog>
 
 
-{#if drag && x && y}
+{#if dragged && x && y}
 	<DragGhost {x} {y}>
 	<DragGhost {x} {y}>
-		<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-44">
-			<div>
+		<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-fit max-w-40">
+			<div class="flex items-center gap-1">
+				<Document className=" size-[18px]" strokeWidth="2" />
 				<div class=" text-xs text-white line-clamp-1">
 				<div class=" text-xs text-white line-clamp-1">
-					{chat.title}
+					{title}
 				</div>
 				</div>
 			</div>
 			</div>
 		</div>
 		</div>
 	</DragGhost>
 	</DragGhost>
 {/if}
 {/if}
 
 
-<div bind:this={itemElement} class=" w-full pr-2 relative group" draggable="true">
+<div bind:this={itemElement} class=" w-full {className} relative group" draggable="true">
 	{#if confirmEdit}
 	{#if confirmEdit}
 		<div
 		<div
-			class=" w-full flex justify-between rounded-xl px-2.5 py-2 {chat.id === $chatId || confirmEdit
+			class=" w-full flex justify-between rounded-lg px-[11px] py-[6px] {id === $chatId ||
+			confirmEdit
 				? 'bg-gray-200 dark:bg-gray-900'
 				? 'bg-gray-200 dark:bg-gray-900'
 				: selected
 				: selected
 					? 'bg-gray-100 dark:bg-gray-950'
 					? 'bg-gray-100 dark:bg-gray-950'
@@ -177,12 +222,13 @@
 		</div>
 		</div>
 	{:else}
 	{:else}
 		<a
 		<a
-			class=" w-full flex justify-between rounded-lg px-2.5 py-2 {chat.id === $chatId || confirmEdit
+			class=" w-full flex justify-between rounded-lg px-[11px] py-[6px] {id === $chatId ||
+			confirmEdit
 				? 'bg-gray-200 dark:bg-gray-900'
 				? 'bg-gray-200 dark:bg-gray-900'
 				: selected
 				: selected
 					? 'bg-gray-100 dark:bg-gray-950'
 					? 'bg-gray-100 dark:bg-gray-950'
 					: ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'}  whitespace-nowrap text-ellipsis"
 					: ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'}  whitespace-nowrap text-ellipsis"
-			href="/c/{chat.id}"
+			href="/c/{id}"
 			on:click={() => {
 			on:click={() => {
 				dispatch('select');
 				dispatch('select');
 
 
@@ -191,7 +237,7 @@
 				}
 				}
 			}}
 			}}
 			on:dblclick={() => {
 			on:dblclick={() => {
-				chatTitle = chat.title;
+				chatTitle = title;
 				confirmEdit = true;
 				confirmEdit = true;
 			}}
 			}}
 			on:mouseenter={(e) => {
 			on:mouseenter={(e) => {
@@ -205,7 +251,7 @@
 		>
 		>
 			<div class=" flex self-center flex-1 w-full">
 			<div class=" flex self-center flex-1 w-full">
 				<div class=" text-left self-center overflow-hidden w-full h-[20px]">
 				<div class=" text-left self-center overflow-hidden w-full h-[20px]">
-					{chat.title}
+					{title}
 				</div>
 				</div>
 			</div>
 			</div>
 		</a>
 		</a>
@@ -214,12 +260,14 @@
 	<!-- svelte-ignore a11y-no-static-element-interactions -->
 	<!-- svelte-ignore a11y-no-static-element-interactions -->
 	<div
 	<div
 		class="
 		class="
-        {chat.id === $chatId || confirmEdit
+        {id === $chatId || confirmEdit
 			? 'from-gray-200 dark:from-gray-900'
 			? 'from-gray-200 dark:from-gray-900'
 			: selected
 			: selected
 				? 'from-gray-100 dark:from-gray-950'
 				? 'from-gray-100 dark:from-gray-950'
 				: 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
 				: 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
-            absolute right-[10px] top-[6px] py-1 pr-2 pl-5 bg-gradient-to-l from-80%
+            absolute {className === 'pr-2'
+			? 'right-[8px]'
+			: 'right-0'}  top-[4px] py-1 pr-0.5 mr-1.5 pl-5 bg-gradient-to-l from-80%
 
 
               to-transparent"
               to-transparent"
 		on:mouseenter={(e) => {
 		on:mouseenter={(e) => {
@@ -230,28 +278,19 @@
 		}}
 		}}
 	>
 	>
 		{#if confirmEdit}
 		{#if confirmEdit}
-			<div class="flex self-center space-x-1.5 z-10">
+			<div
+				class="flex self-center items-center space-x-1.5 z-10 translate-y-[0.5px] -translate-x-[0.5px]"
+			>
 				<Tooltip content={$i18n.t('Confirm')}>
 				<Tooltip content={$i18n.t('Confirm')}>
 					<button
 					<button
 						class=" self-center dark:hover:text-white transition"
 						class=" self-center dark:hover:text-white transition"
 						on:click={() => {
 						on:click={() => {
-							editChatTitle(chat.id, chatTitle);
+							editChatTitle(id, chatTitle);
 							confirmEdit = false;
 							confirmEdit = false;
 							chatTitle = '';
 							chatTitle = '';
 						}}
 						}}
 					>
 					>
-						<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="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
-								clip-rule="evenodd"
-							/>
-						</svg>
+						<Check className=" size-3.5" strokeWidth="2.5" />
 					</button>
 					</button>
 				</Tooltip>
 				</Tooltip>
 
 
@@ -263,16 +302,7 @@
 							chatTitle = '';
 							chatTitle = '';
 						}}
 						}}
 					>
 					>
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 20 20"
-							fill="currentColor"
-							class="w-4 h-4"
-						>
-							<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>
+						<XMark strokeWidth="2.5" />
 					</button>
 					</button>
 				</Tooltip>
 				</Tooltip>
 			</div>
 			</div>
@@ -282,7 +312,7 @@
 					<button
 					<button
 						class=" self-center dark:hover:text-white transition"
 						class=" self-center dark:hover:text-white transition"
 						on:click={() => {
 						on:click={() => {
-							archiveChatHandler(chat.id);
+							archiveChatHandler(id);
 						}}
 						}}
 						type="button"
 						type="button"
 					>
 					>
@@ -294,7 +324,7 @@
 					<button
 					<button
 						class=" self-center dark:hover:text-white transition"
 						class=" self-center dark:hover:text-white transition"
 						on:click={() => {
 						on:click={() => {
-							dispatch('delete', 'shift');
+							deleteChatHandler(id);
 						}}
 						}}
 						type="button"
 						type="button"
 					>
 					>
@@ -305,29 +335,29 @@
 		{:else}
 		{:else}
 			<div class="flex self-center space-x-1 z-10">
 			<div class="flex self-center space-x-1 z-10">
 				<ChatMenu
 				<ChatMenu
-					chatId={chat.id}
+					chatId={id}
 					cloneChatHandler={() => {
 					cloneChatHandler={() => {
-						cloneChatHandler(chat.id);
+						cloneChatHandler(id);
 					}}
 					}}
 					shareHandler={() => {
 					shareHandler={() => {
 						showShareChatModal = true;
 						showShareChatModal = true;
 					}}
 					}}
 					archiveChatHandler={() => {
 					archiveChatHandler={() => {
-						archiveChatHandler(chat.id);
+						archiveChatHandler(id);
 					}}
 					}}
 					renameHandler={() => {
 					renameHandler={() => {
-						chatTitle = chat.title;
+						chatTitle = title;
 
 
 						confirmEdit = true;
 						confirmEdit = true;
 					}}
 					}}
 					deleteHandler={() => {
 					deleteHandler={() => {
-						dispatch('delete');
+						showDeleteConfirm = true;
 					}}
 					}}
 					onClose={() => {
 					onClose={() => {
 						dispatch('unselect');
 						dispatch('unselect');
 					}}
 					}}
 					on:change={async () => {
 					on:change={async () => {
-						await pinnedChats.set(await getPinnedChatList(localStorage.token));
+						dispatch('change');
 					}}
 					}}
 					on:tag={(e) => {
 					on:tag={(e) => {
 						dispatch('tag', e.detail);
 						dispatch('tag', e.detail);
@@ -353,13 +383,13 @@
 					</button>
 					</button>
 				</ChatMenu>
 				</ChatMenu>
 
 
-				{#if chat.id === $chatId}
+				{#if id === $chatId}
 					<!-- Shortcut support using "delete-chat-button" id -->
 					<!-- Shortcut support using "delete-chat-button" id -->
 					<button
 					<button
 						id="delete-chat-button"
 						id="delete-chat-button"
 						class="hidden"
 						class="hidden"
 						on:click={() => {
 						on:click={() => {
-							dispatch('delete');
+							showDeleteConfirm = true;
 						}}
 						}}
 					>
 					>
 						<svg
 						<svg

+ 123 - 9
src/lib/components/layout/Sidebar/ChatMenu.svelte

@@ -3,6 +3,9 @@
 	import { flyAndScale } from '$lib/utils/transitions';
 	import { flyAndScale } from '$lib/utils/transitions';
 	import { getContext, createEventDispatcher } from 'svelte';
 	import { getContext, createEventDispatcher } from 'svelte';
 
 
+	import fileSaver from 'file-saver';
+	const { saveAs } = fileSaver;
+
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
 	import Dropdown from '$lib/components/common/Dropdown.svelte';
 	import Dropdown from '$lib/components/common/Dropdown.svelte';
@@ -15,8 +18,15 @@
 	import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
 	import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
 	import Bookmark from '$lib/components/icons/Bookmark.svelte';
 	import Bookmark from '$lib/components/icons/Bookmark.svelte';
 	import BookmarkSlash from '$lib/components/icons/BookmarkSlash.svelte';
 	import BookmarkSlash from '$lib/components/icons/BookmarkSlash.svelte';
-	import { getChatPinnedStatusById, toggleChatPinnedStatusById } from '$lib/apis/chats';
+	import {
+		getChatById,
+		getChatPinnedStatusById,
+		toggleChatPinnedStatusById
+	} from '$lib/apis/chats';
 	import { chats } from '$lib/stores';
 	import { chats } from '$lib/stores';
+	import { createMessagesList } from '$lib/utils';
+	import { downloadChatAsPDF } from '$lib/apis/utils';
+	import Download from '$lib/components/icons/Download.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -41,6 +51,70 @@
 		pinned = await getChatPinnedStatusById(localStorage.token, chatId);
 		pinned = await getChatPinnedStatusById(localStorage.token, chatId);
 	};
 	};
 
 
+	const getChatAsText = async (chat) => {
+		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`;
+		}, '');
+
+		return chatText.trim();
+	};
+
+	const downloadTxt = async () => {
+		const chat = await getChatById(localStorage.token, chatId);
+		if (!chat) {
+			return;
+		}
+
+		const chatText = await getChatAsText(chat);
+		let blob = new Blob([chatText], {
+			type: 'text/plain'
+		});
+
+		saveAs(blob, `chat-${chat.chat.title}.txt`);
+	};
+
+	const downloadPdf = async () => {
+		const chat = await getChatById(localStorage.token, chatId);
+		if (!chat) {
+			return;
+		}
+
+		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);
+
+		// Create a link element to trigger the download
+		const a = document.createElement('a');
+		a.href = url;
+		a.download = `chat-${chat.chat.title}.pdf`;
+
+		// Append the link to the body and click it programmatically
+		document.body.appendChild(a);
+		a.click();
+
+		// Remove the link from the body
+		document.body.removeChild(a);
+
+		// Revoke the URL to release memory
+		window.URL.revokeObjectURL(url);
+	};
+
+	const downloadJSONExport = async () => {
+		const chat = await getChatById(localStorage.token, chatId);
+
+		if (chat) {
+			let blob = new Blob([JSON.stringify([chat])], {
+				type: 'application/json'
+			});
+			saveAs(blob, `chat-export-${Date.now()}.json`);
+		}
+	};
+
 	$: if (show) {
 	$: if (show) {
 		checkPinned();
 		checkPinned();
 	}
 	}
@@ -60,14 +134,14 @@
 
 
 	<div slot="content">
 	<div slot="content">
 		<DropdownMenu.Content
 		<DropdownMenu.Content
-			class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
+			class="w-full max-w-[200px] rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-xl"
 			sideOffset={-2}
 			sideOffset={-2}
 			side="bottom"
 			side="bottom"
 			align="start"
 			align="start"
 			transition={flyAndScale}
 			transition={flyAndScale}
 		>
 		>
 			<DropdownMenu.Item
 			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+				class="flex gap-2 items-center px-3 py-1.5 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
 				on:click={() => {
 				on:click={() => {
 					pinHandler();
 					pinHandler();
 				}}
 				}}
@@ -82,7 +156,7 @@
 			</DropdownMenu.Item>
 			</DropdownMenu.Item>
 
 
 			<DropdownMenu.Item
 			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+				class="flex gap-2 items-center px-3 py-1.5 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
 				on:click={() => {
 				on:click={() => {
 					renameHandler();
 					renameHandler();
 				}}
 				}}
@@ -92,7 +166,7 @@
 			</DropdownMenu.Item>
 			</DropdownMenu.Item>
 
 
 			<DropdownMenu.Item
 			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+				class="flex gap-2 items-center px-3 py-1.5 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
 				on:click={() => {
 				on:click={() => {
 					cloneChatHandler();
 					cloneChatHandler();
 				}}
 				}}
@@ -102,7 +176,7 @@
 			</DropdownMenu.Item>
 			</DropdownMenu.Item>
 
 
 			<DropdownMenu.Item
 			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+				class="flex gap-2 items-center px-3 py-1.5 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
 				on:click={() => {
 				on:click={() => {
 					archiveChatHandler();
 					archiveChatHandler();
 				}}
 				}}
@@ -112,7 +186,7 @@
 			</DropdownMenu.Item>
 			</DropdownMenu.Item>
 
 
 			<DropdownMenu.Item
 			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-md"
+				class="flex gap-2 items-center px-3 py-1.5 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-md"
 				on:click={() => {
 				on:click={() => {
 					shareHandler();
 					shareHandler();
 				}}
 				}}
@@ -121,8 +195,48 @@
 				<div class="flex items-center">{$i18n.t('Share')}</div>
 				<div class="flex items-center">{$i18n.t('Share')}</div>
 			</DropdownMenu.Item>
 			</DropdownMenu.Item>
 
 
+			<DropdownMenu.Sub>
+				<DropdownMenu.SubTrigger
+					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"
+				>
+					<Download strokeWidth="2" />
+
+					<div class="flex items-center">{$i18n.t('Download')}</div>
+				</DropdownMenu.SubTrigger>
+				<DropdownMenu.SubContent
+					class="w-full rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
+					transition={flyAndScale}
+					sideOffset={8}
+				>
+					<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"
+						on:click={() => {
+							downloadJSONExport();
+						}}
+					>
+						<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</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"
+						on:click={() => {
+							downloadTxt();
+						}}
+					>
+						<div class="flex items-center line-clamp-1">{$i18n.t('Plain text (.txt)')}</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"
+						on:click={() => {
+							downloadPdf();
+						}}
+					>
+						<div class="flex items-center line-clamp-1">{$i18n.t('PDF document (.pdf)')}</div>
+					</DropdownMenu.Item>
+				</DropdownMenu.SubContent>
+			</DropdownMenu.Sub>
 			<DropdownMenu.Item
 			<DropdownMenu.Item
-				class="flex  gap-2  items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+				class="flex  gap-2  items-center px-3 py-1.5 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
 				on:click={() => {
 				on:click={() => {
 					deleteHandler();
 					deleteHandler();
 				}}
 				}}
@@ -131,7 +245,7 @@
 				<div class="flex items-center">{$i18n.t('Delete')}</div>
 				<div class="flex items-center">{$i18n.t('Delete')}</div>
 			</DropdownMenu.Item>
 			</DropdownMenu.Item>
 
 
-			<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
+			<hr class="border-gray-50 dark:border-gray-850 my-0.5" />
 
 
 			<div class="flex p-1">
 			<div class="flex p-1">
 				<Tags
 				<Tags

+ 35 - 0
src/lib/components/layout/Sidebar/Folders.svelte

@@ -0,0 +1,35 @@
+<script lang="ts">
+	import { createEventDispatcher } from 'svelte';
+
+	const dispatch = createEventDispatcher();
+	import RecursiveFolder from './RecursiveFolder.svelte';
+	export let folders = {};
+
+	let folderList = [];
+	// Get the list of folders that have no parent, sorted by name alphabetically
+	$: folderList = Object.keys(folders)
+		.filter((key) => folders[key].parent_id === null)
+		.sort((a, b) =>
+			folders[a].name.localeCompare(folders[b].name, undefined, {
+				numeric: true,
+				sensitivity: 'base'
+			})
+		);
+</script>
+
+{#each folderList as folderId (folderId)}
+	<RecursiveFolder
+		className="px-2"
+		{folders}
+		{folderId}
+		on:import={(e) => {
+			dispatch('import', e.detail);
+		}}
+		on:update={(e) => {
+			dispatch('update', e.detail);
+		}}
+		on:change={(e) => {
+			dispatch('change', e.detail);
+		}}
+	/>
+{/each}

+ 70 - 0
src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte

@@ -0,0 +1,70 @@
+<script lang="ts">
+	import { DropdownMenu } from 'bits-ui';
+	import { flyAndScale } from '$lib/utils/transitions';
+	import { getContext, createEventDispatcher } from 'svelte';
+
+	const i18n = getContext('i18n');
+	const dispatch = createEventDispatcher();
+
+	import Dropdown from '$lib/components/common/Dropdown.svelte';
+	import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
+	import Pencil from '$lib/components/icons/Pencil.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import Download from '$lib/components/icons/Download.svelte';
+
+	let show = false;
+</script>
+
+<Dropdown
+	bind:show
+	on:change={(e) => {
+		if (e.detail === false) {
+			dispatch('close');
+		}
+	}}
+>
+	<Tooltip content={$i18n.t('More')}>
+		<slot />
+	</Tooltip>
+
+	<div slot="content">
+		<DropdownMenu.Content
+			class="w-full max-w-[160px] rounded-lg px-1 py-1.5  z-50 bg-white dark:bg-gray-850 dark:text-white shadow-xl"
+			sideOffset={-2}
+			side="bottom"
+			align="start"
+			transition={flyAndScale}
+		>
+			<DropdownMenu.Item
+				class="flex gap-2 items-center px-3 py-1.5 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+				on:click={() => {
+					dispatch('rename');
+				}}
+			>
+				<Pencil strokeWidth="2" />
+				<div class="flex items-center">{$i18n.t('Rename')}</div>
+			</DropdownMenu.Item>
+
+			<DropdownMenu.Item
+				class="flex gap-2 items-center px-3 py-1.5 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+				on:click={() => {
+					dispatch('export');
+				}}
+			>
+				<Download strokeWidth="2" />
+
+				<div class="flex items-center">{$i18n.t('Export')}</div>
+			</DropdownMenu.Item>
+
+			<DropdownMenu.Item
+				class="flex  gap-2  items-center px-3 py-1.5 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+				on:click={() => {
+					dispatch('delete');
+				}}
+			>
+				<GarbageBin strokeWidth="2" />
+				<div class="flex items-center">{$i18n.t('Delete')}</div>
+			</DropdownMenu.Item>
+		</DropdownMenu.Content>
+	</div>
+</Dropdown>

+ 482 - 0
src/lib/components/layout/Sidebar/RecursiveFolder.svelte

@@ -0,0 +1,482 @@
+<script>
+	import { getContext, createEventDispatcher, onMount, onDestroy, tick } from 'svelte';
+
+	const i18n = getContext('i18n');
+	const dispatch = createEventDispatcher();
+
+	import DOMPurify from 'dompurify';
+	import fileSaver from 'file-saver';
+	const { saveAs } = fileSaver;
+
+	import ChevronDown from '../../icons/ChevronDown.svelte';
+	import ChevronRight from '../../icons/ChevronRight.svelte';
+	import Collapsible from '../../common/Collapsible.svelte';
+	import DragGhost from '$lib/components/common/DragGhost.svelte';
+
+	import FolderOpen from '$lib/components/icons/FolderOpen.svelte';
+	import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
+	import {
+		deleteFolderById,
+		updateFolderIsExpandedById,
+		updateFolderNameById,
+		updateFolderParentIdById
+	} from '$lib/apis/folders';
+	import { toast } from 'svelte-sonner';
+	import { getChatsByFolderId, updateChatFolderIdById } from '$lib/apis/chats';
+	import ChatItem from './ChatItem.svelte';
+	import FolderMenu from './Folders/FolderMenu.svelte';
+	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
+
+	export let open = false;
+
+	export let folders;
+	export let folderId;
+
+	export let className = '';
+
+	export let parentDragged = false;
+
+	let folderElement;
+
+	let edit = false;
+
+	let draggedOver = false;
+	let dragged = false;
+
+	let name = '';
+
+	const onDragOver = (e) => {
+		e.preventDefault();
+		e.stopPropagation();
+		if (dragged || parentDragged) {
+			return;
+		}
+		draggedOver = true;
+	};
+
+	const onDrop = async (e) => {
+		e.preventDefault();
+		e.stopPropagation();
+		if (dragged || parentDragged) {
+			return;
+		}
+
+		if (folderElement.contains(e.target)) {
+			console.log('Dropped on the Button');
+
+			if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
+				// Iterate over all items in the DataTransferItemList use functional programming
+				for (const item of Array.from(e.dataTransfer.items)) {
+					// If dropped items aren't files, reject them
+					if (item.kind === 'file') {
+						const file = item.getAsFile();
+						if (file && file.type === 'application/json') {
+							console.log('Dropped file is a JSON file!');
+
+							// Read the JSON file with FileReader
+							const reader = new FileReader();
+							reader.onload = async function (event) {
+								try {
+									const fileContent = JSON.parse(event.target.result);
+									open = true;
+									dispatch('import', {
+										folderId: folderId,
+										items: fileContent
+									});
+								} catch (error) {
+									console.error('Error parsing JSON file:', error);
+								}
+							};
+
+							// Start reading the file
+							reader.readAsText(file);
+						} else {
+							console.error('Only JSON file types are supported.');
+						}
+
+						console.log(file);
+					} else {
+						// Handle the drag-and-drop data for folders or chats (same as before)
+						const dataTransfer = e.dataTransfer.getData('text/plain');
+						const data = JSON.parse(dataTransfer);
+						console.log(data);
+
+						const { type, id } = data;
+
+						if (type === 'folder') {
+							open = true;
+							if (id === folderId) {
+								return;
+							}
+							// Move the folder
+							const res = await updateFolderParentIdById(localStorage.token, id, folderId).catch(
+								(error) => {
+									toast.error(error);
+									return null;
+								}
+							);
+
+							if (res) {
+								dispatch('update');
+							}
+						} else if (type === 'chat') {
+							open = true;
+
+							// Move the chat
+							const res = await updateChatFolderIdById(localStorage.token, id, folderId).catch(
+								(error) => {
+									toast.error(error);
+									return null;
+								}
+							);
+
+							if (res) {
+								dispatch('update');
+							}
+						}
+					}
+				}
+			}
+
+			draggedOver = false;
+		}
+	};
+
+	const onDragLeave = (e) => {
+		e.preventDefault();
+		if (dragged || parentDragged) {
+			return;
+		}
+
+		draggedOver = false;
+	};
+
+	const dragImage = new Image();
+	dragImage.src =
+		'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
+
+	let x;
+	let y;
+
+	const onDragStart = (event) => {
+		event.stopPropagation();
+		event.dataTransfer.setDragImage(dragImage, 0, 0);
+
+		// Set the data to be transferred
+		event.dataTransfer.setData(
+			'text/plain',
+			JSON.stringify({
+				type: 'folder',
+				id: folderId
+			})
+		);
+
+		dragged = true;
+		folderElement.style.opacity = '0.5'; // Optional: Visual cue to show it's being dragged
+	};
+
+	const onDrag = (event) => {
+		event.stopPropagation();
+
+		x = event.clientX;
+		y = event.clientY;
+	};
+
+	const onDragEnd = (event) => {
+		event.stopPropagation();
+
+		folderElement.style.opacity = '1'; // Reset visual cue after drag
+		dragged = false;
+	};
+
+	onMount(() => {
+		open = folders[folderId].is_expanded;
+		if (folderElement) {
+			folderElement.addEventListener('dragover', onDragOver);
+			folderElement.addEventListener('drop', onDrop);
+			folderElement.addEventListener('dragleave', onDragLeave);
+
+			// Event listener for when dragging starts
+			folderElement.addEventListener('dragstart', onDragStart);
+			// Event listener for when dragging occurs (optional)
+			folderElement.addEventListener('drag', onDrag);
+			// Event listener for when dragging ends
+			folderElement.addEventListener('dragend', onDragEnd);
+		}
+	});
+
+	onDestroy(() => {
+		if (folderElement) {
+			folderElement.addEventListener('dragover', onDragOver);
+			folderElement.removeEventListener('drop', onDrop);
+			folderElement.removeEventListener('dragleave', onDragLeave);
+
+			folderElement.removeEventListener('dragstart', onDragStart);
+			folderElement.removeEventListener('drag', onDrag);
+			folderElement.removeEventListener('dragend', onDragEnd);
+		}
+	});
+
+	let showDeleteConfirm = false;
+
+	const deleteHandler = async () => {
+		const res = await deleteFolderById(localStorage.token, folderId).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (res) {
+			toast.success($i18n.t('Folder deleted successfully'));
+			dispatch('update');
+		}
+	};
+
+	const nameUpdateHandler = async () => {
+		if (name === '') {
+			toast.error($i18n.t('Folder name cannot be empty'));
+			return;
+		}
+
+		if (name === folders[folderId].name) {
+			edit = false;
+			return;
+		}
+
+		const currentName = folders[folderId].name;
+
+		name = name.trim();
+		folders[folderId].name = name;
+
+		const res = await updateFolderNameById(localStorage.token, folderId, name).catch((error) => {
+			toast.error(error);
+
+			folders[folderId].name = currentName;
+			return null;
+		});
+
+		if (res) {
+			folders[folderId].name = name;
+			toast.success($i18n.t('Folder name updated successfully'));
+			dispatch('update');
+		}
+	};
+
+	const isExpandedUpdateHandler = async () => {
+		const res = await updateFolderIsExpandedById(localStorage.token, folderId, open).catch(
+			(error) => {
+				toast.error(error);
+				return null;
+			}
+		);
+	};
+
+	let isExpandedUpdateTimeout;
+
+	const isExpandedUpdateDebounceHandler = (open) => {
+		clearTimeout(isExpandedUpdateTimeout);
+		isExpandedUpdateTimeout = setTimeout(() => {
+			isExpandedUpdateHandler();
+		}, 500);
+	};
+
+	$: isExpandedUpdateDebounceHandler(open);
+
+	const editHandler = async () => {
+		console.log('Edit');
+		await tick();
+		name = folders[folderId].name;
+		edit = true;
+
+		await tick();
+
+		// focus on the input
+		setTimeout(() => {
+			const input = document.getElementById(`folder-${folderId}-input`);
+			input.focus();
+		}, 100);
+	};
+
+	const exportHandler = async () => {
+		const chats = await getChatsByFolderId(localStorage.token, folderId).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+		if (!chats) {
+			return;
+		}
+
+		const blob = new Blob([JSON.stringify(chats)], {
+			type: 'application/json'
+		});
+
+		saveAs(blob, `folder-${folders[folderId].name}-export-${Date.now()}.json`);
+	};
+</script>
+
+<DeleteConfirmDialog
+	bind:show={showDeleteConfirm}
+	title={$i18n.t('Delete folder?')}
+	on:confirm={() => {
+		deleteHandler();
+	}}
+>
+	<div class=" text-sm text-gray-700 dark:text-gray-300 flex-1 line-clamp-3">
+		{@html DOMPurify.sanitize(
+			$i18n.t('This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.', {
+				NAME: folders[folderId].name
+			})
+		)}
+	</div>
+</DeleteConfirmDialog>
+
+{#if dragged && x && y}
+	<DragGhost {x} {y}>
+		<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-fit max-w-40">
+			<div class="flex items-center gap-1">
+				<FolderOpen className="size-3.5" strokeWidth="2" />
+				<div class=" text-xs text-white line-clamp-1">
+					{folders[folderId].name}
+				</div>
+			</div>
+		</div>
+	</DragGhost>
+{/if}
+
+<div bind:this={folderElement} class="relative {className}" draggable="true">
+	{#if draggedOver}
+		<div
+			class="absolute top-0 left-0 w-full h-full rounded-sm bg-[hsla(260,85%,65%,0.1)] bg-opacity-50 dark:bg-opacity-10 z-50 pointer-events-none touch-none"
+		></div>
+	{/if}
+
+	<Collapsible
+		bind:open
+		className="w-full"
+		buttonClassName="w-full"
+		hide={(folders[folderId]?.childrenIds ?? []).length === 0 &&
+			(folders[folderId].items?.chats ?? []).length === 0}
+		on:change={(e) => {
+			dispatch('open', e.detail);
+		}}
+	>
+		<!-- svelte-ignore a11y-no-static-element-interactions -->
+		<div class="w-full group">
+			<button
+				id="folder-{folderId}-button"
+				class="relative w-full py-1.5 px-2 rounded-md flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
+				on:dblclick={() => {
+					editHandler();
+				}}
+			>
+				<div class="text-gray-300 dark:text-gray-600">
+					{#if open}
+						<ChevronDown className=" size-3" strokeWidth="2.5" />
+					{:else}
+						<ChevronRight className=" size-3" strokeWidth="2.5" />
+					{/if}
+				</div>
+
+				<div class="translate-y-[0.5px] flex-1 justify-start text-start line-clamp-1">
+					{#if edit}
+						<input
+							id="folder-{folderId}-input"
+							type="text"
+							bind:value={name}
+							on:blur={() => {
+								nameUpdateHandler();
+								edit = false;
+							}}
+							on:click={(e) => {
+								// Prevent accidental collapse toggling when clicking inside input
+								e.stopPropagation();
+							}}
+							on:mousedown={(e) => {
+								// Prevent accidental collapse toggling when clicking inside input
+								e.stopPropagation();
+							}}
+							on:keydown={(e) => {
+								if (e.key === 'Enter') {
+									edit = false;
+								}
+							}}
+							class="w-full h-full bg-transparent text-gray-500 dark:text-gray-500 outline-none"
+						/>
+					{:else}
+						{folders[folderId].name}
+					{/if}
+				</div>
+
+				<button
+					class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
+					on:pointerup={(e) => {
+						e.stopPropagation();
+					}}
+				>
+					<FolderMenu
+						on:rename={() => {
+							editHandler();
+						}}
+						on:delete={() => {
+							showDeleteConfirm = true;
+						}}
+						on:export={() => {
+							exportHandler();
+						}}
+					>
+						<button class="p-0.5 dark:hover:bg-gray-850 rounded-lg touch-auto" on:click={(e) => {}}>
+							<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
+						</button>
+					</FolderMenu>
+				</button>
+			</button>
+		</div>
+
+		<div slot="content" class="w-full">
+			{#if (folders[folderId]?.childrenIds ?? []).length > 0 || (folders[folderId].items?.chats ?? []).length > 0}
+				<div
+					class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900"
+				>
+					{#if folders[folderId]?.childrenIds}
+						{@const children = folders[folderId]?.childrenIds
+							.map((id) => folders[id])
+							.sort((a, b) =>
+								a.name.localeCompare(b.name, undefined, {
+									numeric: true,
+									sensitivity: 'base'
+								})
+							)}
+
+						{#each children as childFolder (`${folderId}-${childFolder.id}`)}
+							<svelte:self
+								{folders}
+								folderId={childFolder.id}
+								parentDragged={dragged}
+								on:import={(e) => {
+									dispatch('import', e.detail);
+								}}
+								on:update={(e) => {
+									dispatch('update', e.detail);
+								}}
+								on:change={(e) => {
+									dispatch('change', e.detail);
+								}}
+							/>
+						{/each}
+					{/if}
+
+					{#if folders[folderId].items?.chats}
+						{#each folders[folderId].items.chats as chat (chat.id)}
+							<ChatItem
+								id={chat.id}
+								title={chat.title}
+								on:change={(e) => {
+									dispatch('change', e.detail);
+								}}
+							/>
+						{/each}
+					{/if}
+				</div>
+			{/if}
+		</div>
+	</Collapsible>
+</div>

+ 14 - 4
src/lib/components/layout/Sidebar/SearchInput.svelte

@@ -30,7 +30,13 @@
 
 
 	let filteredTags = [];
 	let filteredTags = [];
 	$: filteredTags = lastWord.startsWith('tag:')
 	$: filteredTags = lastWord.startsWith('tag:')
-		? $tags.filter((tag) => {
+		? [
+				...$tags,
+				{
+					id: 'none',
+					name: $i18n.t('Untagged')
+				}
+			].filter((tag) => {
 				const tagName = lastWord.slice(4);
 				const tagName = lastWord.slice(4);
 				if (tagName) {
 				if (tagName) {
 					const tagId = tagName.replace(' ', '_').toLowerCase();
 					const tagId = tagName.replace(' ', '_').toLowerCase();
@@ -144,7 +150,7 @@
 				{#if filteredTags.length > 0}
 				{#if filteredTags.length > 0}
 					<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1">Tags</div>
 					<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1">Tags</div>
 
 
-					<div class="">
+					<div class="max-h-60 overflow-auto">
 						{#each filteredTags as tag, tagIdx}
 						{#each filteredTags as tag, tagIdx}
 							<button
 							<button
 								class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx ===
 								class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx ===
@@ -163,7 +169,11 @@
 									dispatch('input');
 									dispatch('input');
 								}}
 								}}
 							>
 							>
-								<div class="dark:text-gray-300 text-gray-700 font-medium">{tag.name}</div>
+								<div
+									class="dark:text-gray-300 text-gray-700 font-medium line-clamp-1 flex-shrink-0"
+								>
+									{tag.name}
+								</div>
 
 
 								<div class=" text-gray-500 line-clamp-1">
 								<div class=" text-gray-500 line-clamp-1">
 									{tag.id}
 									{tag.id}
@@ -174,7 +184,7 @@
 				{:else if filteredOptions.length > 0}
 				{:else if filteredOptions.length > 0}
 					<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1">Search options</div>
 					<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1">Search options</div>
 
 
-					<div class="">
+					<div class=" max-h-60 overflow-auto">
 						{#each filteredOptions as option, optionIdx}
 						{#each filteredOptions as option, optionIdx}
 							<button
 							<button
 								class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx ===
 								class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx ===

+ 8 - 8
src/lib/components/layout/Sidebar/UserMenu.svelte

@@ -30,7 +30,7 @@
 
 
 	<slot name="content">
 	<slot name="content">
 		<DropdownMenu.Content
 		<DropdownMenu.Content
-			class="w-full {className} text-sm rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow font-primary"
+			class="w-full {className} text-sm rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-xl font-primary"
 			sideOffset={8}
 			sideOffset={8}
 			side="bottom"
 			side="bottom"
 			align="start"
 			align="start"
@@ -68,7 +68,7 @@
 						/>
 						/>
 					</svg>
 					</svg>
 				</div>
 				</div>
-				<div class=" self-center font-medium">{$i18n.t('Settings')}</div>
+				<div class=" self-center">{$i18n.t('Settings')}</div>
 			</button>
 			</button>
 
 
 			<button
 			<button
@@ -85,7 +85,7 @@
 				<div class=" self-center mr-3">
 				<div class=" self-center mr-3">
 					<ArchiveBox className="size-5" strokeWidth="1.5" />
 					<ArchiveBox className="size-5" strokeWidth="1.5" />
 				</div>
 				</div>
-				<div class=" self-center font-medium">{$i18n.t('Archived Chats')}</div>
+				<div class=" self-center">{$i18n.t('Archived Chats')}</div>
 			</button>
 			</button>
 
 
 			{#if role === 'admin'}
 			{#if role === 'admin'}
@@ -116,7 +116,7 @@
 							/>
 							/>
 						</svg>
 						</svg>
 					</div>
 					</div>
-					<div class=" self-center font-medium">{$i18n.t('Playground')}</div>
+					<div class=" self-center">{$i18n.t('Playground')}</div>
 				</button>
 				</button>
 
 
 				<button
 				<button
@@ -146,7 +146,7 @@
 							/>
 							/>
 						</svg>
 						</svg>
 					</div>
 					</div>
-					<div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div>
+					<div class=" self-center">{$i18n.t('Admin Panel')}</div>
 				</button>
 				</button>
 			{/if}
 			{/if}
 
 
@@ -179,7 +179,7 @@
 						/>
 						/>
 					</svg>
 					</svg>
 				</div>
 				</div>
-				<div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
+				<div class=" self-center">{$i18n.t('Sign Out')}</div>
 			</button>
 			</button>
 
 
 			{#if $activeUserCount}
 			{#if $activeUserCount}
@@ -201,7 +201,7 @@
 						</div>
 						</div>
 
 
 						<div class=" ">
 						<div class=" ">
-							<span class=" font-medium">
+							<span class="">
 								{$i18n.t('Active Users')}:
 								{$i18n.t('Active Users')}:
 							</span>
 							</span>
 							<span class=" font-semibold">
 							<span class=" font-semibold">
@@ -212,7 +212,7 @@
 				</Tooltip>
 				</Tooltip>
 			{/if}
 			{/if}
 
 
-			<!-- <DropdownMenu.Item class="flex items-center px-3 py-2 text-sm  font-medium">
+			<!-- <DropdownMenu.Item class="flex items-center px-3 py-2 text-sm ">
 				<div class="flex items-center">Profile</div>
 				<div class="flex items-center">Profile</div>
 			</DropdownMenu.Item> -->
 			</DropdownMenu.Item> -->
 		</DropdownMenu.Content>
 		</DropdownMenu.Content>

+ 36 - 29
src/lib/components/workspace/Functions.svelte

@@ -3,7 +3,7 @@
 	import fileSaver from 'file-saver';
 	import fileSaver from 'file-saver';
 	const { saveAs } = fileSaver;
 	const { saveAs } = fileSaver;
 
 
-	import { WEBUI_NAME, functions, models } from '$lib/stores';
+	import { WEBUI_NAME, config, functions, models } from '$lib/stores';
 	import { onMount, getContext, tick } from 'svelte';
 	import { onMount, getContext, tick } from 'svelte';
 	import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
 	import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
 
 
@@ -468,38 +468,45 @@
 	</div>
 	</div>
 </div>
 </div>
 
 
-<div class=" my-16">
-	<div class=" text-lg font-semibold mb-3 line-clamp-1">
-		{$i18n.t('Made by OpenWebUI Community')}
-	</div>
+{#if $config?.features.enable_community_sharing}
+	<div class=" my-16">
+		<div class=" text-lg font-semibold mb-3 line-clamp-1">
+			{$i18n.t('Made by OpenWebUI Community')}
+		</div>
 
 
-	<a
-		class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
-		href="https://openwebui.com/#open-webui-community"
-		target="_blank"
-	>
-		<div class=" self-center w-10 flex-shrink-0">
-			<div
-				class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
-			>
-				<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
-					<path
-						fill-rule="evenodd"
-						d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
-						clip-rule="evenodd"
-					/>
-				</svg>
+		<a
+			class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
+			href="https://openwebui.com/#open-webui-community"
+			target="_blank"
+		>
+			<div class=" self-center w-10 flex-shrink-0">
+				<div
+					class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						viewBox="0 0 24 24"
+						fill="currentColor"
+						class="w-6"
+					>
+						<path
+							fill-rule="evenodd"
+							d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
+							clip-rule="evenodd"
+						/>
+					</svg>
+				</div>
 			</div>
 			</div>
-		</div>
 
 
-		<div class=" self-center">
-			<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div>
-			<div class=" text-sm line-clamp-1">
-				{$i18n.t('Discover, download, and explore custom functions')}
+			<div class=" self-center">
+				<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div>
+				<div class=" text-sm line-clamp-1">
+					{$i18n.t('Discover, download, and explore custom functions')}
+				</div>
 			</div>
 			</div>
-		</div>
-	</a>
-</div>
+		</a>
+	</div>
+{/if}
 
 
 <DeleteConfirmDialog
 <DeleteConfirmDialog
 	bind:show={showDeleteConfirm}
 	bind:show={showDeleteConfirm}

+ 53 - 47
src/lib/components/workspace/Functions/FunctionEditor.svelte

@@ -7,6 +7,9 @@
 
 
 	import CodeEditor from '$lib/components/common/CodeEditor.svelte';
 	import CodeEditor from '$lib/components/common/CodeEditor.svelte';
 	import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
+	import Badge from '$lib/components/common/Badge.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
 
 
 	let formElement = null;
 	let formElement = null;
 	let loading = false;
 	let loading = false;
@@ -294,61 +297,64 @@ class Pipe:
 				}
 				}
 			}}
 			}}
 		>
 		>
-			<div class="mb-2.5">
-				<button
-					class="flex space-x-1"
-					on:click={() => {
-						goto('/workspace/functions');
-					}}
-					type="button"
-				>
-					<div class=" self-center">
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 20 20"
-							fill="currentColor"
-							class="w-4 h-4"
-						>
-							<path
-								fill-rule="evenodd"
-								d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
-								clip-rule="evenodd"
+			<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
+				<div class="w-full mb-2 flex flex-col gap-0.5">
+					<div class="flex w-full items-center">
+						<div class=" flex-shrink-0 mr-2">
+							<Tooltip content={$i18n.t('Back')}>
+								<button
+									class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
+									on:click={() => {
+										goto('/workspace/functions');
+									}}
+									type="button"
+								>
+									<ChevronLeft strokeWidth="2.5" />
+								</button>
+							</Tooltip>
+						</div>
+
+						<div class="flex-1">
+							<input
+								class="w-full text-2xl font-medium bg-transparent outline-none font-primary"
+								type="text"
+								placeholder={$i18n.t('Function Name (e.g. My Filter)')}
+								bind:value={name}
+								required
 							/>
 							/>
-						</svg>
+						</div>
+
+						<div>
+							<Badge type="muted" content={$i18n.t('Function')} />
+						</div>
 					</div>
 					</div>
-					<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
-				</button>
-			</div>
 
 
-			<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
-				<div class="w-full mb-2 flex flex-col gap-1.5">
-					<div class="flex gap-2 w-full">
-						<input
-							class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
-							type="text"
-							placeholder={$i18n.t('Function Name (e.g. My Filter)')}
-							bind:value={name}
-							required
-						/>
+					<div class=" flex gap-2 px-1">
+						{#if edit}
+							<div class="text-sm text-gray-500 flex-shrink-0">
+								{id}
+							</div>
+						{:else}
+							<input
+								class="w-full text-sm disabled:text-gray-500 bg-transparent outline-none"
+								type="text"
+								placeholder={$i18n.t('Function ID (e.g. my_filter)')}
+								bind:value={id}
+								required
+								disabled={edit}
+							/>
+						{/if}
 
 
 						<input
 						<input
-							class="w-full px-3 py-2 text-sm font-medium disabled:text-gray-300 dark:disabled:text-gray-700 bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
+							class="w-full text-sm bg-transparent outline-none"
 							type="text"
 							type="text"
-							placeholder={$i18n.t('Function ID (e.g. my_filter)')}
-							bind:value={id}
+							placeholder={$i18n.t(
+								'Function Description (e.g. A filter to remove profanity from text)'
+							)}
+							bind:value={meta.description}
 							required
 							required
-							disabled={edit}
 						/>
 						/>
 					</div>
 					</div>
-					<input
-						class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
-						type="text"
-						placeholder={$i18n.t(
-							'Function Description (e.g. A filter to remove profanity from text)'
-						)}
-						bind:value={meta.description}
-						required
-					/>
 				</div>
 				</div>
 
 
 				<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
 				<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
@@ -380,7 +386,7 @@ class Pipe:
 					</div>
 					</div>
 
 
 					<button
 					<button
-						class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg"
+						class="px-3 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
 						type="submit"
 						type="submit"
 					>
 					>
 						{$i18n.t('Save')}
 						{$i18n.t('Save')}

+ 4 - 11
src/lib/components/workspace/Knowledge.svelte

@@ -21,6 +21,7 @@
 	import Pencil from '../icons/Pencil.svelte';
 	import Pencil from '../icons/Pencil.svelte';
 	import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
 	import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
 	import ItemMenu from './Knowledge/ItemMenu.svelte';
 	import ItemMenu from './Knowledge/ItemMenu.svelte';
+	import Badge from '../common/Badge.svelte';
 
 
 	let query = '';
 	let query = '';
 	let selectedItem = null;
 	let selectedItem = null;
@@ -167,20 +168,12 @@
 					<div class="mt-5 flex justify-between">
 					<div class="mt-5 flex justify-between">
 						<div>
 						<div>
 							{#if item?.meta?.document}
 							{#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"
-								>
-									{$i18n.t('Document')}
-								</div>
+								<Badge type="muted" content={$i18n.t('Document')} />
 							{:else}
 							{:else}
-								<div
-									class="bg-green-500/20 text-green-700 dark:text-green-200 rounded uppercase text-xs font-bold px-1"
-								>
-									{$i18n.t('Collection')}
-								</div>
+								<Badge type="success" content={$i18n.t('Collection')} />
 							{/if}
 							{/if}
 						</div>
 						</div>
-						<div class=" text-xs text-gray-500">
+						<div class=" text-xs text-gray-500 line-clamp-1">
 							{$i18n.t('Updated')}
 							{$i18n.t('Updated')}
 							{dayjs(item.updated_at * 1000).fromNow()}
 							{dayjs(item.updated_at * 1000).fromNow()}
 						</div>
 						</div>

+ 265 - 127
src/lib/components/workspace/Knowledge/Collection.svelte

@@ -2,6 +2,7 @@
 	import Fuse from 'fuse.js';
 	import Fuse from 'fuse.js';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import { v4 as uuidv4 } from 'uuid';
 	import { v4 as uuidv4 } from 'uuid';
+	import { PaneGroup, Pane, PaneResizer } from 'paneforge';
 
 
 	import { onMount, getContext, onDestroy, tick } from 'svelte';
 	import { onMount, getContext, onDestroy, tick } from 'svelte';
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
@@ -21,22 +22,30 @@
 		updateKnowledgeById
 		updateKnowledgeById
 	} from '$lib/apis/knowledge';
 	} from '$lib/apis/knowledge';
 
 
-	import Spinner from '$lib/components/common/Spinner.svelte';
-	import Tooltip from '$lib/components/common/Tooltip.svelte';
-	import Badge from '$lib/components/common/Badge.svelte';
-	import Files from './Collection/Files.svelte';
-	import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
-	import AddContentModal from './Collection/AddTextContentModal.svelte';
 	import { transcribeAudio } from '$lib/apis/audio';
 	import { transcribeAudio } from '$lib/apis/audio';
 	import { blobToFile } from '$lib/utils';
 	import { blobToFile } from '$lib/utils';
 	import { processFile } from '$lib/apis/retrieval';
 	import { processFile } from '$lib/apis/retrieval';
+
+	import Spinner from '$lib/components/common/Spinner.svelte';
+	import Files from './Collection/Files.svelte';
+	import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
+
 	import AddContentMenu from './Collection/AddContentMenu.svelte';
 	import AddContentMenu from './Collection/AddContentMenu.svelte';
 	import AddTextContentModal from './Collection/AddTextContentModal.svelte';
 	import AddTextContentModal from './Collection/AddTextContentModal.svelte';
 
 
 	import SyncConfirmDialog from '../../common/ConfirmDialog.svelte';
 	import SyncConfirmDialog from '../../common/ConfirmDialog.svelte';
+	import RichTextInput from '$lib/components/common/RichTextInput.svelte';
+	import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
+	import Drawer from '$lib/components/common/Drawer.svelte';
+	import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
+	import MenuLines from '$lib/components/icons/MenuLines.svelte';
 
 
 	let largeScreen = true;
 	let largeScreen = true;
 
 
+	let pane;
+	let showSidepanel = true;
+	let minSize = 0;
+
 	type Knowledge = {
 	type Knowledge = {
 		id: string;
 		id: string;
 		name: string;
 		name: string;
@@ -93,7 +102,7 @@
 
 
 	const createFileFromText = (name, content) => {
 	const createFileFromText = (name, content) => {
 		const blob = new Blob([content], { type: 'text/plain' });
 		const blob = new Blob([content], { type: 'text/plain' });
-		const file = blobToFile(blob, `${name}.md`);
+		const file = blobToFile(blob, `${name}.txt`);
 
 
 		console.log(file);
 		console.log(file);
 		return file;
 		return file;
@@ -459,6 +468,36 @@
 		mediaQuery.addEventListener('change', handleMediaQuery);
 		mediaQuery.addEventListener('change', handleMediaQuery);
 		handleMediaQuery(mediaQuery);
 		handleMediaQuery(mediaQuery);
 
 
+		// Select the container element you want to observe
+		const container = document.getElementById('collection-container');
+
+		// initialize the minSize based on the container width
+		minSize = !largeScreen ? 100 : Math.floor((300 / 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 300
+				const percentage = (300 / width) * 100;
+				// set the minSize to the percentage, must be an integer
+				minSize = !largeScreen ? 100 : Math.floor(percentage);
+
+				if (showSidepanel) {
+					if (pane && pane.isExpanded() && pane.getSize() < minSize) {
+						pane.resize(minSize);
+					}
+				}
+			}
+		});
+
+		// Start observing the container's size changes
+		resizeObserver.observe(container);
+
+		if (pane) {
+			pane.expand();
+		}
+
 		id = $page.params.id;
 		id = $page.params.id;
 
 
 		const res = await getKnowledgeById(localStorage.token, id).catch((e) => {
 		const res = await getKnowledgeById(localStorage.token, id).catch((e) => {
@@ -552,157 +591,256 @@
 	}}
 	}}
 />
 />
 
 
-<div class="flex flex-col w-full max-h-[100dvh] h-full">
-	<div class="flex flex-col mb-2 flex-1 overflow-auto h-0">
-		{#if id && knowledge}
-			<div class="flex flex-row h-0 flex-1 overflow-auto">
-				<div
-					class=" {largeScreen
-						? 'flex-shrink-0'
-						: 'flex-1'} flex py-2.5 w-80 rounded-2xl border border-gray-50 dark:border-gray-850"
+<div class="flex flex-col w-full h-full max-h-[100dvh]" id="collection-container">
+	{#if id && knowledge}
+		<div class="flex flex-row flex-1 h-full max-h-full pb-2.5">
+			<PaneGroup direction="horizontal">
+				<Pane
+					bind:pane
+					defaultSize={minSize}
+					collapsible={true}
+					maxSize={50}
+					{minSize}
+					class="h-full"
+					onExpand={() => {
+						showSidepanel = true;
+					}}
+					onCollapse={() => {
+						showSidepanel = false;
+					}}
 				>
 				>
-					<div class=" flex flex-col w-full space-x-2 rounded-lg h-full">
-						<div class="w-full h-full flex flex-col">
-							<div class=" px-3">
-								<div class="flex">
-									<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"
+					<div
+						class="{largeScreen ? 'flex-shrink-0' : 'flex-1'}
+						flex
+						py-2
+						rounded-2xl
+						border
+						border-gray-50
+						h-full
+						dark:border-gray-850"
+					>
+						<div class=" flex flex-col w-full space-x-2 rounded-lg h-full">
+							<div class="w-full h-full flex flex-col">
+								<div class=" px-3">
+									<div class="flex py-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 Collection')}
+											on:focus={() => {
+												selectedFileId = null;
+											}}
+										/>
+
+										<div>
+											<AddContentMenu
+												on:upload={(e) => {
+													if (e.detail.type === 'directory') {
+														uploadDirectoryHandler();
+													} else if (e.detail.type === 'text') {
+														showAddTextContentModal = true;
+													} else {
+														document.getElementById('files-input').click();
+													}
+												}}
+												on:sync={(e) => {
+													showSyncConfirmModal = true;
+												}}
 											/>
 											/>
-										</svg>
+										</div>
 									</div>
 									</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 Collection')}
-										on:focus={() => {
-											selectedFileId = null;
-										}}
-									/>
+								</div>
 
 
-									<div>
-										<AddContentMenu
-											on:upload={(e) => {
-												if (e.detail.type === 'directory') {
-													uploadDirectoryHandler();
-												} else if (e.detail.type === 'text') {
-													showAddTextContentModal = true;
-												} else {
-													document.getElementById('files-input').click();
-												}
+								{#if filteredItems.length > 0}
+									<div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
+										<Files
+											files={filteredItems}
+											{selectedFileId}
+											on:click={(e) => {
+												selectedFileId = selectedFileId === e.detail ? null : e.detail;
 											}}
 											}}
-											on:sync={(e) => {
-												showSyncConfirmModal = true;
+											on:delete={(e) => {
+												console.log(e.detail);
+
+												selectedFileId = null;
+												deleteFileHandler(e.detail);
 											}}
 											}}
 										/>
 										/>
 									</div>
 									</div>
-								</div>
-
-								<hr class=" mt-2 mb-1 border-gray-50 dark:border-gray-850" />
+								{:else}
+									<div class="m-auto text-gray-500 text-xs">{$i18n.t('No content found')}</div>
+								{/if}
 							</div>
 							</div>
-
-							{#if filteredItems.length > 0}
-								<div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
-									<Files
-										files={filteredItems}
-										{selectedFileId}
-										on:click={(e) => {
-											selectedFileId = selectedFileId === e.detail ? null : e.detail;
-										}}
-										on:delete={(e) => {
-											console.log(e.detail);
-
-											selectedFileId = null;
-											deleteFileHandler(e.detail);
-										}}
-									/>
-								</div>
-							{:else}
-								<div class="m-auto text-gray-500 text-xs">{$i18n.t('No content found')}</div>
-							{/if}
 						</div>
 						</div>
 					</div>
 					</div>
-				</div>
+				</Pane>
 
 
 				{#if largeScreen}
 				{#if largeScreen}
-					<div class="flex-1 flex justify-start max-h-full overflow-hidden pl-3">
-						{#if selectedFile}
-							<div class=" flex flex-col w-full h-full">
-								<div class=" flex-shrink-0 mb-2 flex items-center">
-									<div class=" flex-1 text-xl line-clamp-1">
-										{selectedFile?.meta?.name}
+					<PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
+						<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
+							<EllipsisVertical className="size-4 invisible group-hover:visible" />
+						</div>
+					</PaneResizer>
+					<Pane>
+						<div class="flex-1 flex justify-start h-full max-h-full">
+							{#if selectedFile}
+								<div class=" flex flex-col w-full h-full max-h-full ml-2.5">
+									<div class="flex-shrink-0 mb-2 flex items-center">
+										{#if !showSidepanel}
+											<div class="-translate-x-2">
+												<button
+													class="w-full text-left text-sm p-1.5 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
+													on:click={() => {
+														pane.expand();
+													}}
+												>
+													<ChevronLeft strokeWidth="2.5" />
+												</button>
+											</div>
+										{/if}
+
+										<div class=" flex-1 text-2xl font-medium">
+											<a
+												class="hover:text-gray-500 hover:dark:text-gray-100 hover:underline flex-grow line-clamp-1"
+												href={selectedFile.id ? `/api/v1/files/${selectedFile.id}/content` : '#'}
+												target="_blank"
+											>
+												{selectedFile?.meta?.name}
+											</a>
+										</div>
+
+										<div>
+											<button
+												class="self-center w-fit text-sm py-1 px-2.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-lg"
+												on:click={() => {
+													updateFileContentHandler();
+												}}
+											>
+												{$i18n.t('Save')}
+											</button>
+										</div>
 									</div>
 									</div>
 
 
-									<div>
-										<button
-											class="self-center w-fit text-sm py-1 px-2.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-lg"
-											on:click={() => {
-												updateFileContentHandler();
-											}}
-										>
-											{$i18n.t('Save')}
-										</button>
+									<div
+										class=" flex-1 w-full h-full max-h-full text-sm bg-transparent outline-none overflow-y-auto scrollbar-hidden"
+									>
+										{#key selectedFile.id}
+											<RichTextInput
+												className="input-prose-sm"
+												bind:value={selectedFile.data.content}
+												placeholder={$i18n.t('Add content here')}
+											/>
+										{/key}
 									</div>
 									</div>
 								</div>
 								</div>
+							{:else}
+								<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-grow">
-									<textarea
-										class=" w-full h-full resize-none rounded-xl py-4 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-										bind:value={selectedFile.data.content}
-										placeholder={$i18n.t('Add content here')}
-									/>
-								</div>
-							</div>
-						{:else}
-							<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">
+												<div class="flex w-full px-1">
 													<input
 													<input
 														type="text"
 														type="text"
-														class="text-center w-full font-medium text-3xl font-primary bg-transparent outline-none"
-														bind:value={knowledge.name}
+														class="text-center w-full text-gray-500 bg-transparent outline-none"
+														bind:value={knowledge.description}
 														on:input={() => {
 														on:input={() => {
 															changeDebounceHandler();
 															changeDebounceHandler();
 														}}
 														}}
 													/>
 													/>
 												</div>
 												</div>
 											</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>
 									</div>
 									</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>
 								</div>
+							{/if}
+						</div>
+					</Pane>
+				{:else if !largeScreen && selectedFileId !== null}
+					<Drawer
+						className="h-full"
+						show={selectedFileId !== null}
+						on:close={() => {
+							selectedFileId = null;
+						}}
+					>
+						<div class="flex flex-col justify-start h-full max-h-full p-2">
+							<div class=" flex flex-col w-full h-full max-h-full">
+								<div class="flex-shrink-0 mt-1 mb-2 flex items-center">
+									<div class="mr-2">
+										<button
+											class="w-full text-left text-sm p-1.5 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
+											on:click={() => {
+												selectedFileId = null;
+											}}
+										>
+											<ChevronLeft strokeWidth="2.5" />
+										</button>
+									</div>
+									<div class=" flex-1 text-xl line-clamp-1">
+										{selectedFile?.meta?.name}
+									</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>
+										<button
+											class="self-center w-fit text-sm py-1 px-2.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-lg"
+											on:click={() => {
+												updateFileContentHandler();
+											}}
+										>
+											{$i18n.t('Save')}
+										</button>
+									</div>
+								</div>
+
+								<div
+									class=" flex-1 w-full h-full max-h-full py-2.5 px-3.5 rounded-lg text-sm bg-transparent overflow-y-auto scrollbar-hidden"
+								>
+									{#key selectedFile.id}
+										<RichTextInput
+											className="input-prose-sm"
+											bind:value={selectedFile.data.content}
+											placeholder={$i18n.t('Add content here')}
+										/>
+									{/key}
 								</div>
 								</div>
 							</div>
 							</div>
-						{/if}
-					</div>
+						</div>
+					</Drawer>
 				{/if}
 				{/if}
-			</div>
-		{:else}
-			<Spinner />
-		{/if}
-	</div>
+			</PaneGroup>
+		</div>
+	{:else}
+		<Spinner />
+	{/if}
 </div>
 </div>

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

@@ -29,7 +29,7 @@
 >
 >
 	<Tooltip content={$i18n.t('Add Content')}>
 	<Tooltip content={$i18n.t('Add Content')}>
 		<button
 		<button
-			class=" px-2 py-2 rounded-xl border border-gray-50 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"
+			class=" p-1.5 rounded-xl hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition font-medium text-sm flex items-center space-x-1"
 			on:click={(e) => {
 			on:click={(e) => {
 				e.stopPropagation();
 				e.stopPropagation();
 				show = true;
 				show = true;

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

@@ -7,98 +7,138 @@
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
+	import RichTextInput from '$lib/components/common/RichTextInput.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
+	import Mic from '$lib/components/icons/Mic.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import VoiceRecording from '$lib/components/chat/MessageInput/VoiceRecording.svelte';
 	export let show = false;
 	export let show = false;
 
 
-	let name = '';
+	let name = 'Untitled';
 	let content = '';
 	let content = '';
+
+	let voiceInput = false;
 </script>
 </script>
 
 
-<Modal size="md" 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 Content')}</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={() => {
-						if (name.trim() === '' || content.trim() === '') {
-							toast.error($i18n.t('Please fill in all fields.'));
-							name = '';
-							content = '';
-							return;
-						}
+<Modal size="full" className="h-full bg-white dark:bg-gray-900" bind:show>
+	<div class="absolute top-0 right-0 p-5">
+		<button
+			class="self-center dark:text-white"
+			type="button"
+			on:click={() => {
+				show = false;
+			}}
+		>
+			<XMark className="size-3.5" />
+		</button>
+	</div>
+	<div class="flex flex-col md:flex-row w-full h-full md:space-x-4 dark:text-gray-200">
+		<form
+			class="flex flex-col w-full h-full"
+			on:submit|preventDefault={() => {
+				if (name.trim() === '' || content.trim() === '') {
+					toast.error($i18n.t('Please fill in all fields.'));
+					name = name.trim();
+					content = content.trim();
+					return;
+				}
 
 
-						dispatch('submit', {
-							name,
-							content
-						});
-						show = false;
-						name = '';
-						content = '';
-					}}
-				>
-					<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">{$i18n.t('Title')}</div>
+				dispatch('submit', {
+					name,
+					content
+				});
+				show = false;
+				name = '';
+				content = '';
+			}}
+		>
+			<div class=" flex-1 w-full h-full flex justify-center overflow-auto px-5 py-4">
+				<div class=" max-w-3xl py-2 md:py-10 w-full flex flex-col gap-2">
+					<div class="flex-shrink-0 w-full flex justify-between items-center">
+						<div class="w-full">
+							<input
+								class="w-full text-3xl font-semibold bg-transparent outline-none"
+								type="text"
+								bind:value={name}
+								placeholder={$i18n.t('Title')}
+								required
+							/>
+						</div>
+					</div>
 
 
-								<div class="w-full mt-1">
-									<input
-										class="w-full rounded-lg py-2 px-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none"
-										type="text"
-										bind:value={name}
-										placeholder={`Name your content`}
-										required
-									/>
-								</div>
-							</div>
+					<div class=" flex-1 w-full h-full">
+						<RichTextInput bind:value={content} placeholder={$i18n.t('Write something...')} />
+					</div>
+				</div>
+			</div>
 
 
-							<div>
-								<div class="text-sm mb-2">{$i18n.t('Content')}</div>
+			<div
+				class="flex flex-row items-center justify-end text-sm font-medium flex-shrink-0 mt-1 p-4 gap-1.5"
+			>
+				<div class="">
+					{#if voiceInput}
+						<div class=" max-w-full w-64">
+							<VoiceRecording
+								bind:recording={voiceInput}
+								className="p-1"
+								on:cancel={() => {
+									voiceInput = false;
+								}}
+								on:confirm={(e) => {
+									const response = e.detail;
+									content = `${content}${response} `;
 
 
-								<div class=" w-full mt-1">
-									<textarea
-										class="w-full resize-none rounded-lg py-2 px-4 text-sm bg-whites dark:text-gray-300 dark:bg-gray-850 outline-none"
-										rows="10"
-										bind:value={content}
-										placeholder={`Write your content here`}
-										required
-									/>
-								</div>
-							</div>
+									voiceInput = false;
+								}}
+							/>
 						</div>
 						</div>
-					</div>
+					{:else}
+						<Tooltip content={$i18n.t('Voice Input')}>
+							<button
+								class=" p-2 bg-gray-50 text-gray-700 dark:bg-gray-700 dark:text-white transition rounded-full"
+								type="button"
+								on:click={async () => {
+									try {
+										let stream = await navigator.mediaDevices
+											.getUserMedia({ audio: true })
+											.catch(function (err) {
+												toast.error(
+													$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
+														error: err
+													})
+												);
+												return null;
+											});
 
 
-					<div class="flex justify-end text-sm font-medium">
+										if (stream) {
+											voiceInput = true;
+											const tracks = stream.getTracks();
+											tracks.forEach((track) => track.stop());
+										}
+										stream = null;
+									} catch {
+										toast.error($i18n.t('Permission denied when accessing microphone'));
+									}
+								}}
+							>
+								<Mic className="size-5" />
+							</button>
+						</Tooltip>
+					{/if}
+				</div>
+
+				<div class=" flex-shrink-0">
+					<Tooltip content={$i18n.t('Save')}>
 						<button
 						<button
-							class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
+							class=" px-3.5 py-2 bg-black text-white dark:bg-white dark:text-black transition rounded-full"
 							type="submit"
 							type="submit"
 						>
 						>
-							{$i18n.t('Add Content')}
+							{$i18n.t('Save')}
 						</button>
 						</button>
-					</div>
-				</form>
+					</Tooltip>
+				</div>
 			</div>
 			</div>
-		</div>
+		</form>
 	</div>
 	</div>
 </Modal>
 </Modal>
 
 

+ 9 - 1
src/lib/components/workspace/Knowledge/Collection/Files.svelte

@@ -10,7 +10,7 @@
 
 
 <div class=" max-h-full flex flex-col w-full">
 <div class=" max-h-full flex flex-col w-full">
 	{#each files as file}
 	{#each files as file}
-		<div class="mt-2 px-2">
+		<div class="mt-1 px-2">
 			<FileItem
 			<FileItem
 				className="w-full"
 				className="w-full"
 				colorClassName="{selectedFileId === file.id
 				colorClassName="{selectedFileId === file.id
@@ -23,9 +23,17 @@
 				loading={file.status === 'uploading'}
 				loading={file.status === 'uploading'}
 				dismissible
 				dismissible
 				on:click={() => {
 				on:click={() => {
+					if (file.status === 'uploading') {
+						return;
+					}
+
 					dispatch('click', file.id);
 					dispatch('click', file.id);
 				}}
 				}}
 				on:dismiss={() => {
 				on:dismiss={() => {
+					if (file.status === 'uploading') {
+						return;
+					}
+
 					dispatch('delete', file.id);
 					dispatch('delete', file.id);
 				}}
 				}}
 			/>
 			/>

+ 36 - 29
src/lib/components/workspace/Models.svelte

@@ -9,7 +9,7 @@
 
 
 	import { onMount, getContext, tick } from 'svelte';
 	import { onMount, getContext, tick } from 'svelte';
 
 
-	import { WEBUI_NAME, mobile, models, settings, user } from '$lib/stores';
+	import { WEBUI_NAME, config, mobile, models, settings, user } from '$lib/stores';
 	import { addNewModel, deleteModelById, getModelInfos, updateModelById } from '$lib/apis/models';
 	import { addNewModel, deleteModelById, getModelInfos, updateModelById } from '$lib/apis/models';
 
 
 	import { deleteModel } from '$lib/apis/ollama';
 	import { deleteModel } from '$lib/apis/ollama';
@@ -674,35 +674,42 @@
 	{/if}
 	{/if}
 </div>
 </div>
 
 
-<div class=" my-16">
-	<div class=" text-lg font-semibold mb-3 line-clamp-1">
-		{$i18n.t('Made by OpenWebUI Community')}
-	</div>
+{#if $config?.features.enable_community_sharing}
+	<div class=" my-16">
+		<div class=" text-lg font-semibold mb-3 line-clamp-1">
+			{$i18n.t('Made by OpenWebUI Community')}
+		</div>
 
 
-	<a
-		class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
-		href="https://openwebui.com/#open-webui-community"
-		target="_blank"
-	>
-		<div class=" self-center w-10 flex-shrink-0">
-			<div
-				class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
-			>
-				<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
-					<path
-						fill-rule="evenodd"
-						d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
-						clip-rule="evenodd"
-					/>
-				</svg>
+		<a
+			class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
+			href="https://openwebui.com/#open-webui-community"
+			target="_blank"
+		>
+			<div class=" self-center w-10 flex-shrink-0">
+				<div
+					class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						viewBox="0 0 24 24"
+						fill="currentColor"
+						class="w-6"
+					>
+						<path
+							fill-rule="evenodd"
+							d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
+							clip-rule="evenodd"
+						/>
+					</svg>
+				</div>
 			</div>
 			</div>
-		</div>
 
 
-		<div class=" self-center">
-			<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a model')}</div>
-			<div class=" text-sm line-clamp-1">
-				{$i18n.t('Discover, download, and explore model presets')}
+			<div class=" self-center">
+				<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a model')}</div>
+				<div class=" text-sm line-clamp-1">
+					{$i18n.t('Discover, download, and explore model presets')}
+				</div>
 			</div>
 			</div>
-		</div>
-	</a>
-</div>
+		</a>
+	</div>
+{/if}

+ 48 - 41
src/lib/components/workspace/Prompts.svelte

@@ -4,7 +4,7 @@
 	const { saveAs } = fileSaver;
 	const { saveAs } = fileSaver;
 
 
 	import { onMount, getContext } from 'svelte';
 	import { onMount, getContext } from 'svelte';
-	import { WEBUI_NAME, prompts } from '$lib/stores';
+	import { WEBUI_NAME, config, prompts } from '$lib/stores';
 	import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
 	import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
 	import { error } from '@sveltejs/kit';
 	import { error } from '@sveltejs/kit';
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
@@ -67,6 +67,18 @@
 	</title>
 	</title>
 </svelte:head>
 </svelte:head>
 
 
+<DeleteConfirmDialog
+	bind:show={showDeleteConfirm}
+	title={$i18n.t('Delete prompt?')}
+	on:confirm={() => {
+		deleteHandler(deletePrompt);
+	}}
+>
+	<div class=" text-sm text-gray-500">
+		{$i18n.t('This will delete')} <span class="  font-semibold">{deletePrompt.command}</span>.
+	</div>
+</DeleteConfirmDialog>
+
 <div class=" flex w-full space-x-2 mb-2.5">
 <div class=" flex w-full space-x-2 mb-2.5">
 	<div class="flex flex-1">
 	<div class="flex flex-1">
 		<div class=" self-center ml-1 mr-3">
 		<div class=" self-center ml-1 mr-3">
@@ -128,7 +140,7 @@
 		>
 		>
 			<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
 			<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
 				<a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}>
 				<a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}>
-					<div class=" flex-1 self-center pl-5">
+					<div class=" flex-1 self-center pl-1.5">
 						<div class=" font-semibold line-clamp-1">{prompt.command}</div>
 						<div class=" font-semibold line-clamp-1">{prompt.command}</div>
 						<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
 						<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
 							{prompt.title}
 							{prompt.title}
@@ -284,47 +296,42 @@
 	</div>
 	</div>
 </div>
 </div>
 
 
-<div class=" my-16">
-	<div class=" text-lg font-semibold mb-3 line-clamp-1">
-		{$i18n.t('Made by OpenWebUI Community')}
-	</div>
-
-	<a
-		class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
-		href="https://openwebui.com/#open-webui-community"
-		target="_blank"
-	>
-		<div class=" self-center w-10 flex-shrink-0">
-			<div
-				class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
-			>
-				<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
-					<path
-						fill-rule="evenodd"
-						d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
-						clip-rule="evenodd"
-					/>
-				</svg>
-			</div>
+{#if $config?.features.enable_community_sharing}
+	<div class=" my-16">
+		<div class=" text-lg font-semibold mb-3 line-clamp-1">
+			{$i18n.t('Made by OpenWebUI Community')}
 		</div>
 		</div>
 
 
-		<div class=" self-center">
-			<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a prompt')}</div>
-			<div class=" text-sm line-clamp-1">
-				{$i18n.t('Discover, download, and explore custom prompts')}
+		<a
+			class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
+			href="https://openwebui.com/#open-webui-community"
+			target="_blank"
+		>
+			<div class=" self-center w-10 flex-shrink-0">
+				<div
+					class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						viewBox="0 0 24 24"
+						fill="currentColor"
+						class="w-6"
+					>
+						<path
+							fill-rule="evenodd"
+							d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
+							clip-rule="evenodd"
+						/>
+					</svg>
+				</div>
 			</div>
 			</div>
-		</div>
-	</a>
-</div>
 
 
-<DeleteConfirmDialog
-	bind:show={showDeleteConfirm}
-	title={$i18n.t('Delete prompt?')}
-	on:confirm={() => {
-		deleteHandler(deletePrompt);
-	}}
->
-	<div class=" text-sm text-gray-500">
-		{$i18n.t('This will delete')} <span class="  font-semibold">{deletePrompt.command}</span>.
+			<div class=" self-center">
+				<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a prompt')}</div>
+				<div class=" text-sm line-clamp-1">
+					{$i18n.t('Discover, download, and explore custom prompts')}
+				</div>
+			</div>
+		</a>
 	</div>
 	</div>
-</DeleteConfirmDialog>
+{/if}

+ 36 - 29
src/lib/components/workspace/Tools.svelte

@@ -4,7 +4,7 @@
 	const { saveAs } = fileSaver;
 	const { saveAs } = fileSaver;
 
 
 	import { onMount, getContext } from 'svelte';
 	import { onMount, getContext } from 'svelte';
-	import { WEBUI_NAME, prompts, tools } from '$lib/stores';
+	import { WEBUI_NAME, config, prompts, tools } from '$lib/stores';
 	import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
 	import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
 
 
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
@@ -422,38 +422,45 @@
 	</div>
 	</div>
 </div>
 </div>
 
 
-<div class=" my-16">
-	<div class=" text-lg font-semibold mb-3 line-clamp-1">
-		{$i18n.t('Made by OpenWebUI Community')}
-	</div>
+{#if $config?.features.enable_community_sharing}
+	<div class=" my-16">
+		<div class=" text-lg font-semibold mb-3 line-clamp-1">
+			{$i18n.t('Made by OpenWebUI Community')}
+		</div>
 
 
-	<a
-		class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
-		href="https://openwebui.com/#open-webui-community"
-		target="_blank"
-	>
-		<div class=" self-center w-10 flex-shrink-0">
-			<div
-				class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
-			>
-				<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
-					<path
-						fill-rule="evenodd"
-						d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
-						clip-rule="evenodd"
-					/>
-				</svg>
+		<a
+			class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
+			href="https://openwebui.com/#open-webui-community"
+			target="_blank"
+		>
+			<div class=" self-center w-10 flex-shrink-0">
+				<div
+					class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						viewBox="0 0 24 24"
+						fill="currentColor"
+						class="w-6"
+					>
+						<path
+							fill-rule="evenodd"
+							d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
+							clip-rule="evenodd"
+						/>
+					</svg>
+				</div>
 			</div>
 			</div>
-		</div>
 
 
-		<div class=" self-center">
-			<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a tool')}</div>
-			<div class=" text-sm line-clamp-1">
-				{$i18n.t('Discover, download, and explore custom tools')}
+			<div class=" self-center">
+				<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a tool')}</div>
+				<div class=" text-sm line-clamp-1">
+					{$i18n.t('Discover, download, and explore custom tools')}
+				</div>
 			</div>
 			</div>
-		</div>
-	</a>
-</div>
+		</a>
+	</div>
+{/if}
 
 
 <DeleteConfirmDialog
 <DeleteConfirmDialog
 	bind:show={showDeleteConfirm}
 	bind:show={showDeleteConfirm}

+ 53 - 47
src/lib/components/workspace/Tools/ToolkitEditor.svelte

@@ -6,6 +6,9 @@
 	import CodeEditor from '$lib/components/common/CodeEditor.svelte';
 	import CodeEditor from '$lib/components/common/CodeEditor.svelte';
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
 	import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
+	import Badge from '$lib/components/common/Badge.svelte';
+	import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
 
 
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
@@ -182,61 +185,64 @@ class Tools:
 				}
 				}
 			}}
 			}}
 		>
 		>
-			<div class="mb-2.5">
-				<button
-					class="flex space-x-1"
-					on:click={() => {
-						goto('/workspace/tools');
-					}}
-					type="button"
-				>
-					<div class=" self-center">
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 20 20"
-							fill="currentColor"
-							class="w-4 h-4"
-						>
-							<path
-								fill-rule="evenodd"
-								d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
-								clip-rule="evenodd"
+			<div class="flex flex-col flex-1 overflow-auto h-0">
+				<div class="w-full mb-2 flex flex-col gap-0.5">
+					<div class="flex w-full items-center">
+						<div class=" flex-shrink-0 mr-2">
+							<Tooltip content={$i18n.t('Back')}>
+								<button
+									class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
+									on:click={() => {
+										goto('/workspace/tools');
+									}}
+									type="button"
+								>
+									<ChevronLeft strokeWidth="2.5" />
+								</button>
+							</Tooltip>
+						</div>
+
+						<div class="flex-1">
+							<input
+								class="w-full text-2xl font-medium bg-transparent outline-none"
+								type="text"
+								placeholder={$i18n.t('Toolkit Name (e.g. My ToolKit)')}
+								bind:value={name}
+								required
 							/>
 							/>
-						</svg>
+						</div>
+
+						<div>
+							<Badge type="muted" content={$i18n.t('Tool')} />
+						</div>
 					</div>
 					</div>
-					<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
-				</button>
-			</div>
 
 
-			<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
-				<div class="w-full mb-2 flex flex-col gap-1.5">
-					<div class="flex gap-2 w-full">
-						<input
-							class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
-							type="text"
-							placeholder={$i18n.t('Toolkit Name (e.g. My ToolKit)')}
-							bind:value={name}
-							required
-						/>
+					<div class=" flex gap-2 px-1">
+						{#if edit}
+							<div class="text-sm text-gray-500 flex-shrink-0">
+								{id}
+							</div>
+						{:else}
+							<input
+								class="w-full text-sm disabled:text-gray-500 bg-transparent outline-none"
+								type="text"
+								placeholder={$i18n.t('Toolkit ID (e.g. my_toolkit)')}
+								bind:value={id}
+								required
+								disabled={edit}
+							/>
+						{/if}
 
 
 						<input
 						<input
-							class="w-full px-3 py-2 text-sm font-medium disabled:text-gray-300 dark:disabled:text-gray-700 bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
+							class="w-full text-sm bg-transparent outline-none"
 							type="text"
 							type="text"
-							placeholder={$i18n.t('Toolkit ID (e.g. my_toolkit)')}
-							bind:value={id}
+							placeholder={$i18n.t(
+								'Toolkit Description (e.g. A toolkit for performing various operations)'
+							)}
+							bind:value={meta.description}
 							required
 							required
-							disabled={edit}
 						/>
 						/>
 					</div>
 					</div>
-					<input
-						class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
-						type="text"
-						placeholder={$i18n.t(
-							'Toolkit Description (e.g. A toolkit for performing various operations)'
-						)}
-						bind:value={meta.description}
-						required
-					/>
 				</div>
 				</div>
 
 
 				<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
 				<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
@@ -268,7 +274,7 @@ class Tools:
 					</div>
 					</div>
 
 
 					<button
 					<button
-						class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg"
+						class="px-3 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
 						type="submit"
 						type="submit"
 					>
 					>
 						{$i18n.t('Save')}
 						{$i18n.t('Save')}

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

@@ -42,6 +42,7 @@
 	"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 Parameters": "التعليمات المتقدمة",
 	"Advanced Params": "المعلمات المتقدمة",
 	"Advanced Params": "المعلمات المتقدمة",
+	"All chats": "",
 	"All Documents": "جميع الملفات",
 	"All Documents": "جميع الملفات",
 	"All Users": "جميع المستخدمين",
 	"All Users": "جميع المستخدمين",
 	"Allow Chat Deletion": "يستطيع حذف المحادثات",
 	"Allow Chat Deletion": "يستطيع حذف المحادثات",
@@ -185,6 +186,7 @@
 	"Delete chat": "حذف المحادثه",
 	"Delete chat": "حذف المحادثه",
 	"Delete Chat": "حذف المحادثه.",
 	"Delete Chat": "حذف المحادثه.",
 	"Delete chat?": "",
 	"Delete chat?": "",
+	"Delete folder?": "",
 	"Delete function?": "",
 	"Delete function?": "",
 	"Delete prompt?": "",
 	"Delete prompt?": "",
 	"delete this link": "أحذف هذا الرابط",
 	"delete this link": "أحذف هذا الرابط",
@@ -220,9 +222,7 @@
 	"Download": "تحميل",
 	"Download": "تحميل",
 	"Download canceled": "تم اللغاء التحميل",
 	"Download canceled": "تم اللغاء التحميل",
 	"Download Database": "تحميل قاعدة البيانات",
 	"Download Database": "تحميل قاعدة البيانات",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "أسقط أية ملفات هنا لإضافتها إلى المحادثة",
 	"Drop any files here to add to the conversation": "أسقط أية ملفات هنا لإضافتها إلى المحادثة",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. الوحدات الزمنية الصالحة هي 's', 'm', 'h'.",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. الوحدات الزمنية الصالحة هي 's', 'm', 'h'.",
 	"Edit": "تعديل",
 	"Edit": "تعديل",
 	"Edit Memory": "",
 	"Edit Memory": "",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "تم اكتشاف انتحال بصمة الإصبع: غير قادر على استخدام الأحرف الأولى كصورة رمزية. الافتراضي لصورة الملف الشخصي الافتراضية.",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "تم اكتشاف انتحال بصمة الإصبع: غير قادر على استخدام الأحرف الأولى كصورة رمزية. الافتراضي لصورة الملف الشخصي الافتراضية.",
 	"Fluidly stream large external response chunks": "دفق قطع الاستجابة الخارجية الكبيرة بسلاسة",
 	"Fluidly stream large external response chunks": "دفق قطع الاستجابة الخارجية الكبيرة بسلاسة",
 	"Focus chat input": "التركيز على إدخال الدردشة",
 	"Focus chat input": "التركيز على إدخال الدردشة",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "اتبعت التعليمات على أكمل وجه",
 	"Followed instructions perfectly": "اتبعت التعليمات على أكمل وجه",
 	"Form": "",
 	"Form": "",
 	"Format your variables using square brackets like this:": "قم بتنسيق المتغيرات الخاصة بك باستخدام الأقواس المربعة مثل هذا:",
 	"Format your variables using square brackets like this:": "قم بتنسيق المتغيرات الخاصة بك باستخدام الأقواس المربعة مثل هذا:",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "القائمة البيضاء الموديل",
 	"Model(s) Whitelisted": "القائمة البيضاء الموديل",
 	"Modelfile Content": "محتوى الملف النموذجي",
 	"Modelfile Content": "محتوى الملف النموذجي",
 	"Models": "الموديلات",
 	"Models": "الموديلات",
+	"more": "",
 	"More": "المزيد",
 	"More": "المزيد",
 	"Move to Top": "",
 	"Move to Top": "",
 	"Name": "الأسم",
 	"Name": "الأسم",
 	"Name your model": "قم بتسمية النموذج الخاص بك",
 	"Name your model": "قم بتسمية النموذج الخاص بك",
 	"New Chat": "دردشة جديدة",
 	"New Chat": "دردشة جديدة",
+	"New folder": "",
 	"New Password": "كلمة المرور الجديدة",
 	"New Password": "كلمة المرور الجديدة",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "",
 	"No content to speak": "",
+	"No distance available": "",
 	"No file selected": "",
 	"No file selected": "",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -532,9 +539,11 @@
 	"Record voice": "سجل صوت",
 	"Record voice": "سجل صوت",
 	"Redirecting you to OpenWebUI Community": "OpenWebUI إعادة توجيهك إلى مجتمع ",
 	"Redirecting you to OpenWebUI Community": "OpenWebUI إعادة توجيهك إلى مجتمع ",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
+	"References from": "",
 	"Refused when it shouldn't have": "رفض عندما لا ينبغي أن يكون",
 	"Refused when it shouldn't have": "رفض عندما لا ينبغي أن يكون",
 	"Regenerate": "تجديد",
 	"Regenerate": "تجديد",
 	"Release Notes": "ملاحظات الإصدار",
 	"Release Notes": "ملاحظات الإصدار",
+	"Relevance": "",
 	"Remove": "إزالة",
 	"Remove": "إزالة",
 	"Remove Model": "حذف الموديل",
 	"Remove Model": "حذف الموديل",
 	"Rename": "إعادة تسمية",
 	"Rename": "إعادة تسمية",
@@ -604,6 +613,7 @@
 	"Select model": " أختار موديل",
 	"Select model": " أختار موديل",
 	"Select only one model to call": "",
 	"Select only one model to call": "",
 	"Selected model(s) do not support image inputs": "النموذج (النماذج) المحددة لا تدعم مدخلات الصور",
 	"Selected model(s) do not support image inputs": "النموذج (النماذج) المحددة لا تدعم مدخلات الصور",
+	"Semantic distance to query": "",
 	"Send": "تم",
 	"Send": "تم",
 	"Send a Message": "يُرجى إدخال طلبك هنا",
 	"Send a Message": "يُرجى إدخال طلبك هنا",
 	"Send message": "يُرجى إدخال طلبك هنا.",
 	"Send message": "يُرجى إدخال طلبك هنا.",
@@ -684,6 +694,7 @@
 	"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 option will delete all existing files in the collection and replace them with newly uploaded files.": "",
 	"This will delete": "",
 	"This will delete": "",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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": "شرح شامل",
 	"Thorough explanation": "شرح شامل",
 	"Tika": "",
 	"Tika": "",

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

@@ -42,6 +42,7 @@
 	"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 Parameters": "Разширени Параметри",
 	"Advanced Params": "Разширени параметри",
 	"Advanced Params": "Разширени параметри",
+	"All chats": "",
 	"All Documents": "Всички Документи",
 	"All Documents": "Всички Документи",
 	"All Users": "Всички Потребители",
 	"All Users": "Всички Потребители",
 	"Allow Chat Deletion": "Позволи Изтриване на Чат",
 	"Allow Chat Deletion": "Позволи Изтриване на Чат",
@@ -185,6 +186,7 @@
 	"Delete chat": "Изтриване на чат",
 	"Delete chat": "Изтриване на чат",
 	"Delete Chat": "Изтриване на Чат",
 	"Delete Chat": "Изтриване на Чат",
 	"Delete chat?": "",
 	"Delete chat?": "",
+	"Delete folder?": "",
 	"Delete function?": "",
 	"Delete function?": "",
 	"Delete prompt?": "",
 	"Delete prompt?": "",
 	"delete this link": "Изтриване на този линк",
 	"delete this link": "Изтриване на този линк",
@@ -220,9 +222,7 @@
 	"Download": "Изтегляне отменено",
 	"Download": "Изтегляне отменено",
 	"Download canceled": "Изтегляне отменено",
 	"Download canceled": "Изтегляне отменено",
 	"Download Database": "Сваляне на база данни",
 	"Download Database": "Сваляне на база данни",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "Пускане на файлове тук, за да ги добавите в чата",
 	"Drop any files here to add to the conversation": "Пускане на файлове тук, за да ги добавите в чата",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "напр. '30с','10м'. Валидни единици са 'с', 'м', 'ч'.",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "напр. '30с','10м'. Валидни единици са 'с', 'м', 'ч'.",
 	"Edit": "Редактиране",
 	"Edit": "Редактиране",
 	"Edit Memory": "",
 	"Edit Memory": "",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Потвърждаване на отпечатък: Не може да се използва инициализационна буква като аватар. Потребителят се връща към стандартна аватарка.",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Потвърждаване на отпечатък: Не може да се използва инициализационна буква като аватар. Потребителят се връща към стандартна аватарка.",
 	"Fluidly stream large external response chunks": "Плавно предаване на големи части от външен отговор",
 	"Fluidly stream large external response chunks": "Плавно предаване на големи части от външен отговор",
 	"Focus chat input": "Фокусиране на чат вход",
 	"Focus chat input": "Фокусиране на чат вход",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "Следвайте инструкциите перфектно",
 	"Followed instructions perfectly": "Следвайте инструкциите перфектно",
 	"Form": "",
 	"Form": "",
 	"Format your variables using square brackets like this:": "Форматирайте вашите променливи, като използвате квадратни скоби, както следва:",
 	"Format your variables using square brackets like this:": "Форматирайте вашите променливи, като използвате квадратни скоби, както следва:",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "Модели Whitelisted",
 	"Model(s) Whitelisted": "Модели Whitelisted",
 	"Modelfile Content": "Съдържание на модфайл",
 	"Modelfile Content": "Съдържание на модфайл",
 	"Models": "Модели",
 	"Models": "Модели",
+	"more": "",
 	"More": "Повече",
 	"More": "Повече",
 	"Move to Top": "",
 	"Move to Top": "",
 	"Name": "Име",
 	"Name": "Име",
 	"Name your model": "Дайте име на вашия модел",
 	"Name your model": "Дайте име на вашия модел",
 	"New Chat": "Нов чат",
 	"New Chat": "Нов чат",
+	"New folder": "",
 	"New Password": "Нова парола",
 	"New Password": "Нова парола",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "",
 	"No content to speak": "",
+	"No distance available": "",
 	"No file selected": "",
 	"No file selected": "",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -532,9 +539,11 @@
 	"Record voice": "Записване на глас",
 	"Record voice": "Записване на глас",
 	"Redirecting you to OpenWebUI Community": "Пренасочване към OpenWebUI общността",
 	"Redirecting you to OpenWebUI Community": "Пренасочване към OpenWebUI общността",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
+	"References from": "",
 	"Refused when it shouldn't have": "Отказано, когато не трябва да бъде",
 	"Refused when it shouldn't have": "Отказано, когато не трябва да бъде",
 	"Regenerate": "Регенериране",
 	"Regenerate": "Регенериране",
 	"Release Notes": "Бележки по изданието",
 	"Release Notes": "Бележки по изданието",
+	"Relevance": "",
 	"Remove": "Изтриване",
 	"Remove": "Изтриване",
 	"Remove Model": "Изтриване на модела",
 	"Remove Model": "Изтриване на модела",
 	"Rename": "Преименуване",
 	"Rename": "Преименуване",
@@ -600,6 +609,7 @@
 	"Select model": "Изберете модел",
 	"Select model": "Изберете модел",
 	"Select only one model to call": "",
 	"Select only one model to call": "",
 	"Selected model(s) do not support image inputs": "Избраният(те) модел(и) не поддържа въвеждане на изображения",
 	"Selected model(s) do not support image inputs": "Избраният(те) модел(и) не поддържа въвеждане на изображения",
+	"Semantic distance to query": "",
 	"Send": "Изпрати",
 	"Send": "Изпрати",
 	"Send a Message": "Изпращане на Съобщение",
 	"Send a Message": "Изпращане на Съобщение",
 	"Send message": "Изпращане на съобщение",
 	"Send message": "Изпращане на съобщение",
@@ -680,6 +690,7 @@
 	"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 option will delete all existing files in the collection and replace them with newly uploaded files.": "",
 	"This will delete": "",
 	"This will delete": "",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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": "Това е подробно описание.",
 	"Thorough explanation": "Това е подробно описание.",
 	"Tika": "",
 	"Tika": "",

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

@@ -42,6 +42,7 @@
 	"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 Parameters": "এডভান্সড প্যারামিটার্স",
 	"Advanced Params": "অ্যাডভান্সড প্যারাম",
 	"Advanced Params": "অ্যাডভান্সড প্যারাম",
+	"All chats": "",
 	"All Documents": "সব ডকুমেন্ট",
 	"All Documents": "সব ডকুমেন্ট",
 	"All Users": "সব ইউজার",
 	"All Users": "সব ইউজার",
 	"Allow Chat Deletion": "চ্যাট ডিলিট করতে দিন",
 	"Allow Chat Deletion": "চ্যাট ডিলিট করতে দিন",
@@ -185,6 +186,7 @@
 	"Delete chat": "চ্যাট মুছে ফেলুন",
 	"Delete chat": "চ্যাট মুছে ফেলুন",
 	"Delete Chat": "চ্যাট মুছে ফেলুন",
 	"Delete Chat": "চ্যাট মুছে ফেলুন",
 	"Delete chat?": "",
 	"Delete chat?": "",
+	"Delete folder?": "",
 	"Delete function?": "",
 	"Delete function?": "",
 	"Delete prompt?": "",
 	"Delete prompt?": "",
 	"delete this link": "এই লিংক মুছে ফেলুন",
 	"delete this link": "এই লিংক মুছে ফেলুন",
@@ -220,9 +222,7 @@
 	"Download": "ডাউনলোড",
 	"Download": "ডাউনলোড",
 	"Download canceled": "ডাউনলোড বাতিল করা হয়েছে",
 	"Download canceled": "ডাউনলোড বাতিল করা হয়েছে",
 	"Download Database": "ডেটাবেজ ডাউনলোড করুন",
 	"Download Database": "ডেটাবেজ ডাউনলোড করুন",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "আলোচনায় যুক্ত করার জন্য যে কোন ফাইল এখানে ড্রপ করুন",
 	"Drop any files here to add to the conversation": "আলোচনায় যুক্ত করার জন্য যে কোন ফাইল এখানে ড্রপ করুন",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "যেমন '30s','10m'. সময়ের অনুমোদিত অনুমোদিত এককগুলি হচ্ছে 's', 'm', 'h'.",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "যেমন '30s','10m'. সময়ের অনুমোদিত অনুমোদিত এককগুলি হচ্ছে 's', 'm', 'h'.",
 	"Edit": "এডিট করুন",
 	"Edit": "এডিট করুন",
 	"Edit Memory": "",
 	"Edit Memory": "",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "ফিঙ্গারপ্রিন্ট স্পুফিং ধরা পড়েছে: অ্যাভাটার হিসেবে নামের আদ্যক্ষর ব্যবহার করা যাচ্ছে না। ডিফল্ট প্রোফাইল পিকচারে ফিরিয়ে নেয়া হচ্ছে।",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "ফিঙ্গারপ্রিন্ট স্পুফিং ধরা পড়েছে: অ্যাভাটার হিসেবে নামের আদ্যক্ষর ব্যবহার করা যাচ্ছে না। ডিফল্ট প্রোফাইল পিকচারে ফিরিয়ে নেয়া হচ্ছে।",
 	"Fluidly stream large external response chunks": "বড় এক্সটার্নাল রেসপন্স চাঙ্কগুলো মসৃণভাবে প্রবাহিত করুন",
 	"Fluidly stream large external response chunks": "বড় এক্সটার্নাল রেসপন্স চাঙ্কগুলো মসৃণভাবে প্রবাহিত করুন",
 	"Focus chat input": "চ্যাট ইনপুট ফোকাস করুন",
 	"Focus chat input": "চ্যাট ইনপুট ফোকাস করুন",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "নির্দেশাবলী নিখুঁতভাবে অনুসরণ করা হয়েছে",
 	"Followed instructions perfectly": "নির্দেশাবলী নিখুঁতভাবে অনুসরণ করা হয়েছে",
 	"Form": "",
 	"Form": "",
 	"Format your variables using square brackets like this:": "আপনার ভেরিয়বলগুলো এভাবে স্কয়ার ব্রাকেটের মাধ্যমে সাজান",
 	"Format your variables using square brackets like this:": "আপনার ভেরিয়বলগুলো এভাবে স্কয়ার ব্রাকেটের মাধ্যমে সাজান",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "হোয়াইটলিস্টেড মডেল(সমূহ)",
 	"Model(s) Whitelisted": "হোয়াইটলিস্টেড মডেল(সমূহ)",
 	"Modelfile Content": "মডেলফাইল কনটেন্ট",
 	"Modelfile Content": "মডেলফাইল কনটেন্ট",
 	"Models": "মডেলসমূহ",
 	"Models": "মডেলসমূহ",
+	"more": "",
 	"More": "আরো",
 	"More": "আরো",
 	"Move to Top": "",
 	"Move to Top": "",
 	"Name": "নাম",
 	"Name": "নাম",
 	"Name your model": "আপনার মডেলের নাম দিন",
 	"Name your model": "আপনার মডেলের নাম দিন",
 	"New Chat": "নতুন চ্যাট",
 	"New Chat": "নতুন চ্যাট",
+	"New folder": "",
 	"New Password": "নতুন পাসওয়ার্ড",
 	"New Password": "নতুন পাসওয়ার্ড",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "",
 	"No content to speak": "",
+	"No distance available": "",
 	"No file selected": "",
 	"No file selected": "",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -532,9 +539,11 @@
 	"Record voice": "ভয়েস রেকর্ড করুন",
 	"Record voice": "ভয়েস রেকর্ড করুন",
 	"Redirecting you to OpenWebUI Community": "আপনাকে OpenWebUI কমিউনিটিতে পাঠানো হচ্ছে",
 	"Redirecting you to OpenWebUI Community": "আপনাকে OpenWebUI কমিউনিটিতে পাঠানো হচ্ছে",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
+	"References from": "",
 	"Refused when it shouldn't have": "যদি উপযুক্ত নয়, তবে রেজিগেনেট করা হচ্ছে",
 	"Refused when it shouldn't have": "যদি উপযুক্ত নয়, তবে রেজিগেনেট করা হচ্ছে",
 	"Regenerate": "রেজিগেনেট করুন",
 	"Regenerate": "রেজিগেনেট করুন",
 	"Release Notes": "রিলিজ নোটসমূহ",
 	"Release Notes": "রিলিজ নোটসমূহ",
+	"Relevance": "",
 	"Remove": "রিমুভ করুন",
 	"Remove": "রিমুভ করুন",
 	"Remove Model": "মডেল রিমুভ করুন",
 	"Remove Model": "মডেল রিমুভ করুন",
 	"Rename": "রেনেম",
 	"Rename": "রেনেম",
@@ -600,6 +609,7 @@
 	"Select model": "মডেল নির্বাচন করুন",
 	"Select model": "মডেল নির্বাচন করুন",
 	"Select only one model to call": "",
 	"Select only one model to call": "",
 	"Selected model(s) do not support image inputs": "নির্বাচিত মডেল(গুলি) চিত্র ইনপুট সমর্থন করে না",
 	"Selected model(s) do not support image inputs": "নির্বাচিত মডেল(গুলি) চিত্র ইনপুট সমর্থন করে না",
+	"Semantic distance to query": "",
 	"Send": "পাঠান",
 	"Send": "পাঠান",
 	"Send a Message": "একটি মেসেজ পাঠান",
 	"Send a Message": "একটি মেসেজ পাঠান",
 	"Send message": "মেসেজ পাঠান",
 	"Send message": "মেসেজ পাঠান",
@@ -680,6 +690,7 @@
 	"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 option will delete all existing files in the collection and replace them with newly uploaded files.": "",
 	"This will delete": "",
 	"This will delete": "",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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": "পুঙ্খানুপুঙ্খ ব্যাখ্যা",
 	"Thorough explanation": "পুঙ্খানুপুঙ্খ ব্যাখ্যা",
 	"Tika": "",
 	"Tika": "",

+ 26 - 15
src/lib/i18n/locales/ca-ES/translation.json

@@ -42,6 +42,7 @@
 	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Els administradors tenen accés a totes les eines en tot moment; els usuaris necessiten eines assignades per model a l'espai de treball.",
 	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Els administradors tenen accés a totes les eines en tot moment; els usuaris necessiten eines assignades per model a l'espai de treball.",
 	"Advanced Parameters": "Paràmetres avançats",
 	"Advanced Parameters": "Paràmetres avançats",
 	"Advanced Params": "Paràmetres avançats",
 	"Advanced Params": "Paràmetres avançats",
+	"All chats": "Tots els xats",
 	"All Documents": "Tots els documents",
 	"All Documents": "Tots els documents",
 	"All Users": "Tots els usuaris",
 	"All Users": "Tots els usuaris",
 	"Allow Chat Deletion": "Permetre la supressió del xat",
 	"Allow Chat Deletion": "Permetre la supressió del xat",
@@ -95,7 +96,7 @@
 	"Cancel": "Cancel·lar",
 	"Cancel": "Cancel·lar",
 	"Capabilities": "Capacitats",
 	"Capabilities": "Capacitats",
 	"Change Password": "Canviar la contrasenya",
 	"Change Password": "Canviar la contrasenya",
-	"Character": "",
+	"Character": "Personatge",
 	"Chat": "Xat",
 	"Chat": "Xat",
 	"Chat Background Image": "Imatge de fons del xat",
 	"Chat Background Image": "Imatge de fons del xat",
 	"Chat Bubble UI": "Chat Bubble UI",
 	"Chat Bubble UI": "Chat Bubble UI",
@@ -124,7 +125,7 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Permís d'escriptura al porta-retalls denegat. Comprova els ajustos de navegador per donar l'accés necessari.",
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Permís d'escriptura al porta-retalls denegat. Comprova els ajustos de navegador per donar l'accés necessari.",
 	"Clone": "Clonar",
 	"Clone": "Clonar",
 	"Close": "Tancar",
 	"Close": "Tancar",
-	"Code execution": "",
+	"Code execution": "Execució de codi",
 	"Code formatted successfully": "Codi formatat correctament",
 	"Code formatted successfully": "Codi formatat correctament",
 	"Collection": "Col·lecció",
 	"Collection": "Col·lecció",
 	"ComfyUI": "ComfyUI",
 	"ComfyUI": "ComfyUI",
@@ -153,7 +154,7 @@
 	"Copy last code block": "Copiar l'últim bloc de codi",
 	"Copy last code block": "Copiar l'últim bloc de codi",
 	"Copy last response": "Copiar l'última resposta",
 	"Copy last response": "Copiar l'última resposta",
 	"Copy Link": "Copiar l'enllaç",
 	"Copy Link": "Copiar l'enllaç",
-	"Copy to clipboard": "",
+	"Copy to clipboard": "Copiar al porta-retalls",
 	"Copying to clipboard was successful!": "La còpia al porta-retalls s'ha realitzat correctament",
 	"Copying to clipboard was successful!": "La còpia al porta-retalls s'ha realitzat correctament",
 	"Create a model": "Crear un model",
 	"Create a model": "Crear un model",
 	"Create Account": "Crear un compte",
 	"Create Account": "Crear un compte",
@@ -185,6 +186,7 @@
 	"Delete chat": "Eliminar xat",
 	"Delete chat": "Eliminar xat",
 	"Delete Chat": "Eliminar xat",
 	"Delete Chat": "Eliminar xat",
 	"Delete chat?": "Eliminar el xat?",
 	"Delete chat?": "Eliminar el xat?",
+	"Delete folder?": "",
 	"Delete function?": "Eliminar funció?",
 	"Delete function?": "Eliminar funció?",
 	"Delete prompt?": "Eliminar indicació?",
 	"Delete prompt?": "Eliminar indicació?",
 	"delete this link": "Eliminar aquest enllaç",
 	"delete this link": "Eliminar aquest enllaç",
@@ -220,9 +222,7 @@
 	"Download": "Descarregar",
 	"Download": "Descarregar",
 	"Download canceled": "Descàrrega cancel·lada",
 	"Download canceled": "Descàrrega cancel·lada",
 	"Download Database": "Descarregar la base de dades",
 	"Download Database": "Descarregar la base de dades",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "Deixa qualsevol arxiu aquí per afegir-lo a la conversa",
 	"Drop any files here to add to the conversation": "Deixa qualsevol arxiu aquí per afegir-lo a la conversa",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p. ex. '30s','10m'. Les unitats de temps vàlides són 's', 'm', 'h'.",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p. ex. '30s','10m'. Les unitats de temps vàlides són 's', 'm', 'h'.",
 	"Edit": "Editar",
 	"Edit": "Editar",
 	"Edit Memory": "Editar la memòria",
 	"Edit Memory": "Editar la memòria",
@@ -278,7 +278,7 @@
 	"Enter Your Password": "Introdueix la teva contrasenya",
 	"Enter Your Password": "Introdueix la teva contrasenya",
 	"Enter Your Role": "Introdueix el teu rol",
 	"Enter Your Role": "Introdueix el teu rol",
 	"Error": "Error",
 	"Error": "Error",
-	"ERROR": "",
+	"ERROR": "ERROR",
 	"Experimental": "Experimental",
 	"Experimental": "Experimental",
 	"Export": "Exportar",
 	"Export": "Exportar",
 	"Export All Chats (All Users)": "Exportar tots els xats (Tots els usuaris)",
 	"Export All Chats (All Users)": "Exportar tots els xats (Tots els usuaris)",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "S'ha detectat la suplantació d'identitat de l'empremta digital: no es poden utilitzar les inicials com a avatar. S'estableix la imatge de perfil predeterminada.",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "S'ha detectat la suplantació d'identitat de l'empremta digital: no es poden utilitzar les inicials com a avatar. S'estableix la imatge de perfil predeterminada.",
 	"Fluidly stream large external response chunks": "Transmetre amb fluïdesa grans trossos de resposta externa",
 	"Fluidly stream large external response chunks": "Transmetre amb fluïdesa grans trossos de resposta externa",
 	"Focus chat input": "Estableix el focus a l'entrada del xat",
 	"Focus chat input": "Estableix el focus a l'entrada del xat",
+	"Folder deleted successfully": "Carpeta eliminada correctament",
+	"Folder name cannot be empty": "El nom de la carpeta no pot ser buit",
+	"Folder name cannot be empty.": "El nom de la carpeta no pot ser buit.",
+	"Folder name updated successfully": "Nom de la carpeta actualitzat correctament",
 	"Followed instructions perfectly": "S'han seguit les instruccions perfectament",
 	"Followed instructions perfectly": "S'han seguit les instruccions perfectament",
 	"Form": "Formulari",
 	"Form": "Formulari",
 	"Format your variables using square brackets like this:": "Formata les teves variables utilitzant claudàtors així:",
 	"Format your variables using square brackets like this:": "Formata les teves variables utilitzant claudàtors així:",
@@ -365,7 +369,7 @@
 	"Install from Github URL": "Instal·lar des de l'URL de Github",
 	"Install from Github URL": "Instal·lar des de l'URL de Github",
 	"Instant Auto-Send After Voice Transcription": "Enviament automàtic després de la transcripció de veu",
 	"Instant Auto-Send After Voice Transcription": "Enviament automàtic després de la transcripció de veu",
 	"Interface": "Interfície",
 	"Interface": "Interfície",
-	"Invalid file format.": "",
+	"Invalid file format.": "Format d'arxiu no vàlid.",
 	"Invalid Tag": "Etiqueta no vàlida",
 	"Invalid Tag": "Etiqueta no vàlida",
 	"January": "Gener",
 	"January": "Gener",
 	"join our Discord for help.": "uneix-te al nostre Discord per obtenir ajuda.",
 	"join our Discord for help.": "uneix-te al nostre Discord per obtenir ajuda.",
@@ -440,16 +444,19 @@
 	"Model(s) Whitelisted": "Model(s) a la llista blanca",
 	"Model(s) Whitelisted": "Model(s) a la llista blanca",
 	"Modelfile Content": "Contingut del Modelfile",
 	"Modelfile Content": "Contingut del Modelfile",
 	"Models": "Models",
 	"Models": "Models",
+	"more": "més",
 	"More": "Més",
 	"More": "Més",
 	"Move to Top": "Moure a dalt de tot",
 	"Move to Top": "Moure a dalt de tot",
 	"Name": "Nom",
 	"Name": "Nom",
 	"Name your model": "Posa un nom al teu model",
 	"Name your model": "Posa un nom al teu model",
 	"New Chat": "Nou xat",
 	"New Chat": "Nou xat",
+	"New folder": "Nova carpeta",
 	"New Password": "Nova contrasenya",
 	"New Password": "Nova contrasenya",
 	"No content found": "No s'ha trobat contingut",
 	"No content found": "No s'ha trobat contingut",
 	"No content to speak": "No hi ha contingut per parlar",
 	"No content to speak": "No hi ha contingut per parlar",
+	"No distance available": "No hi ha distància disponible",
 	"No file selected": "No s'ha escollit cap fitxer",
 	"No file selected": "No s'ha escollit cap fitxer",
-	"No files found.": "",
+	"No files found.": "No s'han trobat arxius.",
 	"No HTML, CSS, or JavaScript content found.": "No s'ha trobat contingut HTML, CSS o JavaScript.",
 	"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 knowledge found": "No s'ha trobat Coneixement",
 	"No results found": "No s'han trobat resultats",
 	"No results found": "No s'han trobat resultats",
@@ -492,7 +499,7 @@
 	"OpenAI URL/Key required.": "URL/Clau d'OpenAI requerides.",
 	"OpenAI URL/Key required.": "URL/Clau d'OpenAI requerides.",
 	"or": "o",
 	"or": "o",
 	"Other": "Altres",
 	"Other": "Altres",
-	"OUTPUT": "",
+	"OUTPUT": "SORTIDA",
 	"Output format": "Format de sortida",
 	"Output format": "Format de sortida",
 	"Overview": "Vista general",
 	"Overview": "Vista general",
 	"page": "pàgina",
 	"page": "pàgina",
@@ -532,9 +539,11 @@
 	"Record voice": "Enregistrar la veu",
 	"Record voice": "Enregistrar la veu",
 	"Redirecting you to OpenWebUI Community": "Redirigint-te a la comunitat OpenWebUI",
 	"Redirecting you to OpenWebUI Community": "Redirigint-te a la comunitat OpenWebUI",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Fes referència a tu mateix com a \"Usuari\" (p. ex., \"L'usuari està aprenent espanyol\")",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Fes referència a tu mateix com a \"Usuari\" (p. ex., \"L'usuari està aprenent espanyol\")",
+	"References from": "Referències de",
 	"Refused when it shouldn't have": "Refusat quan no hauria d'haver estat",
 	"Refused when it shouldn't have": "Refusat quan no hauria d'haver estat",
 	"Regenerate": "Regenerar",
 	"Regenerate": "Regenerar",
 	"Release Notes": "Notes de la versió",
 	"Release Notes": "Notes de la versió",
+	"Relevance": "Rellevància",
 	"Remove": "Eliminar",
 	"Remove": "Eliminar",
 	"Remove Model": "Eliminar el model",
 	"Remove Model": "Eliminar el model",
 	"Rename": "Canviar el nom",
 	"Rename": "Canviar el nom",
@@ -545,7 +554,7 @@
 	"Reranking model set to \"{{reranking_model}}\"": "Model de reavaluació establert a \"{{reranking_model}}\"",
 	"Reranking model set to \"{{reranking_model}}\"": "Model de reavaluació establert a \"{{reranking_model}}\"",
 	"Reset": "Restableix",
 	"Reset": "Restableix",
 	"Reset Upload Directory": "Restableix el directori de pujades",
 	"Reset Upload Directory": "Restableix el directori de pujades",
-	"Reset Vector Storage/Knowledge": "",
+	"Reset Vector Storage/Knowledge": "Restableix el Repositori de vectors/Coneixement",
 	"Response AutoCopy to Clipboard": "Copiar la resposta automàticament al porta-retalls",
 	"Response AutoCopy to Clipboard": "Copiar la resposta automàticament al porta-retalls",
 	"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Les notifications de resposta no es poden activar perquè els permisos del lloc web han estat rebutjats. Comprova les preferències del navegador per donar l'accés necessari.",
 	"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Les notifications de resposta no es poden activar perquè els permisos del lloc web han estat rebutjats. Comprova les preferències del navegador per donar l'accés necessari.",
 	"Response splitting": "Divisió de la resposta",
 	"Response splitting": "Divisió de la resposta",
@@ -568,7 +577,7 @@
 	"Search a model": "Cercar un model",
 	"Search a model": "Cercar un model",
 	"Search Chats": "Cercar xats",
 	"Search Chats": "Cercar xats",
 	"Search Collection": "Cercar col·leccions",
 	"Search Collection": "Cercar col·leccions",
-	"search for tags": "",
+	"search for tags": "cercar etiquetes",
 	"Search Functions": "Cercar funcions",
 	"Search Functions": "Cercar funcions",
 	"Search Knowledge": "Cercar coneixement",
 	"Search Knowledge": "Cercar coneixement",
 	"Search Models": "Cercar models",
 	"Search Models": "Cercar models",
@@ -601,6 +610,7 @@
 	"Select model": "Seleccionar un model",
 	"Select model": "Seleccionar un model",
 	"Select only one model to call": "Seleccionar només un model per trucar",
 	"Select only one model to call": "Seleccionar només un model per trucar",
 	"Selected model(s) do not support image inputs": "El(s) model(s) seleccionats no admeten l'entrada d'imatges",
 	"Selected model(s) do not support image inputs": "El(s) model(s) seleccionats no admeten l'entrada d'imatges",
+	"Semantic distance to query": "",
 	"Send": "Enviar",
 	"Send": "Enviar",
 	"Send a Message": "Enviar un missatge",
 	"Send a Message": "Enviar un missatge",
 	"Send message": "Enviar missatge",
 	"Send message": "Enviar missatge",
@@ -643,7 +653,7 @@
 	"Speech Playback Speed": "Velocitat de la parla",
 	"Speech Playback Speed": "Velocitat de la parla",
 	"Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}",
 	"Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}",
 	"Speech-to-Text Engine": "Motor de veu a text",
 	"Speech-to-Text Engine": "Motor de veu a text",
-	"Stop": "",
+	"Stop": "Atura",
 	"Stop Sequence": "Atura la seqüència",
 	"Stop Sequence": "Atura la seqüència",
 	"Stream Chat Response": "Fer streaming de la resposta del xat",
 	"Stream Chat Response": "Fer streaming de la resposta del xat",
 	"STT Model": "Model SST",
 	"STT Model": "Model SST",
@@ -666,7 +676,7 @@
 	"Template": "Plantilla",
 	"Template": "Plantilla",
 	"Temporary Chat": "Xat temporal",
 	"Temporary Chat": "Xat temporal",
 	"Text Completion": "Completament de text",
 	"Text Completion": "Completament de text",
-	"Text Splitter": "",
+	"Text Splitter": "Separador de text",
 	"Text-to-Speech Engine": "Motor de text a veu",
 	"Text-to-Speech Engine": "Motor de text a veu",
 	"Tfs Z": "Tfs Z",
 	"Tfs Z": "Tfs Z",
 	"Thanks for your feedback!": "Gràcies pel teu comentari!",
 	"Thanks for your feedback!": "Gràcies pel teu comentari!",
@@ -681,11 +691,12 @@
 	"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 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.": "Aquesta opció eliminarà tots els fitxers existents de la col·lecció i els substituirà per fitxers recentment penjats.",
 	"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 delete": "Això eliminarà",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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?",
 	"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",
 	"Thorough explanation": "Explicació en detall",
 	"Tika": "Tika",
 	"Tika": "Tika",
 	"Tika Server URL required.": "La URL del servidor Tika és obligatòria.",
 	"Tika Server URL required.": "La URL del servidor Tika és obligatòria.",
-	"Tiktoken": "",
+	"Tiktoken": "Tiktoken",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Consell: Actualitza les diverses variables consecutivament prement la tecla de tabulació en l'entrada del xat després de cada reemplaçament.",
 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Consell: Actualitza les diverses variables consecutivament prement la tecla de tabulació en l'entrada del xat després de cada reemplaçament.",
 	"Title": "Títol",
 	"Title": "Títol",
 	"Title (e.g. Tell me a fun fact)": "Títol (p. ex. Digues-me quelcom divertit)",
 	"Title (e.g. Tell me a fun fact)": "Títol (p. ex. Digues-me quelcom divertit)",
@@ -703,7 +714,7 @@
 	"Today": "Avui",
 	"Today": "Avui",
 	"Toggle settings": "Alterna preferències",
 	"Toggle settings": "Alterna preferències",
 	"Toggle sidebar": "Alterna la barra lateral",
 	"Toggle sidebar": "Alterna la barra lateral",
-	"Token": "",
+	"Token": "Token",
 	"Tokens To Keep On Context Refresh (num_keep)": "Tokens a mantenir en l'actualització del context (num_keep)",
 	"Tokens To Keep On Context Refresh (num_keep)": "Tokens a mantenir en l'actualització del context (num_keep)",
 	"Tool created successfully": "Eina creada correctament",
 	"Tool created successfully": "Eina creada correctament",
 	"Tool deleted successfully": "Eina eliminada correctament",
 	"Tool deleted successfully": "Eina eliminada correctament",

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

@@ -42,6 +42,7 @@
 	"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 settings",
 	"Advanced Parameters": "advanced settings",
 	"Advanced Params": "",
 	"Advanced Params": "",
+	"All chats": "",
 	"All Documents": "",
 	"All Documents": "",
 	"All Users": "Ang tanan nga mga tiggamit",
 	"All Users": "Ang tanan nga mga tiggamit",
 	"Allow Chat Deletion": "Tugoti nga mapapas ang mga chat",
 	"Allow Chat Deletion": "Tugoti nga mapapas ang mga chat",
@@ -185,6 +186,7 @@
 	"Delete chat": "Pagtangtang sa panaghisgot",
 	"Delete chat": "Pagtangtang sa panaghisgot",
 	"Delete Chat": "",
 	"Delete Chat": "",
 	"Delete chat?": "",
 	"Delete chat?": "",
+	"Delete folder?": "",
 	"Delete function?": "",
 	"Delete function?": "",
 	"Delete prompt?": "",
 	"Delete prompt?": "",
 	"delete this link": "",
 	"delete this link": "",
@@ -220,9 +222,7 @@
 	"Download": "",
 	"Download": "",
 	"Download canceled": "",
 	"Download canceled": "",
 	"Download Database": "I-download ang database",
 	"Download Database": "I-download ang database",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "Ihulog ang bisan unsang file dinhi aron idugang kini sa panag-istoryahanay",
 	"Drop any files here to add to the conversation": "Ihulog ang bisan unsang file dinhi aron idugang kini sa panag-istoryahanay",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p. ",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p. ",
 	"Edit": "",
 	"Edit": "",
 	"Edit Memory": "",
 	"Edit Memory": "",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
 	"Fluidly stream large external response chunks": "Hapsay nga paghatud sa daghang mga tipik sa eksternal nga mga tubag",
 	"Fluidly stream large external response chunks": "Hapsay nga paghatud sa daghang mga tipik sa eksternal nga mga tubag",
 	"Focus chat input": "Pag-focus sa entry sa diskusyon",
 	"Focus chat input": "Pag-focus sa entry sa diskusyon",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "",
 	"Followed instructions perfectly": "",
 	"Form": "",
 	"Form": "",
 	"Format your variables using square brackets like this:": "I-format ang imong mga variable gamit ang square brackets sama niini:",
 	"Format your variables using square brackets like this:": "I-format ang imong mga variable gamit ang square brackets sama niini:",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "Gi-whitelist nga (mga) modelo",
 	"Model(s) Whitelisted": "Gi-whitelist nga (mga) modelo",
 	"Modelfile Content": "Mga sulod sa template file",
 	"Modelfile Content": "Mga sulod sa template file",
 	"Models": "Mga modelo",
 	"Models": "Mga modelo",
+	"more": "",
 	"More": "",
 	"More": "",
 	"Move to Top": "",
 	"Move to Top": "",
 	"Name": "Ngalan",
 	"Name": "Ngalan",
 	"Name your model": "",
 	"Name your model": "",
 	"New Chat": "Bag-ong diskusyon",
 	"New Chat": "Bag-ong diskusyon",
+	"New folder": "",
 	"New Password": "Bag-ong Password",
 	"New Password": "Bag-ong Password",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "",
 	"No content to speak": "",
+	"No distance available": "",
 	"No file selected": "",
 	"No file selected": "",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -532,9 +539,11 @@
 	"Record voice": "Irekord ang tingog",
 	"Record voice": "Irekord ang tingog",
 	"Redirecting you to OpenWebUI Community": "Gi-redirect ka sa komunidad sa OpenWebUI",
 	"Redirecting you to OpenWebUI Community": "Gi-redirect ka sa komunidad sa OpenWebUI",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
+	"References from": "",
 	"Refused when it shouldn't have": "",
 	"Refused when it shouldn't have": "",
 	"Regenerate": "",
 	"Regenerate": "",
 	"Release Notes": "Release Notes",
 	"Release Notes": "Release Notes",
+	"Relevance": "",
 	"Remove": "",
 	"Remove": "",
 	"Remove Model": "",
 	"Remove Model": "",
 	"Rename": "",
 	"Rename": "",
@@ -600,6 +609,7 @@
 	"Select model": "Pagpili og modelo",
 	"Select model": "Pagpili og modelo",
 	"Select only one model to call": "",
 	"Select only one model to call": "",
 	"Selected model(s) do not support image inputs": "",
 	"Selected model(s) do not support image inputs": "",
+	"Semantic distance to query": "",
 	"Send": "",
 	"Send": "",
 	"Send a Message": "Magpadala ug mensahe",
 	"Send a Message": "Magpadala ug mensahe",
 	"Send message": "Magpadala ug mensahe",
 	"Send message": "Magpadala ug mensahe",
@@ -680,6 +690,7 @@
 	"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 option will delete all existing files in the collection and replace them with newly uploaded files.": "",
 	"This will delete": "",
 	"This will delete": "",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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": "",
 	"Thorough explanation": "",
 	"Tika": "",
 	"Tika": "",

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

@@ -42,6 +42,7 @@
 	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Administratoren haben jederzeit Zugriff auf alle Werkzeuge. Benutzer können im Arbeitsbereich zugewiesen.",
 	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Administratoren haben jederzeit Zugriff auf alle Werkzeuge. Benutzer können im Arbeitsbereich zugewiesen.",
 	"Advanced Parameters": "Erweiterte Parameter",
 	"Advanced Parameters": "Erweiterte Parameter",
 	"Advanced Params": "Erweiterte Parameter",
 	"Advanced Params": "Erweiterte Parameter",
+	"All chats": "",
 	"All Documents": "Alle Dokumente",
 	"All Documents": "Alle Dokumente",
 	"All Users": "Alle Benutzer",
 	"All Users": "Alle Benutzer",
 	"Allow Chat Deletion": "Unterhaltungen löschen erlauben",
 	"Allow Chat Deletion": "Unterhaltungen löschen erlauben",
@@ -185,6 +186,7 @@
 	"Delete chat": "Unterhaltung löschen",
 	"Delete chat": "Unterhaltung löschen",
 	"Delete Chat": "Unterhaltung löschen",
 	"Delete Chat": "Unterhaltung löschen",
 	"Delete chat?": "Unterhaltung löschen?",
 	"Delete chat?": "Unterhaltung löschen?",
+	"Delete folder?": "",
 	"Delete function?": "Funktion löschen?",
 	"Delete function?": "Funktion löschen?",
 	"Delete prompt?": "Prompt löschen?",
 	"Delete prompt?": "Prompt löschen?",
 	"delete this link": "diesen Link löschen",
 	"delete this link": "diesen Link löschen",
@@ -220,9 +222,7 @@
 	"Download": "Exportieren",
 	"Download": "Exportieren",
 	"Download canceled": "Exportierung abgebrochen",
 	"Download canceled": "Exportierung abgebrochen",
 	"Download Database": "Datenbank exportieren",
 	"Download Database": "Datenbank exportieren",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "Ziehen Sie beliebige Dateien hierher, um sie der Unterhaltung hinzuzufügen",
 	"Drop any files here to add to the conversation": "Ziehen Sie beliebige Dateien hierher, um sie der Unterhaltung hinzuzufügen",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "z. B. '30s','10m'. Gültige Zeiteinheiten sind 's', 'm', 'h'.",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "z. B. '30s','10m'. Gültige Zeiteinheiten sind 's', 'm', 'h'.",
 	"Edit": "Bearbeiten",
 	"Edit": "Bearbeiten",
 	"Edit Memory": "Erinnerungen bearbeiten",
 	"Edit Memory": "Erinnerungen bearbeiten",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Fingerabdruck-Spoofing erkannt: Initialen können nicht als Avatar verwendet werden. Standard-Avatar wird verwendet.",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Fingerabdruck-Spoofing erkannt: Initialen können nicht als Avatar verwendet werden. Standard-Avatar wird verwendet.",
 	"Fluidly stream large external response chunks": "Nahtlose Übertragung großer externer Antwortabschnitte",
 	"Fluidly stream large external response chunks": "Nahtlose Übertragung großer externer Antwortabschnitte",
 	"Focus chat input": "Chat-Eingabe fokussieren",
 	"Focus chat input": "Chat-Eingabe fokussieren",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "Anweisungen perfekt befolgt",
 	"Followed instructions perfectly": "Anweisungen perfekt befolgt",
 	"Form": "Formular",
 	"Form": "Formular",
 	"Format your variables using square brackets like this:": "Formatieren Sie Ihre Variablen mit eckigen Klammern wie folgt:",
 	"Format your variables using square brackets like this:": "Formatieren Sie Ihre Variablen mit eckigen Klammern wie folgt:",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "Modell(e) auf der Whitelist",
 	"Model(s) Whitelisted": "Modell(e) auf der Whitelist",
 	"Modelfile Content": "Modelfile-Inhalt",
 	"Modelfile Content": "Modelfile-Inhalt",
 	"Models": "Modelle",
 	"Models": "Modelle",
+	"more": "mehr",
 	"More": "Mehr",
 	"More": "Mehr",
 	"Move to Top": "",
 	"Move to Top": "",
 	"Name": "Name",
 	"Name": "Name",
 	"Name your model": "Benennen Sie Ihr Modell",
 	"Name your model": "Benennen Sie Ihr Modell",
 	"New Chat": "Neue Unterhaltung",
 	"New Chat": "Neue Unterhaltung",
+	"New folder": "",
 	"New Password": "Neues Passwort",
 	"New Password": "Neues Passwort",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "Kein Inhalt zum Vorlesen",
 	"No content to speak": "Kein Inhalt zum Vorlesen",
+	"No distance available": "",
 	"No file selected": "Keine Datei ausgewählt",
 	"No file selected": "Keine Datei ausgewählt",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -532,9 +539,11 @@
 	"Record voice": "Stimme aufnehmen",
 	"Record voice": "Stimme aufnehmen",
 	"Redirecting you to OpenWebUI Community": "Sie werden zur OpenWebUI-Community weitergeleitet",
 	"Redirecting you to OpenWebUI Community": "Sie werden zur OpenWebUI-Community weitergeleitet",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
+	"References from": "Referenzen aus",
 	"Refused when it shouldn't have": "Abgelehnt, obwohl es nicht hätte abgelehnt werden sollen",
 	"Refused when it shouldn't have": "Abgelehnt, obwohl es nicht hätte abgelehnt werden sollen",
 	"Regenerate": "Neu generieren",
 	"Regenerate": "Neu generieren",
 	"Release Notes": "Veröffentlichungshinweise",
 	"Release Notes": "Veröffentlichungshinweise",
+	"Relevance": "",
 	"Remove": "Entfernen",
 	"Remove": "Entfernen",
 	"Remove Model": "Modell entfernen",
 	"Remove Model": "Modell entfernen",
 	"Rename": "Umbenennen",
 	"Rename": "Umbenennen",
@@ -600,6 +609,7 @@
 	"Select model": "Modell auswählen",
 	"Select model": "Modell auswählen",
 	"Select only one model to call": "Wählen Sie nur ein Modell zum Anrufen aus",
 	"Select only one model to call": "Wählen Sie nur ein Modell zum Anrufen aus",
 	"Selected model(s) do not support image inputs": "Ihre ausgewählten Modelle unterstützen keine Bildeingaben",
 	"Selected model(s) do not support image inputs": "Ihre ausgewählten Modelle unterstützen keine Bildeingaben",
+	"Semantic distance to query": "",
 	"Send": "Senden",
 	"Send": "Senden",
 	"Send a Message": "Eine Nachricht senden",
 	"Send a Message": "Eine Nachricht senden",
 	"Send message": "Nachricht senden",
 	"Send message": "Nachricht senden",
@@ -680,6 +690,7 @@
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "Dies ist eine experimentelle Funktion, sie funktioniert möglicherweise nicht wie erwartet und kann jederzeit geändert werden.",
 	"This is an experimental feature, it may not function as expected and is subject to change at any time.": "Dies ist eine experimentelle Funktion, sie funktioniert möglicherweise nicht wie erwartet und kann jederzeit geändert werden.",
 	"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.": "",
 	"This will delete": "Dies löscht",
 	"This will delete": "Dies löscht",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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": "Ausführliche Erklärung",
 	"Thorough explanation": "Ausführliche Erklärung",
 	"Tika": "Tika",
 	"Tika": "Tika",

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

@@ -42,6 +42,7 @@
 	"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 Parameters",
 	"Advanced Parameters": "Advanced Parameters",
 	"Advanced Params": "",
 	"Advanced Params": "",
+	"All chats": "",
 	"All Documents": "",
 	"All Documents": "",
 	"All Users": "All Users",
 	"All Users": "All Users",
 	"Allow Chat Deletion": "Allow Delete Chats",
 	"Allow Chat Deletion": "Allow Delete Chats",
@@ -185,6 +186,7 @@
 	"Delete chat": "Delete chat",
 	"Delete chat": "Delete chat",
 	"Delete Chat": "",
 	"Delete Chat": "",
 	"Delete chat?": "",
 	"Delete chat?": "",
+	"Delete folder?": "",
 	"Delete function?": "",
 	"Delete function?": "",
 	"Delete prompt?": "",
 	"Delete prompt?": "",
 	"delete this link": "",
 	"delete this link": "",
@@ -220,9 +222,7 @@
 	"Download": "",
 	"Download": "",
 	"Download canceled": "",
 	"Download canceled": "",
 	"Download Database": "Download Database",
 	"Download Database": "Download Database",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "Drop files here to add to conversation",
 	"Drop any files here to add to the conversation": "Drop files here to add to conversation",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. Much time units are 's', 'm', 'h'.",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. Much time units are 's', 'm', 'h'.",
 	"Edit": "",
 	"Edit": "",
 	"Edit Memory": "",
 	"Edit Memory": "",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Fingerprint dogeing: Unable to use initials as avatar. Defaulting to default doge image.",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Fingerprint dogeing: Unable to use initials as avatar. Defaulting to default doge image.",
 	"Fluidly stream large external response chunks": "Fluidly wow big chunks",
 	"Fluidly stream large external response chunks": "Fluidly wow big chunks",
 	"Focus chat input": "Focus chat bork",
 	"Focus chat input": "Focus chat bork",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "",
 	"Followed instructions perfectly": "",
 	"Form": "",
 	"Form": "",
 	"Format your variables using square brackets like this:": "Format variables using square brackets like wow:",
 	"Format your variables using square brackets like this:": "Format variables using square brackets like wow:",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "Wowdel(s) Whitelisted",
 	"Model(s) Whitelisted": "Wowdel(s) Whitelisted",
 	"Modelfile Content": "Modelfile Content",
 	"Modelfile Content": "Modelfile Content",
 	"Models": "Wowdels",
 	"Models": "Wowdels",
+	"more": "",
 	"More": "",
 	"More": "",
 	"Move to Top": "",
 	"Move to Top": "",
 	"Name": "Name",
 	"Name": "Name",
 	"Name your model": "",
 	"Name your model": "",
 	"New Chat": "New Bark",
 	"New Chat": "New Bark",
+	"New folder": "",
 	"New Password": "New Barkword",
 	"New Password": "New Barkword",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "",
 	"No content to speak": "",
+	"No distance available": "",
 	"No file selected": "",
 	"No file selected": "",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -532,9 +539,11 @@
 	"Record voice": "Record Bark",
 	"Record voice": "Record Bark",
 	"Redirecting you to OpenWebUI Community": "Redirecting you to OpenWebUI Community",
 	"Redirecting you to OpenWebUI Community": "Redirecting you to OpenWebUI Community",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
+	"References from": "",
 	"Refused when it shouldn't have": "",
 	"Refused when it shouldn't have": "",
 	"Regenerate": "",
 	"Regenerate": "",
 	"Release Notes": "Release Borks",
 	"Release Notes": "Release Borks",
+	"Relevance": "",
 	"Remove": "",
 	"Remove": "",
 	"Remove Model": "",
 	"Remove Model": "",
 	"Rename": "",
 	"Rename": "",
@@ -602,6 +611,7 @@
 	"Select model": "Select model much choice",
 	"Select model": "Select model much choice",
 	"Select only one model to call": "",
 	"Select only one model to call": "",
 	"Selected model(s) do not support image inputs": "",
 	"Selected model(s) do not support image inputs": "",
+	"Semantic distance to query": "",
 	"Send": "",
 	"Send": "",
 	"Send a Message": "Send a Message much message",
 	"Send a Message": "Send a Message much message",
 	"Send message": "Send message very send",
 	"Send message": "Send message very send",
@@ -682,6 +692,7 @@
 	"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 option will delete all existing files in the collection and replace them with newly uploaded files.": "",
 	"This will delete": "",
 	"This will delete": "",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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": "",
 	"Thorough explanation": "",
 	"Tika": "",
 	"Tika": "",

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

@@ -42,6 +42,7 @@
 	"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 Parameters": "",
 	"Advanced Params": "",
 	"Advanced Params": "",
+	"All chats": "",
 	"All Documents": "",
 	"All Documents": "",
 	"All Users": "",
 	"All Users": "",
 	"Allow Chat Deletion": "",
 	"Allow Chat Deletion": "",
@@ -185,6 +186,7 @@
 	"Delete chat": "",
 	"Delete chat": "",
 	"Delete Chat": "",
 	"Delete Chat": "",
 	"Delete chat?": "",
 	"Delete chat?": "",
+	"Delete folder?": "",
 	"Delete function?": "",
 	"Delete function?": "",
 	"Delete prompt?": "",
 	"Delete prompt?": "",
 	"delete this link": "",
 	"delete this link": "",
@@ -220,9 +222,7 @@
 	"Download": "",
 	"Download": "",
 	"Download canceled": "",
 	"Download canceled": "",
 	"Download Database": "",
 	"Download Database": "",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "",
 	"Drop any files here to add to the conversation": "",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "",
 	"Edit": "",
 	"Edit": "",
 	"Edit Memory": "",
 	"Edit Memory": "",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
 	"Fluidly stream large external response chunks": "",
 	"Fluidly stream large external response chunks": "",
 	"Focus chat input": "",
 	"Focus chat input": "",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "",
 	"Followed instructions perfectly": "",
 	"Form": "",
 	"Form": "",
 	"Format your variables using square brackets like this:": "",
 	"Format your variables using square brackets like this:": "",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "",
 	"Model(s) Whitelisted": "",
 	"Modelfile Content": "",
 	"Modelfile Content": "",
 	"Models": "",
 	"Models": "",
+	"more": "",
 	"More": "",
 	"More": "",
 	"Move to Top": "",
 	"Move to Top": "",
 	"Name": "",
 	"Name": "",
 	"Name your model": "",
 	"Name your model": "",
 	"New Chat": "",
 	"New Chat": "",
+	"New folder": "",
 	"New Password": "",
 	"New Password": "",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "",
 	"No content to speak": "",
+	"No distance available": "",
 	"No file selected": "",
 	"No file selected": "",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -532,9 +539,11 @@
 	"Record voice": "",
 	"Record voice": "",
 	"Redirecting you to OpenWebUI Community": "",
 	"Redirecting you to OpenWebUI Community": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
+	"References from": "",
 	"Refused when it shouldn't have": "",
 	"Refused when it shouldn't have": "",
 	"Regenerate": "",
 	"Regenerate": "",
 	"Release Notes": "",
 	"Release Notes": "",
+	"Relevance": "",
 	"Remove": "",
 	"Remove": "",
 	"Remove Model": "",
 	"Remove Model": "",
 	"Rename": "",
 	"Rename": "",
@@ -600,6 +609,7 @@
 	"Select model": "",
 	"Select model": "",
 	"Select only one model to call": "",
 	"Select only one model to call": "",
 	"Selected model(s) do not support image inputs": "",
 	"Selected model(s) do not support image inputs": "",
+	"Semantic distance to query": "",
 	"Send": "",
 	"Send": "",
 	"Send a Message": "",
 	"Send a Message": "",
 	"Send message": "",
 	"Send message": "",
@@ -680,6 +690,7 @@
 	"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 option will delete all existing files in the collection and replace them with newly uploaded files.": "",
 	"This will delete": "",
 	"This will delete": "",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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": "",
 	"Thorough explanation": "",
 	"Tika": "",
 	"Tika": "",

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

@@ -42,6 +42,7 @@
 	"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 Parameters": "",
 	"Advanced Params": "",
 	"Advanced Params": "",
+	"All chats": "",
 	"All Documents": "",
 	"All Documents": "",
 	"All Users": "",
 	"All Users": "",
 	"Allow Chat Deletion": "",
 	"Allow Chat Deletion": "",
@@ -185,6 +186,7 @@
 	"Delete chat": "",
 	"Delete chat": "",
 	"Delete Chat": "",
 	"Delete Chat": "",
 	"Delete chat?": "",
 	"Delete chat?": "",
+	"Delete folder?": "",
 	"Delete function?": "",
 	"Delete function?": "",
 	"Delete prompt?": "",
 	"Delete prompt?": "",
 	"delete this link": "",
 	"delete this link": "",
@@ -220,9 +222,7 @@
 	"Download": "",
 	"Download": "",
 	"Download canceled": "",
 	"Download canceled": "",
 	"Download Database": "",
 	"Download Database": "",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "",
 	"Drop any files here to add to the conversation": "",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "",
 	"Edit": "",
 	"Edit": "",
 	"Edit Memory": "",
 	"Edit Memory": "",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
 	"Fluidly stream large external response chunks": "",
 	"Fluidly stream large external response chunks": "",
 	"Focus chat input": "",
 	"Focus chat input": "",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "",
 	"Followed instructions perfectly": "",
 	"Form": "",
 	"Form": "",
 	"Format your variables using square brackets like this:": "",
 	"Format your variables using square brackets like this:": "",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "",
 	"Model(s) Whitelisted": "",
 	"Modelfile Content": "",
 	"Modelfile Content": "",
 	"Models": "",
 	"Models": "",
+	"more": "",
 	"More": "",
 	"More": "",
 	"Move to Top": "",
 	"Move to Top": "",
 	"Name": "",
 	"Name": "",
 	"Name your model": "",
 	"Name your model": "",
 	"New Chat": "",
 	"New Chat": "",
+	"New folder": "",
 	"New Password": "",
 	"New Password": "",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "",
 	"No content to speak": "",
+	"No distance available": "",
 	"No file selected": "",
 	"No file selected": "",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -532,9 +539,11 @@
 	"Record voice": "",
 	"Record voice": "",
 	"Redirecting you to OpenWebUI Community": "",
 	"Redirecting you to OpenWebUI Community": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
+	"References from": "",
 	"Refused when it shouldn't have": "",
 	"Refused when it shouldn't have": "",
 	"Regenerate": "",
 	"Regenerate": "",
 	"Release Notes": "",
 	"Release Notes": "",
+	"Relevance": "",
 	"Remove": "",
 	"Remove": "",
 	"Remove Model": "",
 	"Remove Model": "",
 	"Rename": "",
 	"Rename": "",
@@ -600,6 +609,7 @@
 	"Select model": "",
 	"Select model": "",
 	"Select only one model to call": "",
 	"Select only one model to call": "",
 	"Selected model(s) do not support image inputs": "",
 	"Selected model(s) do not support image inputs": "",
+	"Semantic distance to query": "",
 	"Send": "",
 	"Send": "",
 	"Send a Message": "",
 	"Send a Message": "",
 	"Send message": "",
 	"Send message": "",
@@ -680,6 +690,7 @@
 	"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 option will delete all existing files in the collection and replace them with newly uploaded files.": "",
 	"This will delete": "",
 	"This will delete": "",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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": "",
 	"Thorough explanation": "",
 	"Tika": "",
 	"Tika": "",

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

@@ -42,6 +42,7 @@
 	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Admins tienen acceso a todas las herramientas en todo momento; los usuarios necesitan herramientas asignadas por modelo en el espacio de trabajo.",
 	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Admins tienen acceso a todas las herramientas en todo momento; los usuarios necesitan herramientas asignadas por modelo en el espacio de trabajo.",
 	"Advanced Parameters": "Parámetros Avanzados",
 	"Advanced Parameters": "Parámetros Avanzados",
 	"Advanced Params": "Parámetros avanzados",
 	"Advanced Params": "Parámetros avanzados",
+	"All chats": "",
 	"All Documents": "Todos los Documentos",
 	"All Documents": "Todos los Documentos",
 	"All Users": "Todos los Usuarios",
 	"All Users": "Todos los Usuarios",
 	"Allow Chat Deletion": "Permitir Borrar Chats",
 	"Allow Chat Deletion": "Permitir Borrar Chats",
@@ -185,6 +186,7 @@
 	"Delete chat": "Borrar chat",
 	"Delete chat": "Borrar chat",
 	"Delete Chat": "Borrar Chat",
 	"Delete Chat": "Borrar Chat",
 	"Delete chat?": "Borrar el chat?",
 	"Delete chat?": "Borrar el chat?",
+	"Delete folder?": "",
 	"Delete function?": "Borrar la función?",
 	"Delete function?": "Borrar la función?",
 	"Delete prompt?": "Borrar el prompt?",
 	"Delete prompt?": "Borrar el prompt?",
 	"delete this link": "Borrar este enlace",
 	"delete this link": "Borrar este enlace",
@@ -220,9 +222,7 @@
 	"Download": "Descargar",
 	"Download": "Descargar",
 	"Download canceled": "Descarga cancelada",
 	"Download canceled": "Descarga cancelada",
 	"Download Database": "Descarga la Base de Datos",
 	"Download Database": "Descarga la Base de Datos",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "Suelta cualquier archivo aquí para agregarlo a la conversación",
 	"Drop any files here to add to the conversation": "Suelta cualquier archivo aquí para agregarlo a la conversación",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p.ej. '30s','10m'. Unidades válidas de tiempo son 's', 'm', 'h'.",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p.ej. '30s','10m'. Unidades válidas de tiempo son 's', 'm', 'h'.",
 	"Edit": "Editar",
 	"Edit": "Editar",
 	"Edit Memory": "Editar Memoria",
 	"Edit Memory": "Editar Memoria",
@@ -312,6 +312,10 @@
 	"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.",
 	"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",
 	"Fluidly stream large external response chunks": "Transmita con fluidez grandes fragmentos de respuesta externa",
 	"Focus chat input": "Enfoca la entrada del chat",
 	"Focus chat input": "Enfoca la entrada del chat",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "Siguió las instrucciones perfectamente",
 	"Followed instructions perfectly": "Siguió las instrucciones perfectamente",
 	"Form": "De",
 	"Form": "De",
 	"Format your variables using square brackets like this:": "Formatea tus variables usando corchetes de la siguiente manera:",
 	"Format your variables using square brackets like this:": "Formatea tus variables usando corchetes de la siguiente manera:",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "Modelo(s) habilitados",
 	"Model(s) Whitelisted": "Modelo(s) habilitados",
 	"Modelfile Content": "Contenido del Modelfile",
 	"Modelfile Content": "Contenido del Modelfile",
 	"Models": "Modelos",
 	"Models": "Modelos",
+	"more": "",
 	"More": "Más",
 	"More": "Más",
 	"Move to Top": "Mueve al tope",
 	"Move to Top": "Mueve al tope",
 	"Name": "Nombre",
 	"Name": "Nombre",
 	"Name your model": "Asigne un nombre a su modelo",
 	"Name your model": "Asigne un nombre a su modelo",
 	"New Chat": "Nuevo Chat",
 	"New Chat": "Nuevo Chat",
+	"New folder": "",
 	"New Password": "Nueva Contraseña",
 	"New Password": "Nueva Contraseña",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "No hay contenido para hablar",
 	"No content to speak": "No hay contenido para hablar",
+	"No distance available": "",
 	"No file selected": "Ningún archivo fué seleccionado",
 	"No file selected": "Ningún archivo fué seleccionado",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "No se encontró contenido HTML, CSS, o JavaScript.",
 	"No HTML, CSS, or JavaScript content found.": "No se encontró contenido HTML, CSS, o JavaScript.",
@@ -532,9 +539,11 @@
 	"Record voice": "Grabar voz",
 	"Record voice": "Grabar voz",
 	"Redirecting you to OpenWebUI Community": "Redireccionándote a la comunidad OpenWebUI",
 	"Redirecting you to OpenWebUI Community": "Redireccionándote a la comunidad OpenWebUI",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Referirse a usted mismo como \"Usuario\" (por ejemplo, \"El usuario está aprendiendo Español\")",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Referirse a usted mismo como \"Usuario\" (por ejemplo, \"El usuario está aprendiendo Español\")",
+	"References from": "",
 	"Refused when it shouldn't have": "Rechazado cuando no debería",
 	"Refused when it shouldn't have": "Rechazado cuando no debería",
 	"Regenerate": "Regenerar",
 	"Regenerate": "Regenerar",
 	"Release Notes": "Notas de la versión",
 	"Release Notes": "Notas de la versión",
+	"Relevance": "",
 	"Remove": "Eliminar",
 	"Remove": "Eliminar",
 	"Remove Model": "Eliminar modelo",
 	"Remove Model": "Eliminar modelo",
 	"Rename": "Renombrar",
 	"Rename": "Renombrar",
@@ -601,6 +610,7 @@
 	"Select model": "Selecciona un modelo",
 	"Select model": "Selecciona un modelo",
 	"Select only one model to call": "Selecciona sólo un modelo para llamar",
 	"Select only one model to call": "Selecciona sólo un modelo para llamar",
 	"Selected model(s) do not support image inputs": "Los modelos seleccionados no admiten entradas de imagen",
 	"Selected model(s) do not support image inputs": "Los modelos seleccionados no admiten entradas de imagen",
+	"Semantic distance to query": "",
 	"Send": "Enviar",
 	"Send": "Enviar",
 	"Send a Message": "Enviar un Mensaje",
 	"Send a Message": "Enviar un Mensaje",
 	"Send message": "Enviar Mensaje",
 	"Send message": "Enviar Mensaje",
@@ -681,6 +691,7 @@
 	"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 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.": " Esta opción eliminará todos los archivos existentes en la colección y los reemplazará con nuevos archivos subidos.",
 	"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 delete": "Esto eliminará",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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?",
 	"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",
 	"Thorough explanation": "Explicación exhaustiva",
 	"Tika": "Tika",
 	"Tika": "Tika",

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

@@ -42,6 +42,7 @@
 	"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 Parameters": "پارامترهای پیشرفته",
 	"Advanced Params": "پارام های پیشرفته",
 	"Advanced Params": "پارام های پیشرفته",
+	"All chats": "",
 	"All Documents": "تمام سند ها",
 	"All Documents": "تمام سند ها",
 	"All Users": "همه کاربران",
 	"All Users": "همه کاربران",
 	"Allow Chat Deletion": "اجازه حذف گپ",
 	"Allow Chat Deletion": "اجازه حذف گپ",
@@ -185,6 +186,7 @@
 	"Delete chat": "حذف گپ",
 	"Delete chat": "حذف گپ",
 	"Delete Chat": "حذف گپ",
 	"Delete Chat": "حذف گپ",
 	"Delete chat?": "",
 	"Delete chat?": "",
+	"Delete folder?": "",
 	"Delete function?": "",
 	"Delete function?": "",
 	"Delete prompt?": "",
 	"Delete prompt?": "",
 	"delete this link": "حذف این لینک",
 	"delete this link": "حذف این لینک",
@@ -220,9 +222,7 @@
 	"Download": "دانلود کن",
 	"Download": "دانلود کن",
 	"Download canceled": "دانلود لغو شد",
 	"Download canceled": "دانلود لغو شد",
 	"Download Database": "دانلود پایگاه داده",
 	"Download Database": "دانلود پایگاه داده",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "هر فایلی را اینجا رها کنید تا به مکالمه اضافه شود",
 	"Drop any files here to add to the conversation": "هر فایلی را اینجا رها کنید تا به مکالمه اضافه شود",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "به طور مثال '30s','10m'. واحد\u200cهای زمانی معتبر 's', 'm', 'h' هستند.",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "به طور مثال '30s','10m'. واحد\u200cهای زمانی معتبر 's', 'm', 'h' هستند.",
 	"Edit": "ویرایش",
 	"Edit": "ویرایش",
 	"Edit Memory": "",
 	"Edit Memory": "",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "فانگ سرفیس شناسایی شد: نمی توان از نمایه شما به عنوان آواتار استفاده کرد. پیش فرض به عکس پروفایل پیش فرض برگشت داده شد.",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "فانگ سرفیس شناسایی شد: نمی توان از نمایه شما به عنوان آواتار استفاده کرد. پیش فرض به عکس پروفایل پیش فرض برگشت داده شد.",
 	"Fluidly stream large external response chunks": "تکه های پاسخ خارجی بزرگ را به صورت سیال پخش کنید",
 	"Fluidly stream large external response chunks": "تکه های پاسخ خارجی بزرگ را به صورت سیال پخش کنید",
 	"Focus chat input": "فوکوس کردن ورودی گپ",
 	"Focus chat input": "فوکوس کردن ورودی گپ",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "دستورالعمل ها را کاملا دنبال کرد",
 	"Followed instructions perfectly": "دستورالعمل ها را کاملا دنبال کرد",
 	"Form": "",
 	"Form": "",
 	"Format your variables using square brackets like this:": "متغیرهای خود را با استفاده از براکت مربع به شکل زیر قالب بندی کنید:",
 	"Format your variables using square brackets like this:": "متغیرهای خود را با استفاده از براکت مربع به شکل زیر قالب بندی کنید:",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "مدل در لیست سفید ثبت شد",
 	"Model(s) Whitelisted": "مدل در لیست سفید ثبت شد",
 	"Modelfile Content": "محتویات فایل مدل",
 	"Modelfile Content": "محتویات فایل مدل",
 	"Models": "مدل\u200cها",
 	"Models": "مدل\u200cها",
+	"more": "",
 	"More": "بیشتر",
 	"More": "بیشتر",
 	"Move to Top": "",
 	"Move to Top": "",
 	"Name": "نام",
 	"Name": "نام",
 	"Name your model": "نام مدل خود را",
 	"Name your model": "نام مدل خود را",
 	"New Chat": "گپ جدید",
 	"New Chat": "گپ جدید",
+	"New folder": "",
 	"New Password": "رمز عبور جدید",
 	"New Password": "رمز عبور جدید",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "",
 	"No content to speak": "",
+	"No distance available": "",
 	"No file selected": "",
 	"No file selected": "",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -532,9 +539,11 @@
 	"Record voice": "ضبط صدا",
 	"Record voice": "ضبط صدا",
 	"Redirecting you to OpenWebUI Community": "در حال هدایت به OpenWebUI Community",
 	"Redirecting you to OpenWebUI Community": "در حال هدایت به OpenWebUI Community",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
+	"References from": "",
 	"Refused when it shouldn't have": "رد شده زمانی که باید نباشد",
 	"Refused when it shouldn't have": "رد شده زمانی که باید نباشد",
 	"Regenerate": "ری\u200cسازی",
 	"Regenerate": "ری\u200cسازی",
 	"Release Notes": "یادداشت\u200cهای انتشار",
 	"Release Notes": "یادداشت\u200cهای انتشار",
+	"Relevance": "",
 	"Remove": "حذف",
 	"Remove": "حذف",
 	"Remove Model": "حذف مدل",
 	"Remove Model": "حذف مدل",
 	"Rename": "تغییر نام",
 	"Rename": "تغییر نام",
@@ -600,6 +609,7 @@
 	"Select model": "انتخاب یک مدل",
 	"Select model": "انتخاب یک مدل",
 	"Select only one model to call": "",
 	"Select only one model to call": "",
 	"Selected model(s) do not support image inputs": "مدل) های (انتخاب شده ورودیهای تصویر را پشتیبانی نمیکند",
 	"Selected model(s) do not support image inputs": "مدل) های (انتخاب شده ورودیهای تصویر را پشتیبانی نمیکند",
+	"Semantic distance to query": "",
 	"Send": "ارسال",
 	"Send": "ارسال",
 	"Send a Message": "ارسال یک پیام",
 	"Send a Message": "ارسال یک پیام",
 	"Send message": "ارسال پیام",
 	"Send message": "ارسال پیام",
@@ -680,6 +690,7 @@
 	"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 option will delete all existing files in the collection and replace them with newly uploaded files.": "",
 	"This will delete": "",
 	"This will delete": "",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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": "توضیح کامل",
 	"Thorough explanation": "توضیح کامل",
 	"Tika": "",
 	"Tika": "",

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

@@ -42,6 +42,7 @@
 	"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": "Edistyneet parametrit",
 	"Advanced Parameters": "Edistyneet parametrit",
 	"Advanced Params": "Edistyneet parametrit",
 	"Advanced Params": "Edistyneet parametrit",
+	"All chats": "",
 	"All Documents": "Kaikki asiakirjat",
 	"All Documents": "Kaikki asiakirjat",
 	"All Users": "Kaikki käyttäjät",
 	"All Users": "Kaikki käyttäjät",
 	"Allow Chat Deletion": "Salli keskustelujen poisto",
 	"Allow Chat Deletion": "Salli keskustelujen poisto",
@@ -185,6 +186,7 @@
 	"Delete chat": "Poista keskustelu",
 	"Delete chat": "Poista keskustelu",
 	"Delete Chat": "Poista keskustelu",
 	"Delete Chat": "Poista keskustelu",
 	"Delete chat?": "",
 	"Delete chat?": "",
+	"Delete folder?": "",
 	"Delete function?": "",
 	"Delete function?": "",
 	"Delete prompt?": "",
 	"Delete prompt?": "",
 	"delete this link": "poista tämä linkki",
 	"delete this link": "poista tämä linkki",
@@ -220,9 +222,7 @@
 	"Download": "Lataa",
 	"Download": "Lataa",
 	"Download canceled": "Lataus peruutettu",
 	"Download canceled": "Lataus peruutettu",
 	"Download Database": "Lataa tietokanta",
 	"Download Database": "Lataa tietokanta",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "Pudota tiedostoja tähän lisätäksesi ne keskusteluun",
 	"Drop any files here to add to the conversation": "Pudota tiedostoja tähän lisätäksesi ne keskusteluun",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "esim. '30s', '10m'. Kelpoiset aikayksiköt ovat 's', 'm', 'h'.",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "esim. '30s', '10m'. Kelpoiset aikayksiköt ovat 's', 'm', 'h'.",
 	"Edit": "Muokkaa",
 	"Edit": "Muokkaa",
 	"Edit Memory": "",
 	"Edit Memory": "",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Sormenjäljen väärentäminen havaittu: Ei voi käyttää alkukirjaimia avatarina. Käytetään oletusprofiilikuvaa.",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Sormenjäljen väärentäminen havaittu: Ei voi käyttää alkukirjaimia avatarina. Käytetään oletusprofiilikuvaa.",
 	"Fluidly stream large external response chunks": "Virtaa suuria ulkoisia vastausosia joustavasti",
 	"Fluidly stream large external response chunks": "Virtaa suuria ulkoisia vastausosia joustavasti",
 	"Focus chat input": "Fokusoi syöttökenttään",
 	"Focus chat input": "Fokusoi syöttökenttään",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "Noudatti ohjeita täydellisesti",
 	"Followed instructions perfectly": "Noudatti ohjeita täydellisesti",
 	"Form": "",
 	"Form": "",
 	"Format your variables using square brackets like this:": "Muotoile muuttujat hakasulkeilla näin:",
 	"Format your variables using square brackets like this:": "Muotoile muuttujat hakasulkeilla näin:",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "Malli(t) sallittu",
 	"Model(s) Whitelisted": "Malli(t) sallittu",
 	"Modelfile Content": "Mallitiedoston sisältö",
 	"Modelfile Content": "Mallitiedoston sisältö",
 	"Models": "Mallit",
 	"Models": "Mallit",
+	"more": "",
 	"More": "Lisää",
 	"More": "Lisää",
 	"Move to Top": "",
 	"Move to Top": "",
 	"Name": "Nimi",
 	"Name": "Nimi",
 	"Name your model": "Mallin nimeäminen",
 	"Name your model": "Mallin nimeäminen",
 	"New Chat": "Uusi keskustelu",
 	"New Chat": "Uusi keskustelu",
+	"New folder": "",
 	"New Password": "Uusi salasana",
 	"New Password": "Uusi salasana",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "",
 	"No content to speak": "",
+	"No distance available": "",
 	"No file selected": "",
 	"No file selected": "",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -532,9 +539,11 @@
 	"Record voice": "Nauhoita ääni",
 	"Record voice": "Nauhoita ääni",
 	"Redirecting you to OpenWebUI Community": "Ohjataan sinut OpenWebUI-yhteisöön",
 	"Redirecting you to OpenWebUI Community": "Ohjataan sinut OpenWebUI-yhteisöön",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
+	"References from": "",
 	"Refused when it shouldn't have": "Kieltäytyi, vaikka ei olisi pitänyt",
 	"Refused when it shouldn't have": "Kieltäytyi, vaikka ei olisi pitänyt",
 	"Regenerate": "Uudelleenluo",
 	"Regenerate": "Uudelleenluo",
 	"Release Notes": "Julkaisutiedot",
 	"Release Notes": "Julkaisutiedot",
+	"Relevance": "",
 	"Remove": "Poista",
 	"Remove": "Poista",
 	"Remove Model": "Poista malli",
 	"Remove Model": "Poista malli",
 	"Rename": "Nimeä uudelleen",
 	"Rename": "Nimeä uudelleen",
@@ -600,6 +609,7 @@
 	"Select model": "Valitse malli",
 	"Select model": "Valitse malli",
 	"Select only one model to call": "",
 	"Select only one model to call": "",
 	"Selected model(s) do not support image inputs": "Valitut mallit eivät tue kuvasyötteitä",
 	"Selected model(s) do not support image inputs": "Valitut mallit eivät tue kuvasyötteitä",
+	"Semantic distance to query": "",
 	"Send": "Lähetä",
 	"Send": "Lähetä",
 	"Send a Message": "Lähetä viesti",
 	"Send a Message": "Lähetä viesti",
 	"Send message": "Lähetä viesti",
 	"Send message": "Lähetä viesti",
@@ -680,6 +690,7 @@
 	"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 option will delete all existing files in the collection and replace them with newly uploaded files.": "",
 	"This will delete": "",
 	"This will delete": "",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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": "Perusteellinen selitys",
 	"Thorough explanation": "Perusteellinen selitys",
 	"Tika": "",
 	"Tika": "",

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

@@ -42,6 +42,7 @@
 	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Les administrateurs ont accès à tous les outils en tout temps ; les utilisateurs ont besoin d'outils affectés par modèle dans l'espace de travail.",
 	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Les administrateurs ont accès à tous les outils en tout temps ; les utilisateurs ont besoin d'outils affectés par modèle dans l'espace de travail.",
 	"Advanced Parameters": "Paramètres avancés",
 	"Advanced Parameters": "Paramètres avancés",
 	"Advanced Params": "Paramètres avancés",
 	"Advanced Params": "Paramètres avancés",
+	"All chats": "",
 	"All Documents": "Tous les documents",
 	"All Documents": "Tous les documents",
 	"All Users": "Tous les Utilisateurs",
 	"All Users": "Tous les Utilisateurs",
 	"Allow Chat Deletion": "Autoriser la suppression de l'historique de chat",
 	"Allow Chat Deletion": "Autoriser la suppression de l'historique de chat",
@@ -185,6 +186,7 @@
 	"Delete chat": "Supprimer la conversation",
 	"Delete chat": "Supprimer la conversation",
 	"Delete Chat": "Supprimer la Conversation",
 	"Delete Chat": "Supprimer la Conversation",
 	"Delete chat?": "Supprimer la conversation ?",
 	"Delete chat?": "Supprimer la conversation ?",
+	"Delete folder?": "",
 	"Delete function?": "Supprimer la fonction ?",
 	"Delete function?": "Supprimer la fonction ?",
 	"Delete prompt?": "Supprimer la prompt ?",
 	"Delete prompt?": "Supprimer la prompt ?",
 	"delete this link": "supprimer ce lien",
 	"delete this link": "supprimer ce lien",
@@ -220,9 +222,7 @@
 	"Download": "Télécharger",
 	"Download": "Télécharger",
 	"Download canceled": "Téléchargement annulé",
 	"Download canceled": "Téléchargement annulé",
 	"Download Database": "Télécharger la base de données",
 	"Download Database": "Télécharger la base de données",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "Déposez des fichiers ici pour les ajouter à la conversation",
 	"Drop any files here to add to the conversation": "Déposez des fichiers ici pour les ajouter à la conversation",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "par ex. '30s', '10 min'. Les unités de temps valides sont 's', 'm', 'h'.",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "par ex. '30s', '10 min'. Les unités de temps valides sont 's', 'm', 'h'.",
 	"Edit": "Modifier",
 	"Edit": "Modifier",
 	"Edit Memory": "Modifier la mémoire",
 	"Edit Memory": "Modifier la mémoire",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Spoofing détecté : impossible d'utiliser les initiales comme avatar. Retour à l'image de profil par défaut.",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Spoofing détecté : impossible d'utiliser les initiales comme avatar. Retour à l'image de profil par défaut.",
 	"Fluidly stream large external response chunks": "Diffuser de manière fluide de larges portions de réponses externes",
 	"Fluidly stream large external response chunks": "Diffuser de manière fluide de larges portions de réponses externes",
 	"Focus chat input": "Se concentrer sur le chat en entrée",
 	"Focus chat input": "Se concentrer sur le chat en entrée",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "A parfaitement suivi les instructions",
 	"Followed instructions perfectly": "A parfaitement suivi les instructions",
 	"Form": "Formulaire",
 	"Form": "Formulaire",
 	"Format your variables using square brackets like this:": "Formatez vos variables en utilisant des crochets comme suit :",
 	"Format your variables using square brackets like this:": "Formatez vos variables en utilisant des crochets comme suit :",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "Modèle(s) Autorisé(s)",
 	"Model(s) Whitelisted": "Modèle(s) Autorisé(s)",
 	"Modelfile Content": "Contenu du Fichier de Modèle",
 	"Modelfile Content": "Contenu du Fichier de Modèle",
 	"Models": "Modèles",
 	"Models": "Modèles",
+	"more": "",
 	"More": "Plus de",
 	"More": "Plus de",
 	"Move to Top": "",
 	"Move to Top": "",
 	"Name": "Nom",
 	"Name": "Nom",
 	"Name your model": "Nommez votre modèle",
 	"Name your model": "Nommez votre modèle",
 	"New Chat": "Nouvelle conversation",
 	"New Chat": "Nouvelle conversation",
+	"New folder": "",
 	"New Password": "Nouveau mot de passe",
 	"New Password": "Nouveau mot de passe",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "Rien à signaler",
 	"No content to speak": "Rien à signaler",
+	"No distance available": "",
 	"No file selected": "Aucun fichier sélectionné",
 	"No file selected": "Aucun fichier sélectionné",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -532,9 +539,11 @@
 	"Record voice": "Enregistrer la voix",
 	"Record voice": "Enregistrer la voix",
 	"Redirecting you to OpenWebUI Community": "Redirection vers la communauté OpenWebUI",
 	"Redirecting you to OpenWebUI Community": "Redirection vers la communauté OpenWebUI",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Désignez-vous comme « Utilisateur » (par ex. « L'utilisateur apprend l'espagnol »)",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Désignez-vous comme « Utilisateur » (par ex. « L'utilisateur apprend l'espagnol »)",
+	"References from": "",
 	"Refused when it shouldn't have": "Refusé alors qu'il n'aurait pas dû l'être",
 	"Refused when it shouldn't have": "Refusé alors qu'il n'aurait pas dû l'être",
 	"Regenerate": "Regénérer",
 	"Regenerate": "Regénérer",
 	"Release Notes": "Notes de publication",
 	"Release Notes": "Notes de publication",
+	"Relevance": "",
 	"Remove": "Retirer",
 	"Remove": "Retirer",
 	"Remove Model": "Retirer le modèle",
 	"Remove Model": "Retirer le modèle",
 	"Rename": "Renommer",
 	"Rename": "Renommer",
@@ -601,6 +610,7 @@
 	"Select model": "Sélectionnez un modèle",
 	"Select model": "Sélectionnez un modèle",
 	"Select only one model to call": "Sélectionnez seulement un modèle pour appeler",
 	"Select only one model to call": "Sélectionnez seulement un modèle pour appeler",
 	"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",
 	"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",
+	"Semantic distance to query": "",
 	"Send": "Envoyer",
 	"Send": "Envoyer",
 	"Send a Message": "Envoyer un message",
 	"Send a Message": "Envoyer un message",
 	"Send message": "Envoyer un message",
 	"Send message": "Envoyer un message",
@@ -681,6 +691,7 @@
 	"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 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.": "",
 	"This will delete": "Cela supprimera",
 	"This will delete": "Cela supprimera",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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": "Explication approfondie",
 	"Thorough explanation": "Explication approfondie",
 	"Tika": "Tika",
 	"Tika": "Tika",

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

@@ -42,6 +42,7 @@
 	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Les administrateurs ont accès à tous les outils en tout temps ; il faut attribuer des outils aux utilisateurs par modèle dans l'espace de travail.",
 	"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Les administrateurs ont accès à tous les outils en tout temps ; il faut attribuer des outils aux utilisateurs par modèle dans l'espace de travail.",
 	"Advanced Parameters": "Paramètres avancés",
 	"Advanced Parameters": "Paramètres avancés",
 	"Advanced Params": "Paramètres avancés",
 	"Advanced Params": "Paramètres avancés",
+	"All chats": "",
 	"All Documents": "Tous les documents",
 	"All Documents": "Tous les documents",
 	"All Users": "Tous les Utilisateurs",
 	"All Users": "Tous les Utilisateurs",
 	"Allow Chat Deletion": "Autoriser la suppression de l'historique de chat",
 	"Allow Chat Deletion": "Autoriser la suppression de l'historique de chat",
@@ -185,6 +186,7 @@
 	"Delete chat": "Supprimer la conversation",
 	"Delete chat": "Supprimer la conversation",
 	"Delete Chat": "Supprimer la Conversation",
 	"Delete Chat": "Supprimer la Conversation",
 	"Delete chat?": "Supprimer la conversation ?",
 	"Delete chat?": "Supprimer la conversation ?",
+	"Delete folder?": "",
 	"Delete function?": "Supprimer la fonction ?",
 	"Delete function?": "Supprimer la fonction ?",
 	"Delete prompt?": "Supprimer la prompt ?",
 	"Delete prompt?": "Supprimer la prompt ?",
 	"delete this link": "supprimer ce lien",
 	"delete this link": "supprimer ce lien",
@@ -220,9 +222,7 @@
 	"Download": "Télécharger",
 	"Download": "Télécharger",
 	"Download canceled": "Téléchargement annulé",
 	"Download canceled": "Téléchargement annulé",
 	"Download Database": "Télécharger la base de données",
 	"Download Database": "Télécharger la base de données",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "Déposez des fichiers ici pour les ajouter à la conversation",
 	"Drop any files here to add to the conversation": "Déposez des fichiers ici pour les ajouter à la conversation",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "par ex. '30s', '10 min'. Les unités de temps valides sont 's', 'm', 'h'.",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "par ex. '30s', '10 min'. Les unités de temps valides sont 's', 'm', 'h'.",
 	"Edit": "Modifier",
 	"Edit": "Modifier",
 	"Edit Memory": "Modifier la mémoire",
 	"Edit Memory": "Modifier la mémoire",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Spoofing détecté : impossible d'utiliser les initiales comme avatar. Retour à l'image de profil par défaut.",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Spoofing détecté : impossible d'utiliser les initiales comme avatar. Retour à l'image de profil par défaut.",
 	"Fluidly stream large external response chunks": "Streaming fluide de gros morceaux de réponses externes",
 	"Fluidly stream large external response chunks": "Streaming fluide de gros morceaux de réponses externes",
 	"Focus chat input": "Se concentrer sur le chat en entrée",
 	"Focus chat input": "Se concentrer sur le chat en entrée",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "A parfaitement suivi les instructions",
 	"Followed instructions perfectly": "A parfaitement suivi les instructions",
 	"Form": "Formulaire",
 	"Form": "Formulaire",
 	"Format your variables using square brackets like this:": "Formatez vos variables en utilisant des crochets comme suit :",
 	"Format your variables using square brackets like this:": "Formatez vos variables en utilisant des crochets comme suit :",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "Modèle(s) Autorisé(s)",
 	"Model(s) Whitelisted": "Modèle(s) Autorisé(s)",
 	"Modelfile Content": "Contenu du Fichier de Modèle",
 	"Modelfile Content": "Contenu du Fichier de Modèle",
 	"Models": "Modèles",
 	"Models": "Modèles",
+	"more": "",
 	"More": "Plus de",
 	"More": "Plus de",
 	"Move to Top": "Déplacer en haut",
 	"Move to Top": "Déplacer en haut",
 	"Name": "Nom d'utilisateur",
 	"Name": "Nom d'utilisateur",
 	"Name your model": "Nommez votre modèle",
 	"Name your model": "Nommez votre modèle",
 	"New Chat": "Nouvelle conversation",
 	"New Chat": "Nouvelle conversation",
+	"New folder": "",
 	"New Password": "Nouveau mot de passe",
 	"New Password": "Nouveau mot de passe",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "Rien à signaler",
 	"No content to speak": "Rien à signaler",
+	"No distance available": "",
 	"No file selected": "Aucun fichier sélectionné",
 	"No file selected": "Aucun fichier sélectionné",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "Aucun contenu HTML, CSS ou JavaScript trouvé.",
 	"No HTML, CSS, or JavaScript content found.": "Aucun contenu HTML, CSS ou JavaScript trouvé.",
@@ -532,9 +539,11 @@
 	"Record voice": "Enregistrer la voix",
 	"Record voice": "Enregistrer la voix",
 	"Redirecting you to OpenWebUI Community": "Redirection vers la communauté OpenWebUI",
 	"Redirecting you to OpenWebUI Community": "Redirection vers la communauté OpenWebUI",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Désignez-vous comme « Utilisateur » (par ex. « L'utilisateur apprend l'espagnol »)",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Désignez-vous comme « Utilisateur » (par ex. « L'utilisateur apprend l'espagnol »)",
+	"References from": "",
 	"Refused when it shouldn't have": "Refusé alors qu'il n'aurait pas dû l'être",
 	"Refused when it shouldn't have": "Refusé alors qu'il n'aurait pas dû l'être",
 	"Regenerate": "Regénérer",
 	"Regenerate": "Regénérer",
 	"Release Notes": "Notes de publication",
 	"Release Notes": "Notes de publication",
+	"Relevance": "",
 	"Remove": "Retirer",
 	"Remove": "Retirer",
 	"Remove Model": "Retirer le modèle",
 	"Remove Model": "Retirer le modèle",
 	"Rename": "Renommer",
 	"Rename": "Renommer",
@@ -601,6 +610,7 @@
 	"Select model": "Sélectionnez un modèle",
 	"Select model": "Sélectionnez un modèle",
 	"Select only one model to call": "Sélectionnez seulement un modèle pour appeler",
 	"Select only one model to call": "Sélectionnez seulement un modèle pour appeler",
 	"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",
 	"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",
+	"Semantic distance to query": "",
 	"Send": "Envoyer",
 	"Send": "Envoyer",
 	"Send a Message": "Envoyer un message",
 	"Send a Message": "Envoyer un message",
 	"Send message": "Envoyer un message",
 	"Send message": "Envoyer un message",
@@ -681,6 +691,7 @@
 	"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 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.": "Cette option supprimera tous les fichiers existants dans la collection et les remplacera par les fichiers nouvellement téléchargés.",
 	"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 delete": "Cela supprimera",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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 ?",
 	"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",
 	"Thorough explanation": "Explication approfondie",
 	"Tika": "Tika",
 	"Tika": "Tika",

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

@@ -42,6 +42,7 @@
 	"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 Parameters": "פרמטרים מתקדמים",
 	"Advanced Params": "פרמטרים מתקדמים",
 	"Advanced Params": "פרמטרים מתקדמים",
+	"All chats": "",
 	"All Documents": "כל המסמכים",
 	"All Documents": "כל המסמכים",
 	"All Users": "כל המשתמשים",
 	"All Users": "כל המשתמשים",
 	"Allow Chat Deletion": "אפשר מחיקת צ'אט",
 	"Allow Chat Deletion": "אפשר מחיקת צ'אט",
@@ -185,6 +186,7 @@
 	"Delete chat": "מחק צ'אט",
 	"Delete chat": "מחק צ'אט",
 	"Delete Chat": "מחק צ'אט",
 	"Delete Chat": "מחק צ'אט",
 	"Delete chat?": "",
 	"Delete chat?": "",
+	"Delete folder?": "",
 	"Delete function?": "",
 	"Delete function?": "",
 	"Delete prompt?": "",
 	"Delete prompt?": "",
 	"delete this link": "מחק את הקישור הזה",
 	"delete this link": "מחק את הקישור הזה",
@@ -220,9 +222,7 @@
 	"Download": "הורד",
 	"Download": "הורד",
 	"Download canceled": "ההורדה בוטלה",
 	"Download canceled": "ההורדה בוטלה",
 	"Download Database": "הורד מסד נתונים",
 	"Download Database": "הורד מסד נתונים",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "גרור כל קובץ לכאן כדי להוסיף לשיחה",
 	"Drop any files here to add to the conversation": "גרור כל קובץ לכאן כדי להוסיף לשיחה",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "למשל '30s', '10m'. יחידות זמן חוקיות הן 's', 'm', 'h'.",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "למשל '30s', '10m'. יחידות זמן חוקיות הן 's', 'm', 'h'.",
 	"Edit": "ערוך",
 	"Edit": "ערוך",
 	"Edit Memory": "",
 	"Edit Memory": "",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "התגלתה הזיית טביעת אצבע: לא ניתן להשתמש בראשי תיבות כאווטאר. משתמש בתמונת פרופיל ברירת מחדל.",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "התגלתה הזיית טביעת אצבע: לא ניתן להשתמש בראשי תיבות כאווטאר. משתמש בתמונת פרופיל ברירת מחדל.",
 	"Fluidly stream large external response chunks": "שידור נתונים חיצוניים בקצב רציף",
 	"Fluidly stream large external response chunks": "שידור נתונים חיצוניים בקצב רציף",
 	"Focus chat input": "מיקוד הקלט לצ'אט",
 	"Focus chat input": "מיקוד הקלט לצ'אט",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "עקב אחר ההוראות במושלמות",
 	"Followed instructions perfectly": "עקב אחר ההוראות במושלמות",
 	"Form": "",
 	"Form": "",
 	"Format your variables using square brackets like this:": "עצב את המשתנים שלך באמצעות סוגריים מרובעים כך:",
 	"Format your variables using square brackets like this:": "עצב את המשתנים שלך באמצעות סוגריים מרובעים כך:",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "מודלים שנכללו ברשימה הלבנה",
 	"Model(s) Whitelisted": "מודלים שנכללו ברשימה הלבנה",
 	"Modelfile Content": "תוכן קובץ מודל",
 	"Modelfile Content": "תוכן קובץ מודל",
 	"Models": "מודלים",
 	"Models": "מודלים",
+	"more": "",
 	"More": "עוד",
 	"More": "עוד",
 	"Move to Top": "",
 	"Move to Top": "",
 	"Name": "שם",
 	"Name": "שם",
 	"Name your model": "תן שם לדגם שלך",
 	"Name your model": "תן שם לדגם שלך",
 	"New Chat": "צ'אט חדש",
 	"New Chat": "צ'אט חדש",
+	"New folder": "",
 	"New Password": "סיסמה חדשה",
 	"New Password": "סיסמה חדשה",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "",
 	"No content to speak": "",
+	"No distance available": "",
 	"No file selected": "",
 	"No file selected": "",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -532,9 +539,11 @@
 	"Record voice": "הקלט קול",
 	"Record voice": "הקלט קול",
 	"Redirecting you to OpenWebUI Community": "מפנה אותך לקהילת OpenWebUI",
 	"Redirecting you to OpenWebUI Community": "מפנה אותך לקהילת OpenWebUI",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
+	"References from": "",
 	"Refused when it shouldn't have": "נדחה כאשר לא היה צריך",
 	"Refused when it shouldn't have": "נדחה כאשר לא היה צריך",
 	"Regenerate": "הפק מחדש",
 	"Regenerate": "הפק מחדש",
 	"Release Notes": "הערות שחרור",
 	"Release Notes": "הערות שחרור",
+	"Relevance": "",
 	"Remove": "הסר",
 	"Remove": "הסר",
 	"Remove Model": "הסר מודל",
 	"Remove Model": "הסר מודל",
 	"Rename": "שנה שם",
 	"Rename": "שנה שם",
@@ -601,6 +610,7 @@
 	"Select model": "בחר מודל",
 	"Select model": "בחר מודל",
 	"Select only one model to call": "",
 	"Select only one model to call": "",
 	"Selected model(s) do not support image inputs": "דגמים נבחרים אינם תומכים בקלט תמונה",
 	"Selected model(s) do not support image inputs": "דגמים נבחרים אינם תומכים בקלט תמונה",
+	"Semantic distance to query": "",
 	"Send": "שלח",
 	"Send": "שלח",
 	"Send a Message": "שלח הודעה",
 	"Send a Message": "שלח הודעה",
 	"Send message": "שלח הודעה",
 	"Send message": "שלח הודעה",
@@ -681,6 +691,7 @@
 	"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 option will delete all existing files in the collection and replace them with newly uploaded files.": "",
 	"This will delete": "",
 	"This will delete": "",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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": "תיאור מפורט",
 	"Thorough explanation": "תיאור מפורט",
 	"Tika": "",
 	"Tika": "",

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

@@ -42,6 +42,7 @@
 	"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 Parameters": "उन्नत पैरामीटर",
 	"Advanced Params": "उन्नत परम",
 	"Advanced Params": "उन्नत परम",
+	"All chats": "",
 	"All Documents": "सभी डॉक्यूमेंट्स",
 	"All Documents": "सभी डॉक्यूमेंट्स",
 	"All Users": "सभी उपयोगकर्ता",
 	"All Users": "सभी उपयोगकर्ता",
 	"Allow Chat Deletion": "चैट हटाने की अनुमति दें",
 	"Allow Chat Deletion": "चैट हटाने की अनुमति दें",
@@ -185,6 +186,7 @@
 	"Delete chat": "चैट हटाएं",
 	"Delete chat": "चैट हटाएं",
 	"Delete Chat": "चैट हटाएं",
 	"Delete Chat": "चैट हटाएं",
 	"Delete chat?": "",
 	"Delete chat?": "",
+	"Delete folder?": "",
 	"Delete function?": "",
 	"Delete function?": "",
 	"Delete prompt?": "",
 	"Delete prompt?": "",
 	"delete this link": "इस लिंक को हटाएं",
 	"delete this link": "इस लिंक को हटाएं",
@@ -220,9 +222,7 @@
 	"Download": "डाउनलोड",
 	"Download": "डाउनलोड",
 	"Download canceled": "डाउनलोड रद्द किया गया",
 	"Download canceled": "डाउनलोड रद्द किया गया",
 	"Download Database": "डेटाबेस डाउनलोड करें",
 	"Download Database": "डेटाबेस डाउनलोड करें",
-	"Drop a chat export file here to import it.": "",
 	"Drop any files here to add to the conversation": "बातचीत में जोड़ने के लिए कोई भी फ़ाइल यहां छोड़ें",
 	"Drop any files here to add to the conversation": "बातचीत में जोड़ने के लिए कोई भी फ़ाइल यहां छोड़ें",
-	"Drop Chat Export": "",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "जैसे '30s', '10m', मान्य समय इकाइयाँ 's', 'm', 'h' हैं।",
 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "जैसे '30s', '10m', मान्य समय इकाइयाँ 's', 'm', 'h' हैं।",
 	"Edit": "संपादित करें",
 	"Edit": "संपादित करें",
 	"Edit Memory": "",
 	"Edit Memory": "",
@@ -312,6 +312,10 @@
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "फ़िंगरप्रिंट स्पूफ़िंग का पता चला: प्रारंभिक अक्षरों को अवतार के रूप में उपयोग करने में असमर्थ। प्रोफ़ाइल छवि को डिफ़ॉल्ट पर डिफ़ॉल्ट किया जा रहा है.",
 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "फ़िंगरप्रिंट स्पूफ़िंग का पता चला: प्रारंभिक अक्षरों को अवतार के रूप में उपयोग करने में असमर्थ। प्रोफ़ाइल छवि को डिफ़ॉल्ट पर डिफ़ॉल्ट किया जा रहा है.",
 	"Fluidly stream large external response chunks": "बड़े बाह्य प्रतिक्रिया खंडों को तरल रूप से प्रवाहित करें",
 	"Fluidly stream large external response chunks": "बड़े बाह्य प्रतिक्रिया खंडों को तरल रूप से प्रवाहित करें",
 	"Focus chat input": "चैट इनपुट पर फ़ोकस करें",
 	"Focus chat input": "चैट इनपुट पर फ़ोकस करें",
+	"Folder deleted successfully": "",
+	"Folder name cannot be empty": "",
+	"Folder name cannot be empty.": "",
+	"Folder name updated successfully": "",
 	"Followed instructions perfectly": "निर्देशों का पूर्णतः पालन किया",
 	"Followed instructions perfectly": "निर्देशों का पूर्णतः पालन किया",
 	"Form": "",
 	"Form": "",
 	"Format your variables using square brackets like this:": "वर्गाकार कोष्ठकों का उपयोग करके अपने चरों को इस प्रकार प्रारूपित करें :",
 	"Format your variables using square brackets like this:": "वर्गाकार कोष्ठकों का उपयोग करके अपने चरों को इस प्रकार प्रारूपित करें :",
@@ -440,14 +444,17 @@
 	"Model(s) Whitelisted": "मॉडल श्वेतसूची में है",
 	"Model(s) Whitelisted": "मॉडल श्वेतसूची में है",
 	"Modelfile Content": "मॉडल फ़ाइल सामग्री",
 	"Modelfile Content": "मॉडल फ़ाइल सामग्री",
 	"Models": "सभी मॉडल",
 	"Models": "सभी मॉडल",
+	"more": "",
 	"More": "और..",
 	"More": "और..",
 	"Move to Top": "",
 	"Move to Top": "",
 	"Name": "नाम",
 	"Name": "नाम",
 	"Name your model": "अपने मॉडल को नाम दें",
 	"Name your model": "अपने मॉडल को नाम दें",
 	"New Chat": "नई चैट",
 	"New Chat": "नई चैट",
+	"New folder": "",
 	"New Password": "नया पासवर्ड",
 	"New Password": "नया पासवर्ड",
 	"No content found": "",
 	"No content found": "",
 	"No content to speak": "",
 	"No content to speak": "",
+	"No distance available": "",
 	"No file selected": "",
 	"No file selected": "",
 	"No files found.": "",
 	"No files found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
 	"No HTML, CSS, or JavaScript content found.": "",
@@ -532,9 +539,11 @@
 	"Record voice": "आवाज रिकॉर्ड करना",
 	"Record voice": "आवाज रिकॉर्ड करना",
 	"Redirecting you to OpenWebUI Community": "आपको OpenWebUI समुदाय पर पुनर्निर्देशित किया जा रहा है",
 	"Redirecting you to OpenWebUI Community": "आपको OpenWebUI समुदाय पर पुनर्निर्देशित किया जा रहा है",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
+	"References from": "",
 	"Refused when it shouldn't have": "जब ऐसा नहीं होना चाहिए था तो मना कर दिया",
 	"Refused when it shouldn't have": "जब ऐसा नहीं होना चाहिए था तो मना कर दिया",
 	"Regenerate": "पुनः जेनरेट",
 	"Regenerate": "पुनः जेनरेट",
 	"Release Notes": "रिलीज नोट्स",
 	"Release Notes": "रिलीज नोट्स",
+	"Relevance": "",
 	"Remove": "हटा दें",
 	"Remove": "हटा दें",
 	"Remove Model": "मोडेल हटाएँ",
 	"Remove Model": "मोडेल हटाएँ",
 	"Rename": "नाम बदलें",
 	"Rename": "नाम बदलें",
@@ -600,6 +609,7 @@
 	"Select model": "मॉडल चुनें",
 	"Select model": "मॉडल चुनें",
 	"Select only one model to call": "",
 	"Select only one model to call": "",
 	"Selected model(s) do not support image inputs": "चयनित मॉडल छवि इनपुट का समर्थन नहीं करते हैं",
 	"Selected model(s) do not support image inputs": "चयनित मॉडल छवि इनपुट का समर्थन नहीं करते हैं",
+	"Semantic distance to query": "",
 	"Send": "भेज",
 	"Send": "भेज",
 	"Send a Message": "एक संदेश भेजो",
 	"Send a Message": "एक संदेश भेजो",
 	"Send message": "मेसेज भेजें",
 	"Send message": "मेसेज भेजें",
@@ -680,6 +690,7 @@
 	"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 option will delete all existing files in the collection and replace them with newly uploaded files.": "",
 	"This will delete": "",
 	"This will delete": "",
+	"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "",
 	"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": "विस्तृत व्याख्या",
 	"Thorough explanation": "विस्तृत व्याख्या",
 	"Tika": "",
 	"Tika": "",

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