Ver código fonte

Merge branch 'open-webui:dev' into dev

James W. 3 meses atrás
pai
commit
124fdcc48c
53 arquivos alterados com 551 adições e 296 exclusões
  1. 17 11
      backend/open_webui/config.py
  2. 9 8
      backend/open_webui/env.py
  3. 1 0
      backend/open_webui/main.py
  4. 14 9
      backend/open_webui/routers/auths.py
  5. 8 2
      backend/open_webui/routers/chats.py
  6. 31 10
      backend/open_webui/routers/knowledge.py
  7. 5 1
      backend/open_webui/routers/models.py
  8. 3 4
      backend/open_webui/routers/ollama.py
  9. 4 4
      backend/open_webui/routers/openai.py
  10. 5 1
      backend/open_webui/routers/prompts.py
  11. 15 3
      backend/open_webui/routers/tasks.py
  12. 5 1
      backend/open_webui/routers/tools.py
  13. 20 7
      backend/open_webui/utils/middleware.py
  14. 1 0
      backend/open_webui/utils/misc.py
  15. 11 6
      backend/open_webui/utils/oauth.py
  16. 12 10
      backend/open_webui/utils/payload.py
  17. 42 1
      backend/open_webui/utils/response.py
  18. 6 0
      backend/open_webui/utils/task.py
  19. 1 1
      backend/requirements.txt
  20. 1 1
      pyproject.toml
  21. 5 2
      src/lib/apis/chats/index.ts
  22. 1 1
      src/lib/apis/models/index.ts
  23. 2 2
      src/lib/components/admin/Settings/Audio.svelte
  24. 3 1
      src/lib/components/admin/Users/UserList.svelte
  25. 3 1
      src/lib/components/admin/Users/UserList/EditUserModal.svelte
  26. 3 1
      src/lib/components/admin/Users/UserList/UserChatsModal.svelte
  27. 1 1
      src/lib/components/channel/MessageInput.svelte
  28. 5 3
      src/lib/components/channel/Messages/Message.svelte
  29. 22 7
      src/lib/components/chat/Chat.svelte
  30. 1 1
      src/lib/components/chat/MessageInput.svelte
  31. 36 15
      src/lib/components/chat/MessageInput/InputMenu.svelte
  32. 3 1
      src/lib/components/chat/Messages/MultiResponseMessages.svelte
  33. 1 1
      src/lib/components/chat/Messages/ResponseMessage.svelte
  34. 3 1
      src/lib/components/chat/Messages/UserMessage.svelte
  35. 1 1
      src/lib/components/chat/Settings/Audio.svelte
  36. 3 3
      src/lib/components/chat/Settings/Personalization/ManageModal.svelte
  37. 4 1
      src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte
  38. 7 1
      src/lib/components/layout/Sidebar/ChatItem.svelte
  39. 26 1
      src/lib/components/playground/Chat.svelte
  40. 76 0
      src/lib/components/playground/Chat/Message.svelte
  41. 8 65
      src/lib/components/playground/Chat/Messages.svelte
  42. 1 1
      src/lib/components/workspace/Knowledge.svelte
  43. 2 2
      src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte
  44. 9 9
      src/lib/components/workspace/Knowledge/KnowledgeBase.svelte
  45. 1 1
      src/lib/components/workspace/Models.svelte
  46. 4 1
      src/lib/components/workspace/Models/ModelEditor.svelte
  47. 1 1
      src/lib/components/workspace/common/ValvesModal.svelte
  48. 1 1
      src/lib/i18n/locales/ja-JP/translation.json
  49. 83 83
      src/lib/i18n/locales/ko-KR/translation.json
  50. 2 2
      src/lib/i18n/locales/zh-CN/translation.json
  51. 1 1
      src/lib/i18n/locales/zh-TW/translation.json
  52. 18 3
      src/lib/utils/index.ts
  53. 3 1
      src/routes/s/[id]/+page.svelte

+ 17 - 11
backend/open_webui/config.py

@@ -1094,21 +1094,27 @@ TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
     os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""),
     os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""),
 )
 )
 
 
-DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """Create a concise, 3-5 word title with an emoji as a title for the chat history, in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
-
-Examples of titles:
-📉 Stock Market Trends
-🍪 Perfect Chocolate Chip Recipe
-Evolution of Music Streaming
-Remote Work Productivity Tips
-Artificial Intelligence in Healthcare
-🎮 Video Game Development Insights
-
+DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """### Task:
+Generate a concise, 3-5 word title with an emoji summarizing the chat history.
+### Guidelines:
+- The title should clearly represent the main theme or subject of the conversation.
+- Use emojis that enhance understanding of the topic, but avoid quotation marks or special formatting.
+- Write the title in the chat's primary language; default to English if multilingual.
+- Prioritize accuracy over excessive creativity; keep it clear and simple.
+### Output:
+JSON format: { "title": "your concise title here" }
+### Examples:
+- { "title": "📉 Stock Market Trends" },
+- { "title": "🍪 Perfect Chocolate Chip Recipe" },
+- { "title": "Evolution of Music Streaming" },
+- { "title": "Remote Work Productivity Tips" },
+- { "title": "Artificial Intelligence in Healthcare" },
+- { "title": "🎮 Video Game Development Insights" }
+### Chat History:
 <chat_history>
 <chat_history>
 {{MESSAGES:END:2}}
 {{MESSAGES:END:2}}
 </chat_history>"""
 </chat_history>"""
 
 
-
 TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
 TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
     "TAGS_GENERATION_PROMPT_TEMPLATE",
     "TAGS_GENERATION_PROMPT_TEMPLATE",
     "task.tags.prompt_template",
     "task.tags.prompt_template",

+ 9 - 8
backend/open_webui/env.py

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

+ 1 - 0
backend/open_webui/main.py

@@ -875,6 +875,7 @@ async def chat_completion(
             "tool_ids": form_data.get("tool_ids", None),
             "tool_ids": form_data.get("tool_ids", None),
             "files": form_data.get("files", None),
             "files": form_data.get("files", None),
             "features": form_data.get("features", None),
             "features": form_data.get("features", None),
+            "variables": form_data.get("variables", None),
         }
         }
         form_data["metadata"] = metadata
         form_data["metadata"] = metadata
 
 

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

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

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

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

+ 31 - 10
backend/open_webui/routers/knowledge.py

@@ -264,7 +264,11 @@ def add_file_to_knowledge_by_id(
             detail=ERROR_MESSAGES.NOT_FOUND,
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
         )
 
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -342,7 +346,12 @@ def update_file_from_knowledge_by_id(
             detail=ERROR_MESSAGES.NOT_FOUND,
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
         )
 
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+    ):
+
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -406,7 +415,11 @@ def remove_file_from_knowledge_by_id(
             detail=ERROR_MESSAGES.NOT_FOUND,
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
         )
 
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -429,10 +442,6 @@ def remove_file_from_knowledge_by_id(
     if VECTOR_DB_CLIENT.has_collection(collection_name=file_collection):
     if VECTOR_DB_CLIENT.has_collection(collection_name=file_collection):
         VECTOR_DB_CLIENT.delete_collection(collection_name=file_collection)
         VECTOR_DB_CLIENT.delete_collection(collection_name=file_collection)
 
 
-    # Delete physical file
-    if file.path:
-        Storage.delete_file(file.path)
-
     # Delete file from database
     # Delete file from database
     Files.delete_file_by_id(form_data.file_id)
     Files.delete_file_by_id(form_data.file_id)
 
 
@@ -484,7 +493,11 @@ async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)):
             detail=ERROR_MESSAGES.NOT_FOUND,
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
         )
 
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -543,7 +556,11 @@ async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)):
             detail=ERROR_MESSAGES.NOT_FOUND,
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
         )
 
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -582,7 +599,11 @@ def add_files_to_knowledge_batch(
             detail=ERROR_MESSAGES.NOT_FOUND,
             detail=ERROR_MESSAGES.NOT_FOUND,
         )
         )
 
 
-    if knowledge.user_id != user.id and user.role != "admin":
+    if (
+        knowledge.user_id != user.id
+        and not has_access(user.id, "write", knowledge.access_control)
+        and user.role != "admin"
+    ):
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,

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

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

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

@@ -395,7 +395,7 @@ async def get_ollama_tags(
             )
             )
 
 
     if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
     if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
-        models["models"] = get_filtered_models(models, user)
+        models["models"] = await get_filtered_models(models, user)
 
 
     return models
     return models
 
 
@@ -977,6 +977,7 @@ async def generate_chat_completion(
     if BYPASS_MODEL_ACCESS_CONTROL:
     if BYPASS_MODEL_ACCESS_CONTROL:
         bypass_filter = True
         bypass_filter = True
 
 
+    metadata = form_data.pop("metadata", None)
     try:
     try:
         form_data = GenerateChatCompletionForm(**form_data)
         form_data = GenerateChatCompletionForm(**form_data)
     except Exception as e:
     except Exception as e:
@@ -987,8 +988,6 @@ async def generate_chat_completion(
         )
         )
 
 
     payload = {**form_data.model_dump(exclude_none=True)}
     payload = {**form_data.model_dump(exclude_none=True)}
-    if "metadata" in payload:
-        del payload["metadata"]
 
 
     model_id = payload["model"]
     model_id = payload["model"]
     model_info = Models.get_model_by_id(model_id)
     model_info = Models.get_model_by_id(model_id)
@@ -1006,7 +1005,7 @@ async def generate_chat_completion(
             payload["options"] = apply_model_params_to_body_ollama(
             payload["options"] = apply_model_params_to_body_ollama(
                 params, payload["options"]
                 params, payload["options"]
             )
             )
-            payload = apply_model_system_prompt_to_body(params, payload, user)
+            payload = apply_model_system_prompt_to_body(params, payload, metadata)
 
 
         # Check if user has access to the model
         # Check if user has access to the model
         if not bypass_filter and user.role == "user":
         if not bypass_filter and user.role == "user":

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

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

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

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

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

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

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

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

+ 20 - 7
backend/open_webui/utils/middleware.py

@@ -666,6 +666,9 @@ def apply_params_to_form_data(form_data, model):
         if "temperature" in params:
         if "temperature" in params:
             form_data["temperature"] = params["temperature"]
             form_data["temperature"] = params["temperature"]
 
 
+        if "max_tokens" in params:
+            form_data["max_tokens"] = params["max_tokens"]
+
         if "top_p" in params:
         if "top_p" in params:
             form_data["top_p"] = params["top_p"]
             form_data["top_p"] = params["top_p"]
 
 
@@ -746,6 +749,8 @@ async def process_chat_payload(request, form_data, metadata, user, model):
         files.extend(knowledge_files)
         files.extend(knowledge_files)
         form_data["files"] = files
         form_data["files"] = files
 
 
+    variables = form_data.pop("variables", None)
+
     features = form_data.pop("features", None)
     features = form_data.pop("features", None)
     if features:
     if features:
         if "web_search" in features and features["web_search"]:
         if "web_search" in features and features["web_search"]:
@@ -889,16 +894,24 @@ async def process_chat_response(
 
 
                         if res and isinstance(res, dict):
                         if res and isinstance(res, dict):
                             if len(res.get("choices", [])) == 1:
                             if len(res.get("choices", [])) == 1:
-                                title = (
+                                title_string = (
                                     res.get("choices", [])[0]
                                     res.get("choices", [])[0]
                                     .get("message", {})
                                     .get("message", {})
-                                    .get(
-                                        "content",
-                                        message.get("content", "New Chat"),
-                                    )
-                                ).strip()
+                                    .get("content", message.get("content", "New Chat"))
+                                )
                             else:
                             else:
-                                title = None
+                                title_string = ""
+
+                            title_string = title_string[
+                                title_string.find("{") : title_string.rfind("}") + 1
+                            ]
+
+                            try:
+                                title = json.loads(title_string).get(
+                                    "title", "New Chat"
+                                )
+                            except Exception as e:
+                                title = ""
 
 
                             if not title:
                             if not title:
                                 title = messages[0].get("content", "New Chat")
                                 title = messages[0].get("content", "New Chat")

+ 1 - 0
backend/open_webui/utils/misc.py

@@ -149,6 +149,7 @@ def openai_chat_chunk_message_template(
         template["choices"][0]["delta"] = {"content": message}
         template["choices"][0]["delta"] = {"content": message}
     else:
     else:
         template["choices"][0]["finish_reason"] = "stop"
         template["choices"][0]["finish_reason"] = "stop"
+        template["choices"][0]["delta"] = {}
 
 
     if usage:
     if usage:
         template["usage"] = usage
         template["usage"] = usage

+ 11 - 6
backend/open_webui/utils/oauth.py

@@ -35,7 +35,7 @@ from open_webui.config import (
     AppConfig,
     AppConfig,
 )
 )
 from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
 from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
-from open_webui.env import WEBUI_SESSION_COOKIE_SAME_SITE, WEBUI_SESSION_COOKIE_SECURE
+from open_webui.env import WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SECURE
 from open_webui.utils.misc import parse_duration
 from open_webui.utils.misc import parse_duration
 from open_webui.utils.auth import get_password_hash, create_token
 from open_webui.utils.auth import get_password_hash, create_token
 from open_webui.utils.webhook import post_webhook
 from open_webui.utils.webhook import post_webhook
@@ -276,8 +276,13 @@ class OAuthManager:
                         picture_url = ""
                         picture_url = ""
                 if not picture_url:
                 if not picture_url:
                     picture_url = "/user.png"
                     picture_url = "/user.png"
+
                 username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
                 username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
 
 
+                name = user_data.get(username_claim)
+                if not isinstance(user, str):
+                    name = email
+
                 role = self.get_user_role(None, user_data)
                 role = self.get_user_role(None, user_data)
 
 
                 user = Auths.insert_new_auth(
                 user = Auths.insert_new_auth(
@@ -285,7 +290,7 @@ class OAuthManager:
                     password=get_password_hash(
                     password=get_password_hash(
                         str(uuid.uuid4())
                         str(uuid.uuid4())
                     ),  # Random password, not used
                     ),  # Random password, not used
-                    name=user_data.get(username_claim, "User"),
+                    name=name,
                     profile_image_url=picture_url,
                     profile_image_url=picture_url,
                     role=role,
                     role=role,
                     oauth_sub=provider_sub,
                     oauth_sub=provider_sub,
@@ -323,8 +328,8 @@ class OAuthManager:
             key="token",
             key="token",
             value=jwt_token,
             value=jwt_token,
             httponly=True,  # Ensures the cookie is not accessible via JavaScript
             httponly=True,  # Ensures the cookie is not accessible via JavaScript
-            samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
-            secure=WEBUI_SESSION_COOKIE_SECURE,
+            samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
+            secure=WEBUI_AUTH_COOKIE_SECURE,
         )
         )
 
 
         if ENABLE_OAUTH_SIGNUP.value:
         if ENABLE_OAUTH_SIGNUP.value:
@@ -333,8 +338,8 @@ class OAuthManager:
                 key="oauth_id_token",
                 key="oauth_id_token",
                 value=oauth_id_token,
                 value=oauth_id_token,
                 httponly=True,
                 httponly=True,
-                samesite=WEBUI_SESSION_COOKIE_SAME_SITE,
-                secure=WEBUI_SESSION_COOKIE_SECURE,
+                samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
+                secure=WEBUI_AUTH_COOKIE_SECURE,
             )
             )
         # Redirect back to the frontend with the JWT token
         # Redirect back to the frontend with the JWT token
         redirect_url = f"{request.base_url}auth#token={jwt_token}"
         redirect_url = f"{request.base_url}auth#token={jwt_token}"

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

@@ -1,4 +1,4 @@
-from open_webui.utils.task import prompt_template
+from open_webui.utils.task import prompt_variables_template
 from open_webui.utils.misc import (
 from open_webui.utils.misc import (
     add_or_update_system_message,
     add_or_update_system_message,
 )
 )
@@ -7,19 +7,18 @@ from typing import Callable, Optional
 
 
 
 
 # inplace function: form_data is modified
 # inplace function: form_data is modified
-def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict:
+def apply_model_system_prompt_to_body(
+    params: dict, form_data: dict, metadata: Optional[dict] = None
+) -> dict:
     system = params.get("system", None)
     system = params.get("system", None)
     if not system:
     if not system:
         return form_data
         return form_data
 
 
-    if user:
-        template_params = {
-            "user_name": user.name,
-            "user_location": user.info.get("location") if user.info else None,
-        }
-    else:
-        template_params = {}
-    system = prompt_template(system, **template_params)
+    if metadata:
+        print("apply_model_system_prompt_to_body: metadata", metadata)
+        variables = metadata.get("variables", {})
+        system = prompt_variables_template(system, variables)
+
     form_data["messages"] = add_or_update_system_message(
     form_data["messages"] = add_or_update_system_message(
         system, form_data.get("messages", [])
         system, form_data.get("messages", [])
     )
     )
@@ -188,4 +187,7 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
     if ollama_options:
     if ollama_options:
         ollama_payload["options"] = ollama_options
         ollama_payload["options"] = ollama_options
 
 
+    if "metadata" in openai_payload:
+        ollama_payload["metadata"] = openai_payload["metadata"]
+
     return ollama_payload
     return ollama_payload

+ 42 - 1
backend/open_webui/utils/response.py

@@ -9,7 +9,48 @@ def convert_response_ollama_to_openai(ollama_response: dict) -> dict:
     model = ollama_response.get("model", "ollama")
     model = ollama_response.get("model", "ollama")
     message_content = ollama_response.get("message", {}).get("content", "")
     message_content = ollama_response.get("message", {}).get("content", "")
 
 
-    response = openai_chat_completion_message_template(model, message_content)
+    data = ollama_response
+    usage = {
+        "response_token/s": (
+            round(
+                (
+                    (
+                        data.get("eval_count", 0)
+                        / ((data.get("eval_duration", 0) / 10_000_000))
+                    )
+                    * 100
+                ),
+                2,
+            )
+            if data.get("eval_duration", 0) > 0
+            else "N/A"
+        ),
+        "prompt_token/s": (
+            round(
+                (
+                    (
+                        data.get("prompt_eval_count", 0)
+                        / ((data.get("prompt_eval_duration", 0) / 10_000_000))
+                    )
+                    * 100
+                ),
+                2,
+            )
+            if data.get("prompt_eval_duration", 0) > 0
+            else "N/A"
+        ),
+        "total_duration": data.get("total_duration", 0),
+        "load_duration": data.get("load_duration", 0),
+        "prompt_eval_count": data.get("prompt_eval_count", 0),
+        "prompt_eval_duration": data.get("prompt_eval_duration", 0),
+        "eval_count": data.get("eval_count", 0),
+        "eval_duration": data.get("eval_duration", 0),
+        "approximate_total": (lambda s: f"{s // 3600}h{(s % 3600) // 60}m{s % 60}s")(
+            (data.get("total_duration", 0) or 0) // 1_000_000_000
+        ),
+    }
+
+    response = openai_chat_completion_message_template(model, message_content, usage)
     return response
     return response
 
 
 
 

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

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

+ 1 - 1
backend/requirements.txt

@@ -1,4 +1,4 @@
-fastapi==0.111.0
+fastapi==0.115.7
 uvicorn[standard]==0.30.6
 uvicorn[standard]==0.30.6
 pydantic==2.9.2
 pydantic==2.9.2
 python-multipart==0.0.18
 python-multipart==0.0.18

+ 1 - 1
pyproject.toml

@@ -6,7 +6,7 @@ authors = [
 ]
 ]
 license = { file = "LICENSE" }
 license = { file = "LICENSE" }
 dependencies = [
 dependencies = [
-    "fastapi==0.111.0",
+    "fastapi==0.115.7",
     "uvicorn[standard]==0.30.6",
     "uvicorn[standard]==0.30.6",
     "pydantic==2.9.2",
     "pydantic==2.9.2",
     "python-multipart==0.0.18",
     "python-multipart==0.0.18",

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -200,7 +200,7 @@
 				files = files.filter((item) => item?.itemId !== tempItemId);
 				files = files.filter((item) => item?.itemId !== tempItemId);
 			}
 			}
 		} catch (e) {
 		} catch (e) {
-			toast.error(e);
+			toast.error(`${e}`);
 			files = files.filter((item) => item?.itemId !== tempItemId);
 			files = files.filter((item) => item?.itemId !== tempItemId);
 		}
 		}
 	};
 	};

+ 5 - 3
src/lib/components/channel/Messages/Message.svelte

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

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

@@ -45,7 +45,8 @@
 		promptTemplate,
 		promptTemplate,
 		splitStream,
 		splitStream,
 		sleep,
 		sleep,
-		removeDetailsWithReasoning
+		removeDetailsWithReasoning,
+		getPromptVariables
 	} from '$lib/utils';
 	} from '$lib/utils';
 
 
 	import { generateChatCompletion } from '$lib/apis/ollama';
 	import { generateChatCompletion } from '$lib/apis/ollama';
@@ -82,10 +83,12 @@
 	import EventConfirmDialog from '../common/ConfirmDialog.svelte';
 	import EventConfirmDialog from '../common/ConfirmDialog.svelte';
 	import Placeholder from './Placeholder.svelte';
 	import Placeholder from './Placeholder.svelte';
 	import NotificationToast from '../NotificationToast.svelte';
 	import NotificationToast from '../NotificationToast.svelte';
+	import Spinner from '../common/Spinner.svelte';
 
 
 	export let chatIdProp = '';
 	export let chatIdProp = '';
 
 
-	let loaded = false;
+	let loading = false;
+
 	const eventTarget = new EventTarget();
 	const eventTarget = new EventTarget();
 	let controlPane;
 	let controlPane;
 	let controlPaneComponent;
 	let controlPaneComponent;
@@ -133,6 +136,7 @@
 
 
 	$: if (chatIdProp) {
 	$: if (chatIdProp) {
 		(async () => {
 		(async () => {
+			loading = true;
 			console.log(chatIdProp);
 			console.log(chatIdProp);
 
 
 			prompt = '';
 			prompt = '';
@@ -141,11 +145,9 @@
 			webSearchEnabled = false;
 			webSearchEnabled = false;
 			imageGenerationEnabled = false;
 			imageGenerationEnabled = false;
 
 
-			loaded = false;
-
 			if (chatIdProp && (await loadChat())) {
 			if (chatIdProp && (await loadChat())) {
 				await tick();
 				await tick();
-				loaded = true;
+				loading = false;
 
 
 				if (localStorage.getItem(`chat-input-${chatIdProp}`)) {
 				if (localStorage.getItem(`chat-input-${chatIdProp}`)) {
 					try {
 					try {
@@ -627,7 +629,7 @@
 		} catch (e) {
 		} catch (e) {
 			// Remove the failed doc from the files array
 			// Remove the failed doc from the files array
 			files = files.filter((f) => f.name !== url);
 			files = files.filter((f) => f.name !== url);
-			toast.error(e);
+			toast.error(`${e}`);
 		}
 		}
 	};
 	};
 
 
@@ -1557,10 +1559,17 @@
 
 
 				files: (files?.length ?? 0) > 0 ? files : undefined,
 				files: (files?.length ?? 0) > 0 ? files : undefined,
 				tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
 				tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
+
 				features: {
 				features: {
 					image_generation: imageGenerationEnabled,
 					image_generation: imageGenerationEnabled,
 					web_search: webSearchEnabled
 					web_search: webSearchEnabled
 				},
 				},
+				variables: {
+					...getPromptVariables(
+						$user.name,
+						$settings?.userLocation ? await getAndUpdateUserLocation(localStorage.token) : undefined
+					)
+				},
 
 
 				session_id: $socket?.id,
 				session_id: $socket?.id,
 				chat_id: $chatId,
 				chat_id: $chatId,
@@ -1861,7 +1870,7 @@
 		: ' '} w-full max-w-full flex flex-col"
 		: ' '} w-full max-w-full flex flex-col"
 	id="chat-container"
 	id="chat-container"
 >
 >
-	{#if !chatIdProp || (loaded && chatIdProp)}
+	{#if chatIdProp === '' || (!loading && chatIdProp)}
 		{#if $settings?.backgroundImageUrl ?? null}
 		{#if $settings?.backgroundImageUrl ?? null}
 			<div
 			<div
 				class="absolute {$showSidebar
 				class="absolute {$showSidebar
@@ -2065,5 +2074,11 @@
 				{eventTarget}
 				{eventTarget}
 			/>
 			/>
 		</PaneGroup>
 		</PaneGroup>
+	{:else if loading}
+		<div class=" flex items-center justify-center h-full w-full">
+			<div class="m-auto">
+				<Spinner />
+			</div>
+		</div>
 	{/if}
 	{/if}
 </div>
 </div>

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

@@ -211,7 +211,7 @@
 				files = files.filter((item) => item?.itemId !== tempItemId);
 				files = files.filter((item) => item?.itemId !== tempItemId);
 			}
 			}
 		} catch (e) {
 		} catch (e) {
-			toast.error(e);
+			toast.error(`${e}`);
 			files = files.filter((item) => item?.itemId !== tempItemId);
 			files = files.filter((item) => item?.itemId !== tempItemId);
 		}
 		}
 	};
 	};

+ 36 - 15
src/lib/components/chat/MessageInput/InputMenu.svelte

@@ -48,6 +48,9 @@
 		init();
 		init();
 	}
 	}
 
 
+	let fileUploadEnabled = true;
+	$: fileUploadEnabled = $user.role === 'admin' || $user?.permissions?.chat?.file_upload;
+
 	const init = async () => {
 	const init = async () => {
 		if ($_tools === null) {
 		if ($_tools === null) {
 			await _tools.set(await getTools(localStorage.token));
 			await _tools.set(await getTools(localStorage.token));
@@ -166,26 +169,44 @@
 			{/if}
 			{/if}
 
 
 			{#if !$mobile}
 			{#if !$mobile}
+				<Tooltip
+					content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
+					className="w-full"
+				>
+					<DropdownMenu.Item
+						class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-xl {!fileUploadEnabled
+							? 'opacity-50'
+							: ''}"
+						on:click={() => {
+							if (fileUploadEnabled) {
+								screenCaptureHandler();
+							}
+						}}
+					>
+						<CameraSolid />
+						<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
+					</DropdownMenu.Item>
+				</Tooltip>
+			{/if}
+
+			<Tooltip
+				content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
+				className="w-full"
+			>
 				<DropdownMenu.Item
 				<DropdownMenu.Item
-					class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-xl"
+					class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
+						? 'opacity-50'
+						: ''}"
 					on:click={() => {
 					on:click={() => {
-						screenCaptureHandler();
+						if (fileUploadEnabled) {
+							uploadFilesHandler();
+						}
 					}}
 					}}
 				>
 				>
-					<CameraSolid />
-					<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
+					<DocumentArrowUpSolid />
+					<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
 				</DropdownMenu.Item>
 				</DropdownMenu.Item>
-			{/if}
-
-			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
-				on:click={() => {
-					uploadFilesHandler();
-				}}
-			>
-				<DocumentArrowUpSolid />
-				<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
-			</DropdownMenu.Item>
+			</Tooltip>
 
 
 			{#if $config?.features?.enable_google_drive_integration}
 			{#if $config?.features?.enable_google_drive_integration}
 				<DropdownMenu.Item
 				<DropdownMenu.Item

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

@@ -16,7 +16,9 @@
 	import Markdown from './Markdown.svelte';
 	import Markdown from './Markdown.svelte';
 	import Name from './Name.svelte';
 	import Name from './Name.svelte';
 	import Skeleton from './Skeleton.svelte';
 	import Skeleton from './Skeleton.svelte';
+    import localizedFormat from 'dayjs/plugin/localizedFormat';
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
+    dayjs.extend(localizedFormat);
 
 
 	export let chatId;
 	export let chatId;
 	export let history;
 	export let history;
@@ -264,7 +266,7 @@
 										<span
 										<span
 											class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
 											class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
 										>
 										>
-											{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
+											{dayjs(message.timestamp * 1000).format('LT')}
 										</span>
 										</span>
 									{/if}
 									{/if}
 								</Name>
 								</Name>

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

@@ -500,7 +500,7 @@
 					<div
 					<div
 						class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
 						class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
 					>
 					>
-						<Tooltip content={dayjs(message.timestamp * 1000).format('dddd, DD MMMM YYYY HH:mm')}>
+						<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
 							<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
 							<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
 						</Tooltip>
 						</Tooltip>
 					</div>
 					</div>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -766,7 +766,7 @@
 	"Reset": "",
 	"Reset": "",
 	"Reset All Models": "",
 	"Reset All Models": "",
 	"Reset Upload Directory": "アップロードディレクトリをリセット",
 	"Reset Upload Directory": "アップロードディレクトリをリセット",
-	"Reset Vector Storage/Knowledge": "ベクターストレージとナレッジべーうをリセット",
+	"Reset Vector Storage/Knowledge": "ベクターストレージとナレッジベースをリセット",
 	"Reset view": "",
 	"Reset view": "",
 	"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
 	"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
 	"Response splitting": "応答の分割",
 	"Response splitting": "応答の分割",

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

@@ -8,39 +8,39 @@
 	"{{COUNT}} Replies": "",
 	"{{COUNT}} Replies": "",
 	"{{user}}'s Chats": "{{user}}의 채팅",
 	"{{user}}'s Chats": "{{user}}의 채팅",
 	"{{webUIName}} Backend Required": "{{webUIName}} 백엔드가 필요합니다.",
 	"{{webUIName}} Backend Required": "{{webUIName}} 백엔드가 필요합니다.",
-	"*Prompt node ID(s) are required for image generation": "사진 생성을 위해 프롬트 노드 ID가 필요합니다",
+	"*Prompt node ID(s) are required for image generation": "사진 생성을 위해 프롬트 노드 ID가 필요합니다",
 	"A new version (v{{LATEST_VERSION}}) is now available.": "최신 버전 (v{{LATEST_VERSION}})이 가능합니다",
 	"A new version (v{{LATEST_VERSION}}) is now available.": "최신 버전 (v{{LATEST_VERSION}})이 가능합니다",
 	"A task model is used when performing tasks such as generating titles for chats and web search queries": "작업 모델은 채팅 및 웹 검색 쿼리에 대한 제목 생성 등의 작업 수행 시 사용됩니다.",
 	"A task model is used when performing tasks such as generating titles for chats and web search queries": "작업 모델은 채팅 및 웹 검색 쿼리에 대한 제목 생성 등의 작업 수행 시 사용됩니다.",
 	"a user": "사용자",
 	"a user": "사용자",
 	"About": "정보",
 	"About": "정보",
-	"Access": "",
-	"Access Control": "",
+	"Access": "접근",
+	"Access Control": "접근 제어",
 	"Accessible to all users": "모든 사용자가 접근 가능",
 	"Accessible to all users": "모든 사용자가 접근 가능",
 	"Account": "계정",
 	"Account": "계정",
 	"Account Activation Pending": "계정 활성화 대기",
 	"Account Activation Pending": "계정 활성화 대기",
 	"Accurate information": "정확한 정보",
 	"Accurate information": "정확한 정보",
 	"Actions": "행동",
 	"Actions": "행동",
-	"Activate this command by typing \"/{{COMMAND}}\" to chat input.": "",
+	"Activate this command by typing \"/{{COMMAND}}\" to chat input.": "채팅에서 \"{{COMMAND}}\"을 입력하여 이 명령을 활성화할 수 있습니다.",
 	"Active Users": "활성 사용자",
 	"Active Users": "활성 사용자",
 	"Add": "추가",
 	"Add": "추가",
-	"Add a model ID": "",
+	"Add a model ID": "모델 ID 추가",
 	"Add a short description about what this model does": "모델의 기능에 대한 간단한 설명 추가",
 	"Add a short description about what this model does": "모델의 기능에 대한 간단한 설명 추가",
 	"Add a tag": "태그 추가",
 	"Add a tag": "태그 추가",
 	"Add Arena Model": "아레나 모델 추가",
 	"Add Arena Model": "아레나 모델 추가",
-	"Add Connection": "",
+	"Add Connection": "연결 추가",
 	"Add Content": "내용 추가",
 	"Add Content": "내용 추가",
 	"Add content here": "여기에 내용을 추가하세요",
 	"Add content here": "여기에 내용을 추가하세요",
 	"Add custom prompt": "사용자 정의 프롬프트 추가",
 	"Add custom prompt": "사용자 정의 프롬프트 추가",
 	"Add Files": "파일 추가",
 	"Add Files": "파일 추가",
-	"Add Group": "",
+	"Add Group": "그룹 추가",
 	"Add Memory": "메모리 추가",
 	"Add Memory": "메모리 추가",
 	"Add Model": "모델 추가",
 	"Add Model": "모델 추가",
-	"Add Reaction": "",
+	"Add Reaction": "리액션 추가",
 	"Add Tag": "태그 추가",
 	"Add Tag": "태그 추가",
 	"Add Tags": "태그 추가",
 	"Add Tags": "태그 추가",
 	"Add text content": "글 추가",
 	"Add text content": "글 추가",
 	"Add User": "사용자 추가",
 	"Add User": "사용자 추가",
-	"Add User Group": "",
+	"Add User Group": "사용자 그룹 추가",
 	"Adjusting these settings will apply changes universally to all users.": "위와 같이 설정시 모든 사용자에게 적용됩니다.",
 	"Adjusting these settings will apply changes universally to all users.": "위와 같이 설정시 모든 사용자에게 적용됩니다.",
 	"admin": "관리자",
 	"admin": "관리자",
 	"Admin": "관리자",
 	"Admin": "관리자",
@@ -50,12 +50,12 @@
 	"Advanced Parameters": "고급 매개변수",
 	"Advanced Parameters": "고급 매개변수",
 	"Advanced Params": "고급 매개변수",
 	"Advanced Params": "고급 매개변수",
 	"All Documents": "모든 문서",
 	"All Documents": "모든 문서",
-	"All models deleted successfully": "",
-	"Allow Chat Controls": "",
-	"Allow Chat Delete": "",
+	"All models deleted successfully": "성공적으로 모든 모델이 삭제되었습니다",
+	"Allow Chat Controls": "채팅 제어 허용",
+	"Allow Chat Delete": "채팅 삭제 허용",
 	"Allow Chat Deletion": "채팅 삭제 허용",
 	"Allow Chat Deletion": "채팅 삭제 허용",
-	"Allow Chat Edit": "",
-	"Allow File Upload": "",
+	"Allow Chat Edit": "채팅 수정 허용",
+	"Allow File Upload": "파일 업로드 허용",
 	"Allow non-local voices": "외부 음성 허용",
 	"Allow non-local voices": "외부 음성 허용",
 	"Allow Temporary Chat": "임시 채팅 허용",
 	"Allow Temporary Chat": "임시 채팅 허용",
 	"Allow User Location": "사용자 위치 활용 허용",
 	"Allow User Location": "사용자 위치 활용 허용",
@@ -75,15 +75,15 @@
 	"API keys": "API 키",
 	"API keys": "API 키",
 	"Application DN": "",
 	"Application DN": "",
 	"Application DN Password": "",
 	"Application DN Password": "",
-	"applies to all users with the \"user\" role": "",
+	"applies to all users with the \"user\" role": "\"사용자\" 권한의 모든 사용자에게 적용됩니다",
 	"April": "4월",
 	"April": "4월",
 	"Archive": "보관",
 	"Archive": "보관",
 	"Archive All Chats": "모든 채팅 보관",
 	"Archive All Chats": "모든 채팅 보관",
 	"Archived Chats": "보관된 채팅",
 	"Archived Chats": "보관된 채팅",
 	"archived-chat-export": "",
 	"archived-chat-export": "",
-	"Are you sure you want to delete this channel?": "",
-	"Are you sure you want to delete this message?": "",
-	"Are you sure you want to unarchive all archived chats?": "",
+	"Are you sure you want to delete this channel?": "정말 이 채널을 삭제하시겠습니까?",
+	"Are you sure you want to delete this message?": "정말 이 메세지를 삭제하시겠습니까?",
+	"Are you sure you want to unarchive all archived chats?": "정말 보관된 모든 채팅을 보관 해제하시겠습니까?",
 	"Are you sure?": "확실합니까?",
 	"Are you sure?": "확실합니까?",
 	"Arena Models": "아레나 모델",
 	"Arena Models": "아레나 모델",
 	"Artifacts": "아티팩트",
 	"Artifacts": "아티팩트",
@@ -141,7 +141,7 @@
 	"Chat Controls": "채팅 제어",
 	"Chat Controls": "채팅 제어",
 	"Chat direction": "채팅 방향",
 	"Chat direction": "채팅 방향",
 	"Chat Overview": "채팅",
 	"Chat Overview": "채팅",
-	"Chat Permissions": "",
+	"Chat Permissions": "채팅 권한",
 	"Chat Tags Auto-Generation": "채팅 태그 자동생성",
 	"Chat Tags Auto-Generation": "채팅 태그 자동생성",
 	"Chats": "채팅",
 	"Chats": "채팅",
 	"Check Again": "다시 확인",
 	"Check Again": "다시 확인",
@@ -210,16 +210,16 @@
 	"Copy Link": "링크 복사",
 	"Copy Link": "링크 복사",
 	"Copy to clipboard": "클립보드에 복사",
 	"Copy to clipboard": "클립보드에 복사",
 	"Copying to clipboard was successful!": "성공적으로 클립보드에 복사되었습니다!",
 	"Copying to clipboard was successful!": "성공적으로 클립보드에 복사되었습니다!",
-	"Create": "",
-	"Create a knowledge base": "",
-	"Create a model": "모델 만들기",
-	"Create Account": "계정 만들기",
-	"Create Admin Account": "",
-	"Create Channel": "",
-	"Create Group": "",
-	"Create Knowledge": "지식 만들기",
-	"Create new key": "새 키 만들기",
-	"Create new secret key": "새 비밀 키 만들기",
+	"Create": "생성",
+	"Create a knowledge base": "지식 기반 생성",
+	"Create a model": "모델 생성",
+	"Create Account": "계정 생성",
+	"Create Admin Account": "관리자 계정 생성",
+	"Create Channel": "채널 생성",
+	"Create Group": "그룹 생성",
+	"Create Knowledge": "지식 생성",
+	"Create new key": "새로운 키 생성",
+	"Create new secret key": "새로운 비밀 키 생성",
 	"Created at": "생성일",
 	"Created at": "생성일",
 	"Created At": "생성일",
 	"Created At": "생성일",
 	"Created by": "생성자",
 	"Created by": "생성자",
@@ -236,8 +236,8 @@
 	"Default Model": "기본 모델",
 	"Default Model": "기본 모델",
 	"Default model updated": "기본 모델이 업데이트되었습니다.",
 	"Default model updated": "기본 모델이 업데이트되었습니다.",
 	"Default Models": "기본 모델",
 	"Default Models": "기본 모델",
-	"Default permissions": "",
-	"Default permissions updated successfully": "",
+	"Default permissions": "기본 권한",
+	"Default permissions updated successfully": "성공적으로 기본 권한이 수정되었습니다",
 	"Default Prompt Suggestions": "기본 프롬프트 제안",
 	"Default Prompt Suggestions": "기본 프롬프트 제안",
 	"Default to 389 or 636 if TLS is enabled": "",
 	"Default to 389 or 636 if TLS is enabled": "",
 	"Default to ALL": "",
 	"Default to ALL": "",
@@ -245,21 +245,21 @@
 	"Delete": "삭제",
 	"Delete": "삭제",
 	"Delete a model": "모델 삭제",
 	"Delete a model": "모델 삭제",
 	"Delete All Chats": "모든 채팅 삭제",
 	"Delete All Chats": "모든 채팅 삭제",
-	"Delete All Models": "",
+	"Delete All Models": "모든 모델 삭제",
 	"Delete chat": "채팅 삭제",
 	"Delete chat": "채팅 삭제",
 	"Delete Chat": "채팅 삭제",
 	"Delete Chat": "채팅 삭제",
 	"Delete chat?": "채팅을 삭제하겠습니까?",
 	"Delete chat?": "채팅을 삭제하겠습니까?",
 	"Delete folder?": "폴더를 삭제하시겠습니까?",
 	"Delete folder?": "폴더를 삭제하시겠습니까?",
 	"Delete function?": "함수를 삭제하시겠습니까?",
 	"Delete function?": "함수를 삭제하시겠습니까?",
 	"Delete Message": "",
 	"Delete Message": "",
-	"Delete prompt?": "프롬트를 삭제하시겠습니까?",
+	"Delete prompt?": "프롬트를 삭제하시겠습니까?",
 	"delete this link": "이 링크를 삭제합니다.",
 	"delete this link": "이 링크를 삭제합니다.",
 	"Delete tool?": "도구를 삭제하시겠습니까?",
 	"Delete tool?": "도구를 삭제하시겠습니까?",
 	"Delete User": "사용자 삭제",
 	"Delete User": "사용자 삭제",
 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} 삭제됨",
 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} 삭제됨",
 	"Deleted {{name}}": "{{name}}을(를) 삭제했습니다.",
 	"Deleted {{name}}": "{{name}}을(를) 삭제했습니다.",
-	"Deleted User": "",
-	"Describe your knowledge base and objectives": "",
+	"Deleted User": "삭제된 사용자",
+	"Describe your knowledge base and objectives": "지식 기반에 대한 설명과 목적을 입력하세요",
 	"Description": "설명",
 	"Description": "설명",
 	"Didn't fully follow instructions": "완전히 지침을 따르지 않음",
 	"Didn't fully follow instructions": "완전히 지침을 따르지 않음",
 	"Disabled": "제한됨",
 	"Disabled": "제한됨",
@@ -370,7 +370,7 @@
 	"Enter server label": "",
 	"Enter server label": "",
 	"Enter server port": "",
 	"Enter server port": "",
 	"Enter stop sequence": "중지 시퀀스 입력",
 	"Enter stop sequence": "중지 시퀀스 입력",
-	"Enter system prompt": "시스템 프롬트 입력",
+	"Enter system prompt": "시스템 프롬트 입력",
 	"Enter Tavily API Key": "Tavily API 키 입력",
 	"Enter Tavily API Key": "Tavily API 키 입력",
 	"Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.": "WebUI의 공개 URL을 입력해 주세요. 이 URL은 알림에서 링크를 생성하는 데 사용합니다.",
 	"Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.": "WebUI의 공개 URL을 입력해 주세요. 이 URL은 알림에서 링크를 생성하는 데 사용합니다.",
 	"Enter Tika Server URL": "Tika 서버 URL 입력",
 	"Enter Tika Server URL": "Tika 서버 URL 입력",
@@ -419,10 +419,10 @@
 	"Failed to save models configuration": "",
 	"Failed to save models configuration": "",
 	"Failed to update settings": "설정 업데이트에 실패하였습니다.",
 	"Failed to update settings": "설정 업데이트에 실패하였습니다.",
 	"Failed to upload file.": "파일 업로드에 실패했습니다",
 	"Failed to upload file.": "파일 업로드에 실패했습니다",
-	"Features Permissions": "",
+	"Features Permissions": "기능 권한",
 	"February": "2월",
 	"February": "2월",
 	"Feedback History": "피드백 기록",
 	"Feedback History": "피드백 기록",
-	"Feedbacks": "",
+	"Feedbacks": "피드백",
 	"Feel free to add specific details": "자세한 내용을 자유롭게 추가하세요.",
 	"Feel free to add specific details": "자세한 내용을 자유롭게 추가하세요.",
 	"File": "파일",
 	"File": "파일",
 	"File added successfully.": "성공적으로 파일이 추가되었습니다",
 	"File added successfully.": "성공적으로 파일이 추가되었습니다",
@@ -472,12 +472,12 @@
 	"Google Drive": "",
 	"Google Drive": "",
 	"Google PSE API Key": "Google PSE API 키",
 	"Google PSE API Key": "Google PSE API 키",
 	"Google PSE Engine Id": "Google PSE 엔진 ID",
 	"Google PSE Engine Id": "Google PSE 엔진 ID",
-	"Group created successfully": "",
-	"Group deleted successfully": "",
-	"Group Description": "",
-	"Group Name": "",
-	"Group updated successfully": "",
-	"Groups": "",
+	"Group created successfully": "성공적으로 그룹을 생성했습니다",
+	"Group deleted successfully": "성공적으로 그룹을 삭제했습니다",
+	"Group Description": "그룹 설명",
+	"Group Name": "그룹 명",
+	"Group updated successfully": "성공적으로 그룹을 수정했습니다",
+	"Groups": "그룹",
 	"h:mm a": "h:mm a",
 	"h:mm a": "h:mm a",
 	"Haptic Feedback": "햅틱 피드백",
 	"Haptic Feedback": "햅틱 피드백",
 	"has no conversations.": "대화가 없습니다.",
 	"has no conversations.": "대화가 없습니다.",
@@ -494,13 +494,13 @@
 	"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "",
 	"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "",
 	"ID": "ID",
 	"ID": "ID",
 	"Ignite curiosity": "",
 	"Ignite curiosity": "",
-	"Image": "",
+	"Image": "이미지",
 	"Image Compression": "이미지 압축",
 	"Image Compression": "이미지 압축",
-	"Image generation": "",
-	"Image Generation": "",
+	"Image generation": "이미지 생성",
+	"Image Generation": "이미지 생성",
 	"Image Generation (Experimental)": "이미지 생성(실험적)",
 	"Image Generation (Experimental)": "이미지 생성(실험적)",
 	"Image Generation Engine": "이미지 생성 엔진",
 	"Image Generation Engine": "이미지 생성 엔진",
-	"Image Max Compression Size": "",
+	"Image Max Compression Size": "이미지 최대 압축 크기",
 	"Image Prompt Generation": "",
 	"Image Prompt Generation": "",
 	"Image Prompt Generation Prompt": "",
 	"Image Prompt Generation Prompt": "",
 	"Image Settings": "이미지 설정",
 	"Image Settings": "이미지 설정",
@@ -538,7 +538,7 @@
 	"Key": "",
 	"Key": "",
 	"Keyboard shortcuts": "키보드 단축키",
 	"Keyboard shortcuts": "키보드 단축키",
 	"Knowledge": "지식 기반",
 	"Knowledge": "지식 기반",
-	"Knowledge Access": "",
+	"Knowledge Access": "지식 접근",
 	"Knowledge created successfully.": "성공적으로 지식 기반이 생성되었습니다",
 	"Knowledge created successfully.": "성공적으로 지식 기반이 생성되었습니다",
 	"Knowledge deleted successfully.": "성공적으로 지식 기반이 삭제되었습니다",
 	"Knowledge deleted successfully.": "성공적으로 지식 기반이 삭제되었습니다",
 	"Knowledge reset successfully.": "성공적으로 지식 기반이 초기화되었습니다",
 	"Knowledge reset successfully.": "성공적으로 지식 기반이 초기화되었습니다",
@@ -556,7 +556,7 @@
 	"Leave empty to include all models from \"{{URL}}/api/tags\" endpoint": "",
 	"Leave empty to include all models from \"{{URL}}/api/tags\" endpoint": "",
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "특정 모델을 선택하거나 모든 모델을 포함하고 싶으면 빈칸으로 남겨두세요",
 	"Leave empty to include all models or select specific models": "특정 모델을 선택하거나 모든 모델을 포함하고 싶으면 빈칸으로 남겨두세요",
-	"Leave empty to use the default prompt, or enter a custom prompt": "기본 프롬포트를 사용하기 위해 빈칸으로 남겨두거나, 커스텀 프롬포트를 입력하세요",
+	"Leave empty to use the default prompt, or enter a custom prompt": "기본 프롬프트를 사용하기 위해 빈칸으로 남겨두거나, 커스텀 프롬프트를 입력하세요",
 	"Light": "라이트",
 	"Light": "라이트",
 	"Listening...": "듣는 중...",
 	"Listening...": "듣는 중...",
 	"Llama.cpp": "",
 	"Llama.cpp": "",
@@ -617,13 +617,13 @@
 	"Model updated successfully": "성공적으로 모델이 업데이트되었습니다",
 	"Model updated successfully": "성공적으로 모델이 업데이트되었습니다",
 	"Modelfile Content": "Modelfile 내용",
 	"Modelfile Content": "Modelfile 내용",
 	"Models": "모델",
 	"Models": "모델",
-	"Models Access": "",
+	"Models Access": "모델 접근",
 	"Models configuration saved successfully": "",
 	"Models configuration saved successfully": "",
 	"Mojeek Search API Key": "Mojeek Search API 키",
 	"Mojeek Search API Key": "Mojeek Search API 키",
 	"more": "더보기",
 	"more": "더보기",
 	"More": "더보기",
 	"More": "더보기",
 	"Name": "이름",
 	"Name": "이름",
-	"Name your knowledge base": "",
+	"Name your knowledge base": "지식 기반 이름을 지정하세요",
 	"New Chat": "새 채팅",
 	"New Chat": "새 채팅",
 	"New Folder": "",
 	"New Folder": "",
 	"New Password": "새 비밀번호",
 	"New Password": "새 비밀번호",
@@ -667,8 +667,8 @@
 	"Ollama API settings updated": "",
 	"Ollama API settings updated": "",
 	"Ollama Version": "Ollama 버전",
 	"Ollama Version": "Ollama 버전",
 	"On": "켜기",
 	"On": "켜기",
-	"Only alphanumeric characters and hyphens are allowed": "",
-	"Only alphanumeric characters and hyphens are allowed in the command string.": "명령어 문자열에는 영문자, 숫자 및 하이픈만 허용됩니다.",
+	"Only alphanumeric characters and hyphens are allowed": "영문자, 숫자 및 하이픈(-)만 허용됨",
+	"Only alphanumeric characters and hyphens are allowed in the command string.": "명령어 문자열에는 영문자, 숫자 및 하이픈(-)만 허용됩니다.",
 	"Only collections can be edited, create a new knowledge base to edit/add documents.": "가지고 있는 컬렉션만 수정 가능합니다, 새 지식 기반을 생성하여 문서를 수정 혹은 추가하십시오",
 	"Only collections can be edited, create a new knowledge base to edit/add documents.": "가지고 있는 컬렉션만 수정 가능합니다, 새 지식 기반을 생성하여 문서를 수정 혹은 추가하십시오",
 	"Only select users and groups with permission can access": "권한이 있는 사용자와 그룹만 접근 가능합니다",
 	"Only select users and groups with permission can access": "권한이 있는 사용자와 그룹만 접근 가능합니다",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "이런! URL이 잘못된 것 같습니다. 다시 한번 확인하고 다시 시도해주세요.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "이런! URL이 잘못된 것 같습니다. 다시 한번 확인하고 다시 시도해주세요.",
@@ -688,7 +688,7 @@
 	"OpenAI API settings updated": "",
 	"OpenAI API settings updated": "",
 	"OpenAI URL/Key required.": "OpenAI URL/키가 필요합니다.",
 	"OpenAI URL/Key required.": "OpenAI URL/키가 필요합니다.",
 	"or": "또는",
 	"or": "또는",
-	"Organize your users": "",
+	"Organize your users": "사용자를 ",
 	"Other": "기타",
 	"Other": "기타",
 	"OUTPUT": "출력력",
 	"OUTPUT": "출력력",
 	"Output format": "출력 형식",
 	"Output format": "출력 형식",
@@ -702,7 +702,7 @@
 	"Permission denied when accessing media devices": "미디어 장치 접근 권한이 거부되었습니다.",
 	"Permission denied when accessing media devices": "미디어 장치 접근 권한이 거부되었습니다.",
 	"Permission denied when accessing microphone": "마이크 접근 권한이 거부되었습니다.",
 	"Permission denied when accessing microphone": "마이크 접근 권한이 거부되었습니다.",
 	"Permission denied when accessing microphone: {{error}}": "마이크 접근 권환이 거부되었습니다: {{error}}",
 	"Permission denied when accessing microphone: {{error}}": "마이크 접근 권환이 거부되었습니다: {{error}}",
-	"Permissions": "",
+	"Permissions": "권한",
 	"Personalization": "개인화",
 	"Personalization": "개인화",
 	"Pin": "고정",
 	"Pin": "고정",
 	"Pinned": "고정됨",
 	"Pinned": "고정됨",
@@ -710,16 +710,16 @@
 	"Pipeline deleted successfully": "성공적으로 파이프라인이 삭제되었습니다",
 	"Pipeline deleted successfully": "성공적으로 파이프라인이 삭제되었습니다",
 	"Pipeline downloaded successfully": "성공적으로 파이프라인이 설치되었습니다",
 	"Pipeline downloaded successfully": "성공적으로 파이프라인이 설치되었습니다",
 	"Pipelines": "파이프라인",
 	"Pipelines": "파이프라인",
-	"Pipelines Not Detected": "파이프라인 발견되지않음",
+	"Pipelines Not Detected": "파이프라인 발견되지 않음",
 	"Pipelines Valves": "파이프라인 밸브",
 	"Pipelines Valves": "파이프라인 밸브",
 	"Plain text (.txt)": "일반 텍스트(.txt)",
 	"Plain text (.txt)": "일반 텍스트(.txt)",
 	"Playground": "놀이터",
 	"Playground": "놀이터",
 	"Please carefully review the following warnings:": "다음 주의를 조심히 확인해주십시오",
 	"Please carefully review the following warnings:": "다음 주의를 조심히 확인해주십시오",
-	"Please enter a prompt": "프롬트를 입력해주세요",
+	"Please enter a prompt": "프롬트를 입력해주세요",
 	"Please fill in all fields.": "모두 빈칸없이 채워주세요",
 	"Please fill in all fields.": "모두 빈칸없이 채워주세요",
 	"Please select a model first.": "",
 	"Please select a model first.": "",
-	"Please select a reason": "이유를 선택주세요",
-	"Port": "",
+	"Please select a reason": "이유를 선택주세요",
+	"Port": "포트",
 	"Positive attitude": "긍정적인 자세",
 	"Positive attitude": "긍정적인 자세",
 	"Prefix ID": "",
 	"Prefix ID": "",
 	"Prefix ID is used to avoid conflicts with other connections by adding a prefix to the model IDs - leave empty to disable": "",
 	"Prefix ID is used to avoid conflicts with other connections by adding a prefix to the model IDs - leave empty to disable": "",
@@ -728,11 +728,11 @@
 	"Profile Image": "프로필 이미지",
 	"Profile Image": "프로필 이미지",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "프롬프트 (예: 로마 황제에 대해 재미있는 사실을 알려주세요)",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "프롬프트 (예: 로마 황제에 대해 재미있는 사실을 알려주세요)",
 	"Prompt Content": "프롬프트 내용",
 	"Prompt Content": "프롬프트 내용",
-	"Prompt created successfully": "",
+	"Prompt created successfully": "성공적으로 프롬프트를 생성했습니다",
 	"Prompt suggestions": "프롬프트 제안",
 	"Prompt suggestions": "프롬프트 제안",
-	"Prompt updated successfully": "",
+	"Prompt updated successfully": "성공적으로 프롬프트를 수정했습니다",
 	"Prompts": "프롬프트",
 	"Prompts": "프롬프트",
-	"Prompts Access": "",
+	"Prompts Access": "프롬프트 접근",
 	"Proxy URL": "프록시 URL",
 	"Proxy URL": "프록시 URL",
 	"Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com에서 \"{{searchValue}}\" 가져오기",
 	"Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com에서 \"{{searchValue}}\" 가져오기",
 	"Pull a model from Ollama.com": "Ollama.com에서 모델 가져오기(pull)",
 	"Pull a model from Ollama.com": "Ollama.com에서 모델 가져오기(pull)",
@@ -792,13 +792,13 @@
 	"Search a model": "모델 검색",
 	"Search a model": "모델 검색",
 	"Search Base": "",
 	"Search Base": "",
 	"Search Chats": "채팅 검색",
 	"Search Chats": "채팅 검색",
-	"Search Collection": "컬렉션검색",
-	"Search Filters": "",
+	"Search Collection": "컬렉션 검색",
+	"Search Filters": "필터 검색",
 	"search for tags": "태그 검색",
 	"search for tags": "태그 검색",
 	"Search Functions": "함수 검색",
 	"Search Functions": "함수 검색",
 	"Search Knowledge": "지식 기반 검색",
 	"Search Knowledge": "지식 기반 검색",
 	"Search Models": "모델 검색",
 	"Search Models": "모델 검색",
-	"Search options": "",
+	"Search options": "옵션 검색",
 	"Search Prompts": "프롬프트 검색",
 	"Search Prompts": "프롬프트 검색",
 	"Search Result Count": "검색 결과 수",
 	"Search Result Count": "검색 결과 수",
 	"Search the web": "",
 	"Search the web": "",
@@ -895,7 +895,7 @@
 	"System Instructions": "시스템 설명서",
 	"System Instructions": "시스템 설명서",
 	"System Prompt": "시스템 프롬프트",
 	"System Prompt": "시스템 프롬프트",
 	"Tags Generation": "태그 생성",
 	"Tags Generation": "태그 생성",
-	"Tags Generation Prompt": "태그 생성 프롬포트트",
+	"Tags Generation Prompt": "태그 생성 프롬트",
 	"Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting. (default: 1)": "",
 	"Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting. (default: 1)": "",
 	"Tap to interrupt": "탭하여 중단",
 	"Tap to interrupt": "탭하여 중단",
 	"Tavily API Key": "Tavily API 키",
 	"Tavily API Key": "Tavily API 키",
@@ -963,16 +963,16 @@
 	"Too verbose": "말이 너무 많은",
 	"Too verbose": "말이 너무 많은",
 	"Tool created successfully": "성공적으로 도구가 생성되었습니다",
 	"Tool created successfully": "성공적으로 도구가 생성되었습니다",
 	"Tool deleted successfully": "성공적으로 도구가 삭제되었습니다",
 	"Tool deleted successfully": "성공적으로 도구가 삭제되었습니다",
-	"Tool Description": "",
-	"Tool ID": "",
+	"Tool Description": "도구 설명",
+	"Tool ID": "도구 ID",
 	"Tool imported successfully": "성공적으로 도구를 가져왔습니다",
 	"Tool imported successfully": "성공적으로 도구를 가져왔습니다",
-	"Tool Name": "",
+	"Tool Name": "도구 이름",
 	"Tool updated successfully": "성공적으로 도구가 업데이트되었습니다",
 	"Tool updated successfully": "성공적으로 도구가 업데이트되었습니다",
 	"Tools": "도구",
 	"Tools": "도구",
-	"Tools Access": "",
-	"Tools are a function calling system with arbitrary code execution": "도구는 임의 코드를 실행시키는 함수를 불러오는 시스템입니다",
-	"Tools have a function calling system that allows arbitrary code execution": "도구가 임이의 코드를 실행시키는 함수를 가지기",
-	"Tools have a function calling system that allows arbitrary code execution.": "도구가 임이의 코드를 실행시키는 함수를 가지고 있습니다.",
+	"Tools Access": "도구 접근",
+	"Tools are a function calling system with arbitrary code execution": "도구는 임의 코드를 실행시키는 함수를 불러오는 시스템입니다",
+	"Tools have a function calling system that allows arbitrary code execution": "도구에 임의 코드 실행을 허용하는 함수가 포함되어 있습니다",
+	"Tools have a function calling system that allows arbitrary code execution.": "도구에 임의 코드 실행을 허용하는 함수가 포함되어 있습니다.",
 	"Top K": "Top K",
 	"Top K": "Top K",
 	"Top P": "Top P",
 	"Top P": "Top P",
 	"Transformers": "",
 	"Transformers": "",
@@ -984,9 +984,9 @@
 	"Type Hugging Face Resolve (Download) URL": "Hugging Face Resolve (다운로드) URL 입력",
 	"Type Hugging Face Resolve (Download) URL": "Hugging Face Resolve (다운로드) URL 입력",
 	"Uh-oh! There was an issue with the response.": "",
 	"Uh-oh! There was an issue with the response.": "",
 	"UI": "UI",
 	"UI": "UI",
-	"Unarchive All": "",
-	"Unarchive All Archived Chats": "",
-	"Unarchive Chat": "",
+	"Unarchive All": "모두 보관 해제",
+	"Unarchive All Archived Chats": "보관된 모든 채팅을 보관 해제",
+	"Unarchive Chat": "채팅 보관 해제",
 	"Unlock mysteries": "",
 	"Unlock mysteries": "",
 	"Unpin": "고정 해제",
 	"Unpin": "고정 해제",
 	"Unravel secrets": "",
 	"Unravel secrets": "",
@@ -1009,7 +1009,7 @@
 	"URL Mode": "URL 모드",
 	"URL Mode": "URL 모드",
 	"Use '#' in the prompt input to load and include your knowledge.": "프롬프트 입력에서 '#'를 사용하여 지식 기반을 불러오고 포함하세요.",
 	"Use '#' in the prompt input to load and include your knowledge.": "프롬프트 입력에서 '#'를 사용하여 지식 기반을 불러오고 포함하세요.",
 	"Use Gravatar": "Gravatar 사용",
 	"Use Gravatar": "Gravatar 사용",
-	"Use groups to group your users and assign permissions.": "",
+	"Use groups to group your users and assign permissions.": "그룹을 사용하여 사용자를 그룹화하고 권한을 할당하세요.",
 	"Use Initials": "초성 사용",
 	"Use Initials": "초성 사용",
 	"use_mlock (Ollama)": "use_mlock (올라마)",
 	"use_mlock (Ollama)": "use_mlock (올라마)",
 	"use_mmap (Ollama)": "use_mmap (올라마)",
 	"use_mmap (Ollama)": "use_mmap (올라마)",
@@ -1034,7 +1034,7 @@
 	"Voice Input": "음성 입력",
 	"Voice Input": "음성 입력",
 	"Warning": "경고",
 	"Warning": "경고",
 	"Warning:": "주의:",
 	"Warning:": "주의:",
-	"Warning: Enabling this will allow users to upload arbitrary code on the server.": "",
+	"Warning: Enabling this will allow users to upload arbitrary code on the server.": "주의: 이 기능을 활성화하면 사용자가 서버에 임의 코드를 업로드할 수 있습니다.",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "주의: 기존 임베딩 모델을 변경 또는 업데이트하는 경우, 모든 문서를 다시 가져와야 합니다.",
 	"Warning: If you update or change your embedding model, you will need to re-import all documents.": "주의: 기존 임베딩 모델을 변경 또는 업데이트하는 경우, 모든 문서를 다시 가져와야 합니다.",
 	"Web": "웹",
 	"Web": "웹",
 	"Web API": "웹 API",
 	"Web API": "웹 API",
@@ -1047,8 +1047,8 @@
 	"WebUI URL": "",
 	"WebUI URL": "",
 	"WebUI will make requests to \"{{url}}/api/chat\"": "WebUI가 \"{{url}}/api/chat\"로 요청을 보냅니다",
 	"WebUI will make requests to \"{{url}}/api/chat\"": "WebUI가 \"{{url}}/api/chat\"로 요청을 보냅니다",
 	"WebUI will make requests to \"{{url}}/chat/completions\"": "WebUI가 \"{{url}}/chat/completions\"로 요청을 보냅니다",
 	"WebUI will make requests to \"{{url}}/chat/completions\"": "WebUI가 \"{{url}}/chat/completions\"로 요청을 보냅니다",
-	"What are you trying to achieve?": "",
-	"What are you working on?": "",
+	"What are you trying to achieve?": "무엇을 성취하고 싶으신가요?",
+	"What are you working on?": "어떤 작업을 하고 계신가요?",
 	"What’s New in": "새로운 기능:",
 	"What’s New in": "새로운 기능:",
 	"When enabled, the model will respond to each chat message in real-time, generating a response as soon as the user sends a message. This mode is useful for live chat applications, but may impact performance on slower hardware.": "활성화하면 모델이 각 채팅 메시지에 실시간으로 응답하여 사용자가 메시지를 보내는 즉시 응답을 생성합니다. 이 모드는 실시간 채팅 애플리케이션에 유용하지만, 느린 하드웨어에서는 성능에 영향을 미칠 수 있습니다.",
 	"When enabled, the model will respond to each chat message in real-time, generating a response as soon as the user sends a message. This mode is useful for live chat applications, but may impact performance on slower hardware.": "활성화하면 모델이 각 채팅 메시지에 실시간으로 응답하여 사용자가 메시지를 보내는 즉시 응답을 생성합니다. 이 모드는 실시간 채팅 애플리케이션에 유용하지만, 느린 하드웨어에서는 성능에 영향을 미칠 수 있습니다.",
 	"wherever you are": "",
 	"wherever you are": "",
@@ -1058,12 +1058,12 @@
 	"Won": "승리",
 	"Won": "승리",
 	"Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text. (Default: 0.9)": "",
 	"Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text. (Default: 0.9)": "",
 	"Workspace": "워크스페이스",
 	"Workspace": "워크스페이스",
-	"Workspace Permissions": "",
+	"Workspace Permissions": "워크스페이스 권한",
 	"Write": "",
 	"Write": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "프롬프트 제안 작성 (예: 당신은 누구인가요?)",
 	"Write a prompt suggestion (e.g. Who are you?)": "프롬프트 제안 작성 (예: 당신은 누구인가요?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "[주제 또는 키워드]에 대한 50단어 요약문 작성.",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "[주제 또는 키워드]에 대한 50단어 요약문 작성.",
 	"Write something...": "아무거나 쓰세요...",
 	"Write something...": "아무거나 쓰세요...",
-	"Write your model template content here": "",
+	"Write your model template content here": "여기에 모델 템플릿 내용을 입력하세요",
 	"Yesterday": "어제",
 	"Yesterday": "어제",
 	"You": "당신",
 	"You": "당신",
 	"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "동시에 최대 {{maxCount}} 파일과만 대화할 수 있습니다 ",
 	"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "동시에 최대 {{maxCount}} 파일과만 대화할 수 있습니다 ",

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

@@ -494,7 +494,7 @@
 	"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "我已阅读并理解我的行为所带来的影响,明白执行任意代码所涉及的风险。且我已验证代码来源可信度。",
 	"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "我已阅读并理解我的行为所带来的影响,明白执行任意代码所涉及的风险。且我已验证代码来源可信度。",
 	"ID": "ID",
 	"ID": "ID",
 	"Ignite curiosity": "点燃好奇心",
 	"Ignite curiosity": "点燃好奇心",
-	"Image": "图像",
+	"Image": "图像生成",
 	"Image Compression": "图像压缩",
 	"Image Compression": "图像压缩",
 	"Image generation": "图像生成",
 	"Image generation": "图像生成",
 	"Image Generation": "图像生成",
 	"Image Generation": "图像生成",
@@ -934,7 +934,7 @@
 	"This will delete all models including custom models and cannot be undone.": "这将删除所有模型,包括自定义模型,且无法撤销。",
 	"This will delete all models including custom models and cannot be undone.": "这将删除所有模型,包括自定义模型,且无法撤销。",
 	"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": "解释较为详细",
-	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}}": "思考 {{DURATION}}",
 	"Tika": "Tika",
 	"Tika": "Tika",
 	"Tika Server URL required.": "请输入 Tika 服务器地址。",
 	"Tika Server URL required.": "请输入 Tika 服务器地址。",
 	"Tiktoken": "Tiktoken",
 	"Tiktoken": "Tiktoken",

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

@@ -934,7 +934,7 @@
 	"This will delete all models including custom models and cannot be undone.": "這將刪除所有模型,包括自訂模型,且無法復原。",
 	"This will delete all models including custom models and cannot be undone.": "這將刪除所有模型,包括自訂模型,且無法復原。",
 	"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": "詳細解釋",
-	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}}": "{{DURATION}} 思考中",
 	"Tika": "Tika",
 	"Tika": "Tika",
 	"Tika Server URL required.": "需要 Tika 伺服器 URL。",
 	"Tika Server URL required.": "需要 Tika 伺服器 URL。",
 	"Tiktoken": "Tiktoken",
 	"Tiktoken": "Tiktoken",

+ 18 - 3
src/lib/utils/index.ts

@@ -5,10 +5,12 @@ import dayjs from 'dayjs';
 import relativeTime from 'dayjs/plugin/relativeTime';
 import relativeTime from 'dayjs/plugin/relativeTime';
 import isToday from 'dayjs/plugin/isToday';
 import isToday from 'dayjs/plugin/isToday';
 import isYesterday from 'dayjs/plugin/isYesterday';
 import isYesterday from 'dayjs/plugin/isYesterday';
+import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 
 dayjs.extend(relativeTime);
 dayjs.extend(relativeTime);
 dayjs.extend(isToday);
 dayjs.extend(isToday);
 dayjs.extend(isYesterday);
 dayjs.extend(isYesterday);
+dayjs.extend(localizedFormat);
 
 
 import { WEBUI_BASE_URL } from '$lib/constants';
 import { WEBUI_BASE_URL } from '$lib/constants';
 import { TTS_RESPONSE_SPLIT } from '$lib/types';
 import { TTS_RESPONSE_SPLIT } from '$lib/types';
@@ -295,11 +297,11 @@ export const formatDate = (inputDate) => {
 	const now = dayjs();
 	const now = dayjs();
 
 
 	if (date.isToday()) {
 	if (date.isToday()) {
-		return `Today at ${date.format('HH:mm')}`;
+		return `Today at ${date.format('LT')}`;
 	} else if (date.isYesterday()) {
 	} else if (date.isYesterday()) {
-		return `Yesterday at ${date.format('HH:mm')}`;
+		return `Yesterday at ${date.format('LT')}`;
 	} else {
 	} else {
-		return `${date.format('DD/MM/YYYY')} at ${date.format('HH:mm')}`;
+		return `${date.format('L')} at ${date.format('LT')}`;
 	}
 	}
 };
 };
 
 
@@ -764,6 +766,19 @@ export const blobToFile = (blob, fileName) => {
 	return file;
 	return file;
 };
 };
 
 
+export const getPromptVariables = (user_name, user_location) => {
+	return {
+		'{{USER_NAME}}': user_name,
+		'{{USER_LOCATION}}': user_location || 'Unknown',
+		'{{CURRENT_DATETIME}}': getCurrentDateTime(),
+		'{{CURRENT_DATE}}': getFormattedDate(),
+		'{{CURRENT_TIME}}': getFormattedTime(),
+		'{{CURRENT_WEEKDAY}}': getWeekday(),
+		'{{CURRENT_TIMEZONE}}': getUserTimezone(),
+		'{{USER_LANGUAGE}}': localStorage.getItem('locale') || 'en-US'
+	};
+};
+
 /**
 /**
  * @param {string} template - The template string containing placeholders.
  * @param {string} template - The template string containing placeholders.
  * @returns {string} The template string with the placeholders replaced by the prompt.
  * @returns {string} The template string with the placeholders replaced by the prompt.

+ 3 - 1
src/routes/s/[id]/+page.svelte

@@ -16,8 +16,10 @@
 	import { getUserById } from '$lib/apis/users';
 	import { getUserById } from '$lib/apis/users';
 	import { getModels } from '$lib/apis';
 	import { getModels } from '$lib/apis';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
+    import localizedFormat from 'dayjs/plugin/localizedFormat';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
+    dayjs.extend(localizedFormat);
 
 
 	let loaded = false;
 	let loaded = false;
 
 
@@ -138,7 +140,7 @@
 
 
 						<div class="flex text-sm justify-between items-center mt-1">
 						<div class="flex text-sm justify-between items-center mt-1">
 							<div class="text-gray-400">
 							<div class="text-gray-400">
-								{dayjs(chat.chat.timestamp).format($i18n.t('MMMM DD, YYYY'))}
+								{dayjs(chat.chat.timestamp).format('LLL')}
 							</div>
 							</div>
 						</div>
 						</div>
 					</div>
 					</div>